@xgamesfun/solana 0.20.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +10 -10
  2. package/examples/coin-flip-arena.json +2 -7
  3. package/examples/elements-set.json +2 -9
  4. package/examples/expedition-guild.json +1 -8
  5. package/examples/guild-missions.json +3 -10
  6. package/examples/halving-seasons.json +4 -9
  7. package/examples/hero-trials.json +1 -4
  8. package/examples/merge-2048.json +5 -5
  9. package/examples/paper-hands-farm.json +4 -16
  10. package/examples/partner-vault.json +1 -4
  11. package/examples/stakers-draw.json +1 -8
  12. package/examples/star-fleet.json +1 -8
  13. package/examples/starforce.json +6 -6
  14. package/js/approve/details.mjs +8 -7
  15. package/js/approve/server.mjs +20 -5
  16. package/js/chain/borsh.js +9 -0
  17. package/js/chain/core.js +14 -2
  18. package/js/chain/lookup.js +8 -2
  19. package/js/chain/rpc.js +4 -2
  20. package/js/client/accounts.js +448 -116
  21. package/js/client/grant.js +125 -66
  22. package/js/client/idl/loot.js +1 -1
  23. package/js/client/idl/loot.json +2360 -2218
  24. package/js/client/index.js +1 -0
  25. package/js/client/ix.js +1204 -473
  26. package/js/client/pda.js +20 -16
  27. package/js/client/policy.js +44 -23
  28. package/js/client/random.js +70 -0
  29. package/js/client/sizes.js +255 -12
  30. package/js/economy/check.mjs +455 -210
  31. package/js/economy/cost.mjs +45 -30
  32. package/js/economy/examples.mjs +2 -2
  33. package/js/economy/explain.mjs +24 -20
  34. package/js/economy/guide.mjs +31 -30
  35. package/js/economy/live.mjs +30 -7
  36. package/js/economy/model.mjs +171 -56
  37. package/js/economy/plan.mjs +380 -112
  38. package/js/economy/schema.json +291 -143
  39. package/js/economy/simulate.mjs +154 -87
  40. package/js/mcp/tools.mjs +14 -8
  41. package/js/play/index.js +1121 -375
  42. package/js/play/keys.js +167 -0
  43. package/package.json +1 -1
  44. package/release.json +5 -5
  45. package/skills/xsolana-economy/SKILL.md +15 -11
  46. package/skills/xsolana-economy/references/after-launch.md +37 -27
  47. package/skills/xsolana-economy/references/apps.md +55 -19
  48. package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
  49. package/skills/xsolana-economy/references/telling-the-person.md +52 -11
  50. package/types/chain/core.d.ts +3 -1
  51. package/types/chain/lookup.d.ts +5 -2
  52. package/types/chain/rpc.d.ts +3 -1
  53. package/types/client/accounts.d.ts +195 -55
  54. package/types/client/grant.d.ts +72 -27
  55. package/types/client/idl/loot.d.ts +590 -559
  56. package/types/client/index.d.ts +1 -0
  57. package/types/client/ix.d.ts +3862 -234
  58. package/types/client/pda.d.ts +19 -15
  59. package/types/client/policy.d.ts +39 -14
  60. package/types/client/random.d.ts +26 -0
  61. package/types/client/sizes.d.ts +93 -5
  62. package/types/economy/check.d.mts +5 -10
  63. package/types/economy/cost.d.mts +2 -1
  64. package/types/economy/live.d.mts +16 -1
  65. package/types/economy/model.d.mts +105 -37
  66. package/types/economy/plan.d.mts +193 -192
  67. package/types/economy/simulate.d.mts +21 -13
  68. package/types/mcp/tools.d.mts +21 -13
  69. package/types/play/index.d.ts +450 -39
  70. package/types/play/keys.d.ts +51 -0
@@ -16,12 +16,13 @@ export const FEES = Object.freeze({ player: 2_000_000n, project: 10_000n });
16
16
  * on `open_box` (order.rs), whoever sends `draw_lottery` (lottery.rs, its `payer`), the authority on `roll`
17
17
  * (roll.rs). Measured on devnet: 500,000 lamports per open.
18
18
  */
19
- export const ORACLE_FEE = 500_000n;
19
+ export const ORACLE_FEE = 800_000n;
20
20
  /**
21
- * Put in the order itself on every purchase, with the player fee on top of it (order.rs: ESCROW_LAMPORTS + fee); what
22
- * a collectible's rent doesn't use goes back at settle, and a refund returns all of it (constants.rs).
21
+ * What js/play's throwaway settle key is funded with, in the purchase's own transaction, for each prize its settle may
22
+ * open or mint (`loot.SETTLE_RENT_UNIT`): whoever settles pays that rent, the order lends nothing. What the settle
23
+ * doesn't spend goes back to the player when the key empties itself, and all of it when the roll fails.
23
24
  */
24
- export const ESCROW_LAMPORTS = 6_000_000n;
25
+ export const SETTLE_RENT_UNIT = 6_000_000n;
25
26
  /** Rent: (bytes + 128) × 5,080 lamports, as `solana rent` reads it on devnet and mainnet-beta. */
26
27
  export const RENT_PER_BYTE = 5_080n;
27
28
  export const ACCOUNT_OVERHEAD = 128n;
@@ -41,10 +42,17 @@ export const COLLECTIBLE_RENT = 3_100_000n;
41
42
  export const SIGNATURE_FEE = 5_000n;
42
43
  export const LAMPORTS_PER_SOL = 1_000_000_000n;
43
44
  export const WSOL = 'So11111111111111111111111111111111111111112';
44
- /** The program's limits. */
45
- export const LIMITS = Object.freeze({ shares: 4, entries: 8, callbackAccounts: 21, lines: 8, collectiblesPerCraft: 8, places: 8, tiers: 4, boosts: 4, growths: 4, gates: 4, odds: 4, vouchers: 3, deciders: 3, gateWallets: 16, nameBytes: 32, uriBytes: 200, keyBytes: 32, maxBoostBps: 1_000_000, maxTierBps: 110_000 });
46
- /** Attribute names the program writes itself, which a recipe cannot copy and `Set` cannot change (craft.rs, make.rs). */
47
- export const RESERVED_ATTRIBUTES = Object.freeze(['made', 'used', 'used_at', 'item_id', 'rarity', 'serial', 'origin']);
45
+ /** The limits a spec meets: the chain's (the oracle's answer carries 21 accounts; a farm names a tier and a recipe a
46
+ * collectible's place in a byte; Bubblegum keeps a compressed NFT's link in 200 bytes and its name in 32) and the two
47
+ * caps that keep a farm's numbers from overflowing. Since loot 0.22 the program caps no count or size of its own. */
48
+ export const LIMITS = Object.freeze({
49
+ callbackAccounts: 21, tiers: 256, collectiblesPerCraft: 255, vouchers: 3, compressedNameBytes: 32, compressedUriBytes: 200, maxBoostBps: 1_000_000, maxTierBps: 110_000,
50
+ // The program's caps on a record's lists (constants.rs, not in the IDL): a spec over one is refused before anything is signed.
51
+ boxEntries: 128, boxSlots: 32, boxOdds: 32, boxFates: 64, entryPrizes: 14, itemTraits: 64, traitKeyBytes: 64, recipeLines: 48, farmRules: 32, growthSteps: 128, contestSides: 1024, deciders: 32, collections: 32, lotteryRuns: 64, farmEnds: 100_000,
52
+ });
53
+ /** Attribute names the program writes itself, which a recipe cannot copy and `Set` cannot change (craft.rs, make.rs),
54
+ * and the two a collectible's life keeps (`lifeLines`: its uses and its next use). */
55
+ export const RESERVED_ATTRIBUTES = Object.freeze(['made', 'used', 'next_use', 'item_id', 'rarity', 'serial', 'origin']);
48
56
  export const RARITIES = Object.freeze(['Common', 'Uncommon', 'Rare', 'Epic', 'Legendary', 'Mythic']);
49
57
 
50
58
  // ---------------------------------------------------------------- units
@@ -111,8 +119,8 @@ export function toSeconds(duration) {
111
119
  }
112
120
 
113
121
  /**
114
- * How long what lands in a farm's rewards pool takes to be released to its seats, in seconds (a farm that shares its
115
- * pool, `farm.rs` `release_span`): its longest lock tier, and a day at least.
122
+ * How long what lands in a farm's rewards vault takes to be released to its seats, in seconds (a farm that shares what
123
+ * lands, `farm.rs` `release_span`): its longest lock tier, and a day at least.
116
124
  */
117
125
  export const releaseSeconds = (f) => Math.max(86_400, ...(f.tiers ?? []).map((t) => toSeconds(t.lock ?? 0)));
118
126
 
@@ -123,16 +131,54 @@ export function releaseWords(f) {
123
131
  return s % 3_600 === 0 ? `${s / 3_600} hours` : `${s} seconds`;
124
132
  }
125
133
 
134
+ /**
135
+ * What a run sent late does, in words (schedule.rs `catches_up`): a sealed schedule's fixed amount pays every run due;
136
+ * any other run pays once, for the latest run due, and the runs before it lapse.
137
+ */
138
+ export const lateRunWords = (s) => (s.sealed && s.percent === undefined ? 'a run sent late pays every run due since' : 'a run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse');
139
+
140
+ /**
141
+ * Whether a farm's boost reads a number its own growth writes (`refresh_seat` writes it): each level counts from when it is
142
+ * written, so a page or a keeper should refresh seats.
143
+ */
144
+ export const boostReadsGrowth = (f) => (f.boosts ?? []).some((b) => b.when?.attribute && listOf(f.growth).some((g) => b.when.attribute === (g.attribute ?? 'xp') || ((g.levels ?? []).length > 0 && b.when.attribute === (g.level ?? 'level'))));
145
+
146
+ /** A box's roll against what is in stock, in words (order.rs): the odds stay as written, whatever is left. */
147
+ export const STOCK_WORDS = 'The odds stay as written whatever is in stock: a draw that lands on an outcome that can’t be given then (its pool short of the whole amount, its item or stash gone) is not made, no other outcome takes its place, and that draw’s worth of the price goes back at the settle; with no draw made at all, a paid open fails and is refunded, and a free roll goes back to waiting';
148
+
149
+ /**
150
+ * What a box that rolls does once an outcome can't be given (`whenOut`, order.rs `WhenOut`), in words. `sold`: it is
151
+ * sold (an open checks the stock); a box only other routes roll (a prize, a mission) never stops a sale.
152
+ */
153
+ export function whenOutWords(b, { sold = b.active !== false } = {}) {
154
+ if (b.whenOut === 'drawLeft' && !sold)
155
+ return 'When an outcome runs out (an item sold out, a pool short of its prize, a stash empty), a roll draws among the outcomes still there by their written weights, so the others’ chances rise; with nothing left at all, a free roll waits to be asked again and a paid one is refunded';
156
+ if (b.whenOut === 'drawLeft')
157
+ return 'When an outcome runs out (an item sold out, a pool short of its prize, a stash empty), the box keeps selling while its first draw has anything left, and a roll draws among the outcomes still there by their written weights: the others’ chances rise as one runs out, so a player can empty a common outcome to make a rare one likelier (a box selling a rare prize should stop selling instead, the default)';
158
+ if (!sold) return STOCK_WORDS;
159
+ return 'When an outcome runs out, the box stops selling: it isn’t sold while any outcome couldn’t cover this purchase’s draws and those of purchases still waiting for the oracle (an item’s units left, a pool’s tokens, a stash’s NFTs), so every roll is on the odds as written and nobody can buy their way to a refund on every miss; it sells again once the project restocks or edits the box (an outcome whose odds shrink with stock needs no cover). An outcome emptied another way all the same (another route sharing its pool, item or stash) makes no draw, no other outcome taking its place, and gives back that draw’s worth of the price at the settle; with no draw made at all, the open is refunded';
160
+ }
161
+
126
162
  /**
127
163
  * What the end of a sharing farm that `runs` means, in words, or null (no end, or a fixed rate): its release keeps its
128
- * pace, so what it has not let go by the end stays in the pool, the project's, and so does what a seat gives back after
129
- * the end; a seat that may be dead (a watched NFT sold, an upkeep run out) is refreshed a release span before it.
164
+ * pace, so what it has not let go by the end stays in its vault, free for the authority to take back, and so does what
165
+ * a seat forfeits after the end.
130
166
  */
131
167
  export function farmEndWords(f) {
132
168
  if (!f.runs || f.rate) return null;
133
169
  const span = releaseWords(f);
134
- const dead = [f.custody === 'watch' && 'a watched NFT sold', f.upkeepAttribute && 'an upkeep run out'].filter(Boolean);
135
- return `what is not released by its end stays in the pool, the project’s, credited to nobody, and so does what a seat gives back after the end: fund it at least ${span} before its end (what lands later is released only in part, and what lands at or after the end not at all)${dead.length ? `, and refresh a seat you suspect is dead (${dead.join(', ')}) at least ${span} before the end, or what it was credited stays in the pool too` : ''}`;
170
+ return `what is not released by its end stays in its vault, credited to nobody (the authority may take back what is free there), and so does what a seat forfeits after the end: fund it at least ${span} before its end (what lands later is released only in part, and what lands at or after the end not at all)`;
171
+ }
172
+
173
+ /**
174
+ * The token a farm's rewards are in (`rewards`, a token key): by default its staked token, or for a farm of
175
+ * collectibles the project's currency. One that names a pool (as farms did before they had a vault of their own) is
176
+ * read as that pool's token, and the checker refuses it.
177
+ */
178
+ export function farmRewardsToken(f, tokens, currencyKey, poolCurrency = new Map()) {
179
+ if (f.rewards && tokens.has(f.rewards)) return f.rewards;
180
+ if (f.rewards && poolCurrency.has(f.rewards)) return poolCurrency.get(f.rewards);
181
+ return f.stakes && f.stakes !== 'collectibles' ? f.stakes : currencyKey;
136
182
  }
137
183
 
138
184
  /** A raise's or a shift's `by`: a number, or for a time a duration, which may be negative ("-1d"). */
@@ -206,21 +252,14 @@ export function model(economy, { now = 0 } = {}) {
206
252
  const tokens = new Map((e.tokens ?? []).map((t) => [t.key, tokenOf(t)]));
207
253
  const currencyKey = e.currency ?? (tokens.size === 1 ? [...tokens.keys()][0] : undefined);
208
254
  const currency = tokens.get(currencyKey);
209
- // A farm that names no rewards pool pays nothing (staking only to qualify): it gets an empty pool of its own, which
210
- // the program needs to exist (farm.rs) and nothing ever fills. Once live, its number is kept on the farm
211
- // (`rewardsIndex`, live.mjs `recordLanded`), so a pool added later never takes it.
212
- const unpaid = (e.farms ?? []).filter((f) => !f.rewards);
213
- const hidden = unpaid.map((f) => ({ key: `${f.key}-rewards`, currency: f.stakes !== 'collectibles' ? f.stakes : currencyKey, about: `${f.key}: pays nothing`, farm: f.key, ...(Number.isInteger(f.rewardsIndex) ? { index: f.rewardsIndex } : {}) }));
214
- const listed = [...(e.pools ?? []), ...hidden];
215
- // Numbers are given in this order: the pools that have one, then the empty pool of a farm already live that has no
216
- // number written (it went live before numbers were kept, and took the first free one then, before any pool added
217
- // since), then the rest in the spec's order.
218
- const landed = (key) => Number.isInteger(unpaid.find((f) => f.key === key)?.id);
219
- const rank = (p) => (Number.isInteger(p.index) ? 0 : p.farm && landed(p.farm) ? 1 : 2);
220
- const order = [...listed.keys()].sort((a, b) => rank(listed[a]) - rank(listed[b]));
255
+ // Numbers are given in this order: the pools that have one, then the rest in the spec's order.
256
+ const listed = e.pools ?? [];
257
+ const order = [...listed.keys()].sort((a, b) => Number(!Number.isInteger(listed[a].index)) - Number(!Number.isInteger(listed[b].index)));
221
258
  const assigned = assignIds(order.map((i) => listed[i]), 'index', 0);
222
259
  const pools = listed.map((_, i) => assigned[order.indexOf(i)]);
223
- const farms = (e.farms ?? []).map((f) => (f.rewards ? f : { ...f, rewards: `${f.key}-rewards`, unpaid: true }));
260
+ // A farm's rewards land in a vault of its own: `rewardsToken` is their token.
261
+ const poolCurrency = new Map(pools.map((p) => [p.key, p.currency]));
262
+ const farms = (e.farms ?? []).map((f) => ({ ...f, rewardsToken: farmRewardsToken(f, tokens, currencyKey, poolCurrency) }));
224
263
  // A raffle: its NFT is first place's prize, held in the lottery's own stash.
225
264
  const lotteries = (e.lotteries ?? []).map((l) => {
226
265
  if (!l.raffle) return l;
@@ -247,6 +286,7 @@ export function model(economy, { now = 0 } = {}) {
247
286
  items: assignIds(e.items ?? [], 'id', 1),
248
287
  pools,
249
288
  stashes: assignIds(e.stashes ?? [], 'id', 1),
289
+ tables: assignIds(e.tables ?? [], 'id', 1),
250
290
  boxes: [],
251
291
  payments: e.payments ?? [],
252
292
  recipes: assignIds(e.recipes ?? [], 'id', 1),
@@ -290,6 +330,7 @@ export function model(economy, { now = 0 } = {}) {
290
330
  m.item = index(m.items);
291
331
  m.pool = index(m.pools);
292
332
  m.stash = index(m.stashes);
333
+ m.table = index(m.tables);
293
334
  m.box = index(m.allBoxes);
294
335
  m.recipe = index(m.recipes);
295
336
  m.farm = index(m.farms);
@@ -312,16 +353,17 @@ export function model(economy, { now = 0 } = {}) {
312
353
  */
313
354
  export function stashNfts(s) {
314
355
  return (s.nfts ?? []).map((n) => {
315
- if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null };
316
- if (n.mint) return { kind: 'token-metadata', asset: n.mint, programmable: Boolean(n.programmable), ruleSet: n.ruleSet ?? null };
317
- if (n.compressed) return { kind: 'compressed', asset: n.compressed };
318
- return { kind: 'core', asset: n.asset, collection: n.collection ?? s.collection ?? null };
356
+ if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null, weight: 1 };
357
+ const weight = n.weight ?? 1;
358
+ if (n.mint) return { kind: 'token-metadata', asset: n.mint, programmable: Boolean(n.programmable), ruleSet: n.ruleSet ?? null, weight };
359
+ if (n.compressed) return { kind: 'compressed', asset: n.compressed, weight };
360
+ return { kind: 'core', asset: n.asset, collection: n.collection ?? s.collection ?? null, weight };
319
361
  });
320
362
  }
321
363
 
322
364
  /**
323
365
  * A prize as the spec writes it (`$defs/prize`), read the one way: a line { gives, … } as a recipe's `out` lines are:
324
- * "item" (`item`), "tokens" (`amount`, `from` "mint" with `currency`, or "pool:<key>"), "share" (`percent` of `pool`),
366
+ * "item" (`item`), "tokens" (`amount`, `from` "mint" with `currency`, or "pool:<key>"),
325
367
  * "nft" (`nft`, an address the stash `stash` lists) or "roll" (a roll of `box`; in a recipe, on the collectible kept
326
368
  * at `on`). A grant's older `gives` ({ item }, { pool, amount }, { roll }) reads as its line. Null for none.
327
369
  */
@@ -334,8 +376,8 @@ export function prizeLine(p) {
334
376
  return null;
335
377
  }
336
378
 
337
- /** The pool a prize line pays from: a share's, or tokens' "pool:<key>". */
338
- export const prizePool = (p) => (p?.gives === 'share' ? p.pool : p?.gives === 'tokens' && p.from?.startsWith('pool:') ? p.from.slice(5) : null);
379
+ /** The pool a prize line pays from: tokens' "pool:<key>". */
380
+ export const prizePool = (p) => (p?.gives === 'tokens' && p.from?.startsWith('pool:') ? p.from.slice(5) : null);
339
381
 
340
382
  /** The token a prize line pays in: its pool's, or the one it mints. */
341
383
  export const prizeToken = (p, m) => (prizePool(p) ? m.poolToken(prizePool(p)) : p?.gives === 'tokens' ? m.tokens.get(p.currency ?? m.currencyKey) : null);
@@ -364,7 +406,8 @@ export function prizes(m) {
364
406
  * canopy, bytes }
365
407
  */
366
408
  export function compressedTree(m) {
367
- if (!prizes(m).some((p) => p.prize.gives === 'item' && p.prize.compressed)) return null;
409
+ const boxGives = m.allBoxes.some((b) => (b.outcomes ?? []).flatMap(outcomeParts).some((o) => o.gives === 'item' && o.compressed));
410
+ if (!boxGives && !prizes(m).some((p) => p.prize.gives === 'item' && p.prize.compressed)) return null;
368
411
  const depth = Math.min(20, Math.max(14, Math.ceil(Math.log2(Math.max(1, m.treeHolds)))));
369
412
  const canopy = Math.min(depth - 10, 8);
370
413
  return { depth, buffer: 64, canopy, bytes: treeSize(depth, 64, canopy) };
@@ -406,13 +449,13 @@ export function rollsOf(m) {
406
449
  return rolls;
407
450
  }
408
451
 
409
- /** A prize line in words: "one Badge", "100 GOLD from “rewards”", "100 GOLD, minted", "10% of “jackpot”", "the NFT ab12…cd34 of “vault”", "a roll of “Chest”". */
452
+ /** A prize line in words: "one Badge", "100 GOLD from “rewards”", "100 GOLD, minted", "the NFT ab12…cd34 of “vault”", "a roll of “Chest”". */
410
453
  export function prizeWords(p, m) {
411
454
  const line = prizeLine(p);
412
455
  if (!line) return 'nothing';
413
456
  const sym = (key) => m.tokens.get(key)?.symbol ?? key;
414
457
  if (line.gives === 'item') return `one ${m.item.get(line.item)?.name ?? line.item}${line.compressed ? ' (compressed)' : ''}`;
415
- if (line.gives === 'share') return `${line.percent}% of “${line.pool}”`;
458
+ if (line.gives === 'raffled') return 'an NFT drawn at random from the lottery’s own stash';
416
459
  if (line.gives === 'nft' && line.held) return `the NFT ${String(line.nft).slice(0, 4)}…${String(line.nft).slice(-4)} (held in the lottery’s own stash until it is won)`;
417
460
  if (line.gives === 'nft') return `the NFT ${String(line.nft).slice(0, 4)}…${String(line.nft).slice(-4)} of “${m.stash.get(line.stash)?.name ?? line.stash}”`;
418
461
  if (line.gives === 'roll') return `a roll of “${m.box.get(line.box)?.name ?? line.box}”`;
@@ -422,30 +465,81 @@ export function prizeWords(p, m) {
422
465
  }
423
466
 
424
467
  /**
425
- * What one box outcome gives (catalog.rs, at most three prizes an entry), each in the box's own words: "item",
426
- * "pay" (a fixed amount from a pool), "share" (a percent of a pool), "stash" (an NFT of a stash at random), "mint"
468
+ * What one box outcome gives (catalog.rs), each in the box's own words: "item", "pay" (a fixed amount from a pool),
469
+ * "stash" (an NFT of a stash at random), "mint"
427
470
  * (tokens minted), "nft" (one NFT a stash lists) or "roll" (a roll of another box). A bundle gives its `prizes`; "burn"
428
471
  * and "nothing" give none.
429
472
  */
430
473
  export function outcomeParts(o) {
431
474
  if (!o) return [];
432
475
  if (o.gives === 'bundle') return (o.prizes ?? []).flatMap(outcomeParts);
433
- if (o.gives === 'burn' || o.gives === 'nothing') return [];
476
+ if (o.gives === 'nothing') return [];
434
477
  if (o.gives === 'tokens') return [o.from?.startsWith('pool:') ? { gives: 'pay', pool: o.from.slice(5), amount: o.amount } : { gives: 'mint', currency: o.currency, amount: o.amount }];
435
478
  return [o];
436
479
  }
437
480
 
438
- /** Whether an outcome burns the payment in place of the box's split: "burn", or a bundle that says `burns`. */
439
- export const outcomeBurns = (o) => o?.gives === 'burn' || Boolean(o?.burns);
440
-
441
481
  /** Whether a box leaves its outcome to the oracle (catalog.rs `rolls`): more than one outcome, or an NFT at random. */
442
- export const boxRolls = (b) => (b.outcomes ?? []).length > 1 || (b.outcomes ?? []).flatMap(outcomeParts).some((p) => p.gives === 'stash');
482
+ /** How many draws one open of a box makes: its slots' counts, or one. */
483
+ export const drawsOf = (b) => ((b.slots ?? []).length ? b.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
484
+
485
+ /** Whether a box's prize costs its settle a collectible's rent: an item minted as a Core NFT, or a fresh compressed one
486
+ * (its traits' Sheet). A plain compressed NFT costs no rent. */
487
+ export const holdsRent = (p, m) => p.gives === 'item' && (!p.compressed || (m?.item.get(p.item)?.drawn ?? []).length > 0);
488
+
489
+ /**
490
+ * The units of `SETTLE_RENT_UNIT` a settle of box `b`'s order may spend (`loot.settleRent`), which js/play funds its
491
+ * throwaway settle key with and gets back unspent: for each draw, each part of the richest outcome that opens an
492
+ * account (a collectible, a fresh compressed NFT's Sheet, a stash's NFT); once an order, each source of tokens it may
493
+ * pay, and the fate of the NFT it rolls on.
494
+ */
495
+ export function settleUnits(b, m) {
496
+ const opens = (p) => holdsRent(p, m) || p.gives === 'stash' || p.gives === 'nft';
497
+ const parts = (b.outcomes ?? []).map((o) => outcomeParts(o));
498
+ const most = Math.max(0, ...parts.map((ps) => ps.filter(opens).length));
499
+ const sources = new Set(parts.flat().filter((p) => ['tokens', 'pay', 'mint'].includes(p.gives)).map((p) => prizePool(p) ?? `mint:${p.currency ?? ''}`));
500
+ return drawsOf(b) * most + sources.size + (b.rollsOn ? 1 : 0);
501
+ }
502
+
503
+ export const boxRolls = (b, m) => (b.outcomes ?? []).length > 1 || (b.outcomes ?? []).flatMap(outcomeParts).some((p) => p.gives === 'stash' || (p.gives === 'item' && (m?.item.get(p.item)?.drawn ?? []).length > 0));
443
504
 
444
- /** Whether a box sells with `buy`, in one transaction (buy.rs): it doesn't roll, and rolls on no NFT. */
445
- export const boxBuys = (b) => !boxRolls(b) && !b.rollsOn;
505
+ /** Whether a box sells with `buy`, in one transaction (buy.rs): it doesn't roll and rolls on no NFT, any number of draws
506
+ * a sale (a fixed pack: its prizes given once for each draw). */
507
+ export const boxBuys = (b, m) => !boxRolls(b, m) && !b.rollsOn;
508
+
509
+ /** The most a raise lowers a number by (an i32's least but one): a renew's, which counts a collectible's uses from 0. */
510
+ const RENEW_BY = -2_147_483_647;
511
+
512
+ /**
513
+ * What a recipe's lines owe the life of each collectible they take (an item's `life`), as the program's needs and
514
+ * raises (craft.rs `Need`, `Raise`), at each collectible's place in the recipe (each burned unit, then each kept one, in
515
+ * line order; a pay line takes none): ready after `readyAfter` (`made` older than it), for `lasts` (`made` within it),
516
+ * on every line that takes it; `uses` times (`used` raised by 1 up to it) and a `cooldown` (`next_use` passed, then
517
+ * raised to that far from now) on a line that uses it or takes it staked; a renew line counts its uses from 0 again.
518
+ * { needs, raises }, in the program's shapes.
519
+ */
520
+ export function lifeLines(r, m) {
521
+ const places = (r.in ?? []).flatMap((l) => (l.takes === 'pay' ? [] : Array(l.takes === 'burn' ? l.count ?? 1 : 1).fill(l)));
522
+ const needs = [];
523
+ const raises = [];
524
+ places.forEach((l, place) => {
525
+ const it = l.item ? m.item.get(l.item) : null;
526
+ if (!it?.life || it.kind === 'normal') return;
527
+ const [ready, lasts, uses, cooldown] = [toSeconds(it.life.readyAfter ?? 0), toSeconds(it.life.lasts ?? 0), it.life.uses ?? 0, toSeconds(it.life.cooldown ?? 0)];
528
+ if (ready) needs.push({ place, key: 'made', test: { Older: [ready] } });
529
+ if (lasts) needs.push({ place, key: 'made', test: { Within: [lasts] } });
530
+ if (l.takes === 'renew') raises.push({ place, shift: { key: 'used', by: RENEW_BY, most: 0, from_now: false } });
531
+ if (l.takes !== 'use' && l.takes !== 'staked') return;
532
+ if (uses) raises.push({ place, shift: { key: 'used', by: 1, most: uses, from_now: false } });
533
+ if (cooldown) {
534
+ needs.push({ place, key: 'next_use', test: 'Passed' });
535
+ raises.push({ place, shift: { key: 'next_use', by: cooldown, most: cooldown, from_now: true } });
536
+ }
537
+ });
538
+ return { needs, raises };
539
+ }
446
540
 
447
541
  /** A lottery's place as { percent, prize }: a number is a share of the pot and no prize. */
448
- export const placeOf = (p) => (typeof p === 'object' && p !== null ? { percent: p.percent ?? 0, prize: prizeLine(p.prize) } : { percent: p, prize: null });
542
+ export const placeOf = (p) => (typeof p === 'object' && p !== null ? { percent: p.percent ?? 0, prize: prizeLine(p.prize), times: p.times ?? 1 } : { percent: p, prize: null, times: 1 });
449
543
 
450
544
  /** "pool:house" → { kind: 'pool', key: 'house' }; "burn" → { kind: 'burn' }. */
451
545
  export function destination(to) {
@@ -463,7 +557,8 @@ export const sharesOf = (split) => (split ?? []).map((s) => ({ ...destination(s.
463
557
  * The program's `To` for a destination, with pool indexes resolved (the IDL's own shape). A wallet ("wallet:<address>")
464
558
  * is paid at its own token account of the part's currency (the program opens it when it is not there); "treasury" is the game's treasury wallet's; "chosen"
465
559
  * (or "chosen:<item>") is the one the buyer names when they buy; "lottery:<key>" is that lottery's pot
466
- * ("@pot:<id>", which the plan resolves once the game is known).
560
+ * ("@pot:<id>") and "farm:<key>" that farm's rewards vault ("@rewards:<id>"), which the plan resolves once the game is
561
+ * known.
467
562
  */
468
563
  export function toEnum(to, m) {
469
564
  const d = destination(to);
@@ -473,6 +568,7 @@ export function toEnum(to, m) {
473
568
  if (d.kind === 'account') return { Account: [d.key] };
474
569
  if (d.kind === 'wallet') return { Wallet: [d.key] };
475
570
  if (d.kind === 'lottery') return { Account: [`@pot:${m.lottery.get(d.key)?.id ?? 0}`] };
571
+ if (d.kind === 'farm') return { Account: [`@rewards:${m.farm.get(d.key)?.id ?? 0}`] };
476
572
  if (d.kind === 'chosen') return { Chosen: { item: d.key ? (m.item.get(d.key)?.id ?? 0) : null } };
477
573
  return { Holder: [d.key] };
478
574
  }
@@ -544,8 +640,8 @@ export function weights(box) {
544
640
  export function poolTakers(m) {
545
641
  const takers = [];
546
642
  const add = (pool, by, how) => pool && takers.push({ pool, by, how });
547
- for (const b of m.allBoxes) for (const o of (b.outcomes ?? []).flatMap(outcomeParts)) if (o.gives === 'pay' || o.gives === 'share') add(o.pool, `box:${b.key}`, o.gives === 'pay' ? 'pays prizes' : 'pays a share as a prize');
548
- for (const p of prizes(m)) add(prizePool(p.prize), p.by, p.prize.gives === 'share' ? `pays a share as a prize (${p.route})` : `pays prizes (${p.route})`);
643
+ for (const b of m.allBoxes) for (const o of (b.outcomes ?? []).flatMap(outcomeParts)) if (o.gives === 'pay') add(o.pool, `box:${b.key}`, 'pays prizes');
644
+ for (const p of prizes(m)) add(prizePool(p.prize), p.by, `pays prizes (${p.route})`);
549
645
  for (const s of m.schedules) {
550
646
  if (s.does === 'burn') add(s.pool, `schedule:${s.key}`, 'burns a share');
551
647
  if (s.does === 'move') add(s.from, `schedule:${s.key}`, 'moves a share out');
@@ -553,12 +649,6 @@ export function poolTakers(m) {
553
649
  // from a vault of its own and names no pool.
554
650
  if (s.does === 'pay') add(s.from, `schedule:${s.key}`, 'pays out on a timer');
555
651
  }
556
- for (const f of m.farms) {
557
- // Sharing what arrives releases all of the pool over its release span; a fixed rate from it takes the rate; a
558
- // minted one takes nothing.
559
- if (!f.rate) add(f.rewards, `farm:${f.key}`, 'releases everything to its seats');
560
- else if (!f.rate.minted) add(f.rewards, `farm:${f.key}`, 'pays its fixed rate');
561
- }
562
652
  for (const d of m.decisions) if (d.does === 'payout' || d.does === 'burn') add(d.pool, `decision:${d.key}`, d.does === 'payout' ? 'pays out on the authority’s word' : 'burns on the authority’s word');
563
653
  return takers;
564
654
  }
@@ -581,3 +671,28 @@ export function poolFillers(m) {
581
671
  for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
582
672
  return fillers;
583
673
  }
674
+
675
+ /**
676
+ * Every place the spec sends money into a farm's rewards vault ("farm:<key>"): a split's share, a schedule that pays it,
677
+ * a craft that pays it, the owner's seed, an early exit's fee into its own vault. [{ farm, by, how }]
678
+ */
679
+ export function farmFillers(m) {
680
+ const fillers = [];
681
+ const add = (to, by, how) => {
682
+ const d = destination(to ?? '');
683
+ if (d.kind === 'farm') fillers.push({ farm: d.key, by, how });
684
+ };
685
+ const fromSplit = (split, by) => sharesOf(split).forEach((s) => add(s.to, by, `${s.percent}% of each payment`));
686
+ for (const f of m.farms) {
687
+ if (Number(f.seed ?? 0) > 0) fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'the owner’s seed' });
688
+ if (f.exitFeeTo === 'rewards') fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'early-exit fees' });
689
+ }
690
+ for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
691
+ for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
692
+ for (const r of m.recipes) for (const i of r.in ?? []) if (i.takes === 'pay') add(i.to, `recipe:${r.key}`, 'a craft pays in');
693
+ for (const s of m.schedules) if (s.does === 'pay' || s.does === 'issue') add(s.to, `schedule:${s.key}`, s.does === 'issue' ? 'mints into it on a timer' : 'pays in on a timer');
694
+ for (const l of m.lotteries) fromSplit(lotterySplit(l), `lottery:${l.key}`);
695
+ for (const c of m.contests) fromSplit(c.split, `contest:${c.key}`);
696
+ for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
697
+ return fillers;
698
+ }