@xgamesfun/solana 0.24.0 → 0.25.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 (92) hide show
  1. package/README.md +94 -51
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +2 -5
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/fair-presale.json +1 -3
  7. package/examples/hero-trials.json +14 -2
  8. package/examples/holder-den.json +38 -0
  9. package/examples/holders-drop.json +105 -0
  10. package/examples/ideas.json +788 -788
  11. package/examples/match-predictions.json +24 -0
  12. package/examples/nft-raffle.json +1 -0
  13. package/examples/pack-league.json +108 -0
  14. package/examples/partner-vault.json +19 -3
  15. package/examples/sacrifice-raffle.json +2 -10
  16. package/examples/shelter-fair.json +1 -0
  17. package/examples/stakers-draw.json +3 -7
  18. package/examples/stride-sneakers.json +164 -0
  19. package/examples/trade-in-shop.json +2 -3
  20. package/js/approve/details.mjs +117 -48
  21. package/js/approve/ed25519.mjs +5 -1
  22. package/js/approve/live-check.mjs +83 -26
  23. package/js/approve/page.html +217 -78
  24. package/js/approve/page.js +43 -15
  25. package/js/approve/server.mjs +276 -74
  26. package/js/chain/base58.js +7 -1
  27. package/js/chain/borsh.js +15 -11
  28. package/js/chain/bytes.js +11 -8
  29. package/js/chain/core.js +21 -5
  30. package/js/chain/curve.js +1 -1
  31. package/js/chain/lookup.js +118 -36
  32. package/js/chain/mint.js +6 -2
  33. package/js/chain/pda.js +2 -2
  34. package/js/chain/programs.js +6 -6
  35. package/js/chain/rpc.js +40 -14
  36. package/js/chain/token.js +13 -2
  37. package/js/chain/token2022.js +55 -16
  38. package/js/chain/tx.js +32 -22
  39. package/js/client/accounts.js +270 -80
  40. package/js/client/compressed.js +4 -3
  41. package/js/client/grant.js +165 -40
  42. package/js/client/idl/loot.js +1 -1
  43. package/js/client/idl/loot.json +104 -16
  44. package/js/client/ix.js +2121 -457
  45. package/js/client/pda.js +20 -18
  46. package/js/client/policy.js +12 -5
  47. package/js/client/random.js +1 -1
  48. package/js/client/sizes.js +220 -65
  49. package/js/client/traits.js +14 -5
  50. package/js/connect/connect.mjs +59 -17
  51. package/js/economy/check.mjs +3873 -690
  52. package/js/economy/cost.mjs +380 -56
  53. package/js/economy/examples.mjs +176 -35
  54. package/js/economy/explain.mjs +461 -110
  55. package/js/economy/guide.mjs +282 -59
  56. package/js/economy/legal.mjs +12 -6
  57. package/js/economy/live.mjs +60 -19
  58. package/js/economy/model.mjs +320 -97
  59. package/js/economy/plan.mjs +1022 -203
  60. package/js/economy/schema.json +127 -369
  61. package/js/economy/secrets.mjs +17 -7
  62. package/js/economy/simulate.mjs +771 -178
  63. package/js/economy/txsize.mjs +15 -3
  64. package/js/economy/validate.mjs +61 -24
  65. package/js/mcp/server.mjs +42 -14
  66. package/js/mcp/tools.mjs +293 -80
  67. package/js/play/index.js +1960 -461
  68. package/js/play/keys.js +35 -9
  69. package/js/results/index.js +223 -30
  70. package/js/results/server.js +230 -0
  71. package/js/results/wire.js +64 -16
  72. package/package.json +6 -4
  73. package/release.json +5 -5
  74. package/skills/xsolana-economy/SKILL.md +30 -16
  75. package/skills/xsolana-economy/references/after-launch.md +30 -20
  76. package/skills/xsolana-economy/references/apps.md +47 -36
  77. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -129
  78. package/skills/xsolana-economy/references/telling-the-person.md +9 -6
  79. package/types/chain/core.d.ts +1 -1
  80. package/types/chain/lookup.d.ts +1 -1
  81. package/types/client/accounts.d.ts +24 -3
  82. package/types/client/grant.d.ts +2 -2
  83. package/types/client/idl/loot.d.ts +22 -0
  84. package/types/client/ix.d.ts +54 -45
  85. package/types/client/pda.d.ts +3 -1
  86. package/types/economy/guide.d.mts +5 -0
  87. package/types/economy/model.d.mts +48 -3
  88. package/types/economy/plan.d.mts +44 -2
  89. package/types/play/index.d.ts +8 -19
  90. package/types/results/index.d.ts +7 -6
  91. package/types/results/server.d.ts +126 -0
  92. package/types/results/wire.d.ts +1 -1
@@ -31,7 +31,7 @@ export const ACCOUNT_OVERHEAD = 128n;
31
31
  * devnet and mainnet lowered it (a local validator and LiteSVM still charge it). An RPC that asks more is not believed.
32
32
  */
33
33
  export const MOST_RENT_PER_BYTE = 6_960n;
34
- export const rent = (bytes) => (BigInt(bytes) + ACCOUNT_OVERHEAD) * RENT_PER_BYTE;
34
+ export const rent = bytes => (BigInt(bytes) + ACCOUNT_OVERHEAD) * RENT_PER_BYTE;
35
35
  /**
36
36
  * A collectible's Core account, paid from the SOL its order holds (or by whoever gives it): its rent (a Core asset of
37
37
  * about 190 bytes) plus Metaplex Core's creation fee of 0.0015 SOL, which the account keeps. The full simulation
@@ -46,9 +46,30 @@ export const WSOL = 'So11111111111111111111111111111111111111112';
46
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
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
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,
49
+ callbackAccounts: 21,
50
+ tiers: 256,
51
+ collectiblesPerCraft: 255,
52
+ vouchers: 3,
53
+ compressedNameBytes: 32,
54
+ compressedUriBytes: 200,
55
+ maxBoostBps: 1_000_000,
56
+ maxTierBps: 110_000,
50
57
  // 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,
58
+ boxEntries: 128,
59
+ boxSlots: 32,
60
+ boxOdds: 32,
61
+ boxFates: 64,
62
+ entryPrizes: 14,
63
+ itemTraits: 64,
64
+ traitKeyBytes: 64,
65
+ recipeLines: 48,
66
+ farmRules: 32,
67
+ growthSteps: 128,
68
+ contestSides: 1024,
69
+ deciders: 32,
70
+ collections: 32,
71
+ lotteryRuns: 64,
72
+ farmEnds: 100_000,
52
73
  });
53
74
  /** Attribute names the program writes itself, which a recipe cannot copy and `Set` cannot change (craft.rs, make.rs),
54
75
  * and the two a collectible's life keeps (`lifeLines`: its uses and its next use). */
@@ -69,7 +90,8 @@ export class SpecError extends Error {
69
90
  /** A plain decimal string for an amount written as a number or text (no exponent). */
70
91
  export function decimalText(amount) {
71
92
  if (typeof amount === 'string') return amount;
72
- if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0) throw new SpecError('E-AMOUNT', `${amount} is not an amount.`, 'Write a number like 0.1, or text like "0.1".');
93
+ if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0)
94
+ throw new SpecError('E-AMOUNT', `${amount} is not an amount.`, 'Write a number like 0.1, or text like "0.1".');
73
95
  const text = String(amount);
74
96
  if (!/e/i.test(text)) return text;
75
97
  return amount.toFixed(20).replace(/\.?0+$/, '');
@@ -78,12 +100,19 @@ export function decimalText(amount) {
78
100
  /** "0.1" of a currency with 9 decimals → 100000000n. Refuses more decimals than the currency has. */
79
101
  export function toUnits(amount, decimals) {
80
102
  const text = decimalText(amount ?? 0);
81
- if (!/^\d{1,20}(\.\d+)?$/.test(text)) throw new SpecError('E-AMOUNT', `${text} is not an amount.`, 'Write a number like 0.1, or text like "0.1".');
103
+ if (!/^\d{1,20}(\.\d+)?$/.test(text))
104
+ throw new SpecError('E-AMOUNT', `${text} is not an amount.`, 'Write a number like 0.1, or text like "0.1".');
82
105
  const [whole, fraction = ''] = text.split('.');
83
106
  const trimmed = fraction.replace(/0+$/, '');
84
- if (trimmed.length > decimals) throw new SpecError('E-AMOUNT', `${text} has more decimals than its currency (${decimals}).`, `Round it to ${decimals} decimals.`);
107
+ if (trimmed.length > decimals)
108
+ throw new SpecError(
109
+ 'E-AMOUNT',
110
+ `${text} has more decimals than its currency (${decimals}).`,
111
+ `Round it to ${decimals} decimals.`,
112
+ );
85
113
  const units = BigInt(whole + trimmed.padEnd(decimals, '0'));
86
- if (units >= 2n ** 64n) throw new SpecError('E-AMOUNT', `${text} is too large for the program.`, 'Use a smaller amount.');
114
+ if (units >= 2n ** 64n)
115
+ throw new SpecError('E-AMOUNT', `${text} is too large for the program.`, 'Use a smaller amount.');
87
116
  return units;
88
117
  }
89
118
 
@@ -98,7 +127,7 @@ export function fromUnits(units, decimals) {
98
127
 
99
128
  /** A count written as an amount (a contest of NFTs counts NFTs): 2 or "2" → 2n. Null when it is not a whole number. */
100
129
  export function wholeCount(value) {
101
- const text = typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? String(value) : value ?? '0';
130
+ const text = typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? String(value) : (value ?? '0');
102
131
  return typeof text === 'string' && /^\d{1,19}$/.test(text) && BigInt(text) < 2n ** 64n ? BigInt(text) : null;
103
132
  }
104
133
 
@@ -114,7 +143,8 @@ export function toSeconds(duration) {
114
143
  if (duration === undefined || duration === null) return 0;
115
144
  if (typeof duration === 'number') return duration;
116
145
  const m = /^(\d+(?:\.\d+)?)(s|m|h|d|w)$/.exec(duration);
117
- if (!m) throw new SpecError('E-TIME', `${duration} is not a duration.`, 'Write it like "45s", "30m", "6h", "7d" or "2w".');
146
+ if (!m)
147
+ throw new SpecError('E-TIME', `${duration} is not a duration.`, 'Write it like "45s", "30m", "6h", "7d" or "2w".');
118
148
  return Math.round(Number(m[1]) * UNIT[m[2]]);
119
149
  }
120
150
 
@@ -122,7 +152,7 @@ export function toSeconds(duration) {
122
152
  * 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
153
  * lands, `farm.rs` `release_span`): its longest lock tier, and a day at least.
124
154
  */
125
- export const releaseSeconds = (f) => Math.max(86_400, ...(f.tiers ?? []).map((t) => toSeconds(t.lock ?? 0)));
155
+ export const releaseSeconds = f => Math.max(86_400, ...(f.tiers ?? []).map(t => toSeconds(t.lock ?? 0)));
126
156
 
127
157
  /** A farm's release span in words: "a day", "30 days", "36 hours". */
128
158
  export function releaseWords(f) {
@@ -135,16 +165,29 @@ export function releaseWords(f) {
135
165
  * What a run sent late does, in words (schedule.rs `catches_up`): a sealed schedule's fixed amount pays every run due;
136
166
  * any other run pays once, for the latest run due, and the runs before it lapse.
137
167
  */
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');
168
+ export const lateRunWords = s =>
169
+ s.sealed && s.percent === undefined
170
+ ? 'a run sent late pays every run due since'
171
+ : 'a run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse';
139
172
 
140
173
  /**
141
174
  * Whether a farm's boost reads a number its own growth writes (`refresh_seat` writes it): each level counts from when it is
142
175
  * written, so a page or a keeper should refresh seats.
143
176
  */
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'))));
177
+ export const boostReadsGrowth = f =>
178
+ (f.boosts ?? []).some(
179
+ b =>
180
+ b.when?.attribute &&
181
+ listOf(f.growth).some(
182
+ g =>
183
+ b.when.attribute === (g.attribute ?? 'xp') ||
184
+ ((g.levels ?? []).length > 0 && b.when.attribute === (g.level ?? 'level')),
185
+ ),
186
+ );
145
187
 
146
188
  /** 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';
189
+ export const STOCK_WORDS =
190
+ '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
191
 
149
192
  /**
150
193
  * What a box that rolls does once an outcome can't be given (`whenOut`, order.rs `WhenOut`), in words. `sold`: it is
@@ -155,8 +198,9 @@ export function whenOutWords(b, { sold = b.active !== false } = {}) {
155
198
  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
199
  if (b.whenOut === 'drawLeft')
157
200
  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';
201
+ if (!sold)
202
+ return `${STOCK_WORDS}. A free roll (a prize, a recipe’s, a mission’s chest) is on the odds as written too: a draw on an outcome that is out misses and gives nothing back (a pack keeps what its other draws won; a roll that drew nothing waits to be asked again), so emptying a shared source before asking steers nothing. A pack that should draw among what is left needs "whenOut": "drawLeft"`;
203
+ return 'When an outcome runs out, the box stops selling: it isn’t sold while anything an outcome takes (an item’s units left, a pool’s tokens, a stash’s NFTs) couldn’t cover, at the most any outcome sharing it takes in one draw, this purchase’s draws and those of purchases still waiting for the oracle, 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
204
  }
161
205
 
162
206
  /**
@@ -193,21 +237,27 @@ export function toTime(time, now) {
193
237
  const m = /^publish\+(.+)$/.exec(time);
194
238
  if (m) return now + toSeconds(m[1]);
195
239
  const at = Date.parse(time);
196
- if (!Number.isFinite(at)) throw new SpecError('E-TIME', `${time} is not a time.`, 'Write "publish", "publish+7d" or a date like "2026-11-01T18:00:00Z".');
240
+ if (!Number.isFinite(at))
241
+ throw new SpecError(
242
+ 'E-TIME',
243
+ `${time} is not a time.`,
244
+ 'Write "publish", "publish+7d" or a date like "2026-11-01T18:00:00Z".',
245
+ );
197
246
  return Math.floor(at / 1000);
198
247
  }
199
248
 
200
- export const rarityNumber = (rarity) => (typeof rarity === 'number' ? rarity : Math.max(0, RARITIES.indexOf(rarity ?? 'Common')));
201
- export const utf8Bytes = (s) => new TextEncoder().encode(s ?? '').length;
202
- export const isAddress = (s) => typeof s === 'string' && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(s);
249
+ export const rarityNumber = rarity =>
250
+ typeof rarity === 'number' ? rarity : Math.max(0, RARITIES.indexOf(rarity ?? 'Common'));
251
+ export const utf8Bytes = s => new TextEncoder().encode(s ?? '').length;
252
+ export const isAddress = s => typeof s === 'string' && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(s);
203
253
 
204
254
  // ---------------------------------------------------------------- the model
205
255
 
206
256
  /** Gives every part without one the next free program id (pools from 0, the rest from 1). */
207
257
  function assignIds(list, field, first) {
208
- const used = new Set(list.map((x) => x[field]).filter((v) => Number.isInteger(v)));
258
+ const used = new Set(list.map(x => x[field]).filter(v => Number.isInteger(v)));
209
259
  let next = first;
210
- return list.map((x) => {
260
+ return list.map(x => {
211
261
  if (Number.isInteger(x[field])) return x;
212
262
  while (used.has(next)) next += 1;
213
263
  used.add(next);
@@ -227,7 +277,17 @@ export const spendOf = (then, m) => {
227
277
  function tokenOf(given) {
228
278
  // "project" is the spec's word for a token the project mints; "game" is the older spelling of it.
229
279
  const t = given.source === 'project' ? { ...given, source: 'game' } : given;
230
- if (t.source === 'sol') return { ...t, name: t.name ?? 'SOL', symbol: t.symbol ?? 'SOL', decimals: 9, program: 'spl-token', mint: WSOL, wsol: true, gameMints: false };
280
+ if (t.source === 'sol')
281
+ return {
282
+ ...t,
283
+ name: t.name ?? 'SOL',
284
+ symbol: t.symbol ?? 'SOL',
285
+ decimals: 9,
286
+ program: 'spl-token',
287
+ mint: WSOL,
288
+ wsol: true,
289
+ gameMints: false,
290
+ };
231
291
  const decimals = t.decimals ?? (t.source === 'game' ? 0 : undefined);
232
292
  return {
233
293
  ...t,
@@ -249,22 +309,37 @@ function tokenOf(given) {
249
309
  */
250
310
  export function model(economy, { now = 0 } = {}) {
251
311
  const e = economy ?? {};
252
- const tokens = new Map((e.tokens ?? []).map((t) => [t.key, tokenOf(t)]));
312
+ const tokens = new Map((e.tokens ?? []).map(t => [t.key, tokenOf(t)]));
253
313
  const currencyKey = e.currency ?? (tokens.size === 1 ? [...tokens.keys()][0] : undefined);
254
314
  const currency = tokens.get(currencyKey);
255
315
  // Numbers are given in this order: the pools that have one, then the rest in the spec's order.
256
316
  const listed = e.pools ?? [];
257
- const order = [...listed.keys()].sort((a, b) => Number(!Number.isInteger(listed[a].index)) - Number(!Number.isInteger(listed[b].index)));
258
- const assigned = assignIds(order.map((i) => listed[i]), 'index', 0);
317
+ const order = [...listed.keys()].sort(
318
+ (a, b) => Number(!Number.isInteger(listed[a].index)) - Number(!Number.isInteger(listed[b].index)),
319
+ );
320
+ const assigned = assignIds(
321
+ order.map(i => listed[i]),
322
+ 'index',
323
+ 0,
324
+ );
259
325
  const pools = listed.map((_, i) => assigned[order.indexOf(i)]);
260
326
  // 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) }));
327
+ const poolCurrency = new Map(pools.map(p => [p.key, p.currency]));
328
+ const farms = (e.farms ?? []).map(f => ({
329
+ ...f,
330
+ rewardsToken: farmRewardsToken(f, tokens, currencyKey, poolCurrency),
331
+ }));
263
332
  // A raffle: its NFT is first place's prize, held in the lottery's own stash.
264
- const lotteries = (e.lotteries ?? []).map((l) => {
333
+ const lotteries = (e.lotteries ?? []).map(l => {
265
334
  if (!l.raffle) return l;
266
335
  const { nft, collection } = typeof l.raffle === 'string' ? { nft: l.raffle } : l.raffle;
267
- return { ...l, places: [{ percent: 0, prize: { gives: 'nft', nft, held: true, ...(collection ? { collection } : {}) } }, ...(l.places ?? [])] };
336
+ return {
337
+ ...l,
338
+ places: [
339
+ { percent: 0, prize: { gives: 'nft', nft, held: true, ...(collection ? { collection } : {}) } },
340
+ ...(l.places ?? []),
341
+ ],
342
+ };
268
343
  });
269
344
  const m = {
270
345
  now,
@@ -276,7 +351,15 @@ export function model(economy, { now = 0 } = {}) {
276
351
  currencyKey,
277
352
  currency,
278
353
  /** The collection the setup makes (null when the spec says `"collection": false`: a project of tokens only). */
279
- collection: e.collection === false ? null : { name: e.collection?.name ?? (e.name ?? 'Project').slice(0, 32), royaltyPercent: e.collection?.royaltyPercent ?? 5, uri: e.collection?.uri ?? '', ...e.collection },
354
+ collection:
355
+ e.collection === false
356
+ ? null
357
+ : {
358
+ name: e.collection?.name ?? (e.name ?? 'Project').slice(0, 32),
359
+ royaltyPercent: e.collection?.royaltyPercent ?? 5,
360
+ uri: e.collection?.uri ?? '',
361
+ ...e.collection,
362
+ },
280
363
  /** Core collections the owner already has that the project also takes (its update authority is the owner). */
281
364
  collections: e.collections ?? [],
282
365
  /** How many compressed NFTs the project's tree holds (`compressedTree`), when a prize gives one. */
@@ -305,8 +388,8 @@ export function model(economy, { now = 0 } = {}) {
305
388
  // A shop item is a box of one entry (the program sells items only through boxes). Its box takes the item's price,
306
389
  // rise, fall, split and gate; it is numbered with the boxes, after the ones the spec numbered itself.
307
390
  const shop = m.items
308
- .filter((it) => Number(it.price ?? 0) > 0 || it.free)
309
- .map((it) => ({
391
+ .filter(it => Number(it.price ?? 0) > 0 || it.free)
392
+ .map(it => ({
310
393
  key: `shop-${it.key}`,
311
394
  ...(Number.isInteger(it.shopBox) ? { id: it.shopBox } : {}),
312
395
  name: it.name,
@@ -316,17 +399,19 @@ export function model(economy, { now = 0 } = {}) {
316
399
  ...(it.split ? { split: it.split } : {}),
317
400
  ...(it.gate ? { gate: it.gate } : {}),
318
401
  // When it sells, and how many one wallet may buy.
319
- ...Object.fromEntries(['opens', 'closes', 'perWallet', 'perWalletEach'].filter((k) => it[k] !== undefined).map((k) => [k, it[k]])),
402
+ ...Object.fromEntries(
403
+ ['opens', 'closes', 'perWallet', 'perWalletEach'].filter(k => it[k] !== undefined).map(k => [k, it[k]]),
404
+ ),
320
405
  active: it.active !== false,
321
406
  outcomes: [{ gives: 'item', item: it.key, weight: 1 }],
322
407
  shopOf: it.key,
323
408
  }));
324
409
  const numbered = assignIds([...(e.boxes ?? []), ...shop], 'id', 1);
325
- m.boxes = numbered.filter((b) => !b.shopOf);
326
- m.shop = numbered.filter((b) => b.shopOf);
410
+ m.boxes = numbered.filter(b => !b.shopOf);
411
+ m.shop = numbered.filter(b => b.shopOf);
327
412
  /** Every box the program holds: the spec's own and the shop's. */
328
413
  m.allBoxes = numbered;
329
- const index = (list) => new Map(list.map((x) => [x.key, x]));
414
+ const index = list => new Map(list.map(x => [x.key, x]));
330
415
  m.item = index(m.items);
331
416
  m.pool = index(m.pools);
332
417
  m.stash = index(m.stashes);
@@ -340,9 +425,9 @@ export function model(economy, { now = 0 } = {}) {
340
425
  m.auction = index(m.auctions);
341
426
  m.hold = index(m.holds);
342
427
  /** The token a pool holds. */
343
- m.poolToken = (key) => tokens.get(m.pool.get(key)?.currency);
428
+ m.poolToken = key => tokens.get(m.pool.get(key)?.currency);
344
429
  /** The token a part is priced in (its own `currency`, else the game's). */
345
- m.priceToken = (part) => tokens.get(part?.currency ?? currencyKey);
430
+ m.priceToken = part => tokens.get(part?.currency ?? currencyKey);
346
431
  return m;
347
432
  }
348
433
 
@@ -352,10 +437,17 @@ export function model(economy, { now = 0 } = {}) {
352
437
  * NFT (`compressed`, its asset id).
353
438
  */
354
439
  export function stashNfts(s) {
355
- return (s.nfts ?? []).map((n) => {
440
+ return (s.nfts ?? []).map(n => {
356
441
  if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null, weight: 1 };
357
442
  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 };
443
+ if (n.mint)
444
+ return {
445
+ kind: 'token-metadata',
446
+ asset: n.mint,
447
+ programmable: Boolean(n.programmable),
448
+ ruleSet: n.ruleSet ?? null,
449
+ weight,
450
+ };
359
451
  if (n.compressed) return { kind: 'compressed', asset: n.compressed, weight };
360
452
  return { kind: 'core', asset: n.asset, collection: n.collection ?? s.collection ?? null, weight };
361
453
  });
@@ -377,10 +469,11 @@ export function prizeLine(p) {
377
469
  }
378
470
 
379
471
  /** 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);
472
+ export const prizePool = p => (p?.gives === 'tokens' && p.from?.startsWith('pool:') ? p.from.slice(5) : null);
381
473
 
382
474
  /** The token a prize line pays in: its pool's, or the one it mints. */
383
- export const prizeToken = (p, m) => (prizePool(p) ? m.poolToken(prizePool(p)) : p?.gives === 'tokens' ? m.tokens.get(p.currency ?? m.currencyKey) : null);
475
+ export const prizeToken = (p, m) =>
476
+ prizePool(p) ? m.poolToken(prizePool(p)) : p?.gives === 'tokens' ? m.tokens.get(p.currency ?? m.currencyKey) : null;
384
477
 
385
478
  /**
386
479
  * Every prize the spec gives (prize.rs): a grant's, a lottery place's, a contest's winners', a mission tier's and a
@@ -390,12 +483,40 @@ export const prizeToken = (p, m) => (prizePool(p) ? m.poolToken(prizePool(p)) :
390
483
  */
391
484
  export function prizes(m) {
392
485
  const list = [];
393
- const add = (p, where, by, route, strict, once = false) => p && list.push({ prize: p, where, by, route, strict, once });
394
- for (const g of m.grants) add(prizeLine(g.prize ?? g.gives), `economy.grants.${g.key}.${g.prize ? 'prize' : 'gives'}`, `grant:${g.key}`, 'grant', true, Number(g.limits?.total ?? 0) === 1);
395
- for (const l of m.lotteries) (l.places ?? []).forEach((p, i) => add(placeOf(p).prize, `economy.lotteries.${l.key}.places[${i}].prize`, `lottery:${l.key}`, `place ${i + 1}`, false, !l.every));
396
- for (const c of m.contests) add(prizeLine(c.prize), `economy.contests.${c.key}.prize`, `contest:${c.key}`, 'contest', false);
397
- for (const f of m.farms) (f.tiers ?? []).forEach((t, i) => add(prizeLine(t.prize), `economy.farms.${f.key}.tiers[${i}].prize`, `farm:${f.key}`, 'mission', false));
398
- for (const r of m.recipes) (r.out ?? []).forEach((o, i) => o.gives !== 'roll' || o.on === undefined ? add(o, `economy.recipes.${r.key}.out[${i}]`, `recipe:${r.key}`, 'recipe', true) : null);
486
+ const add = (p, where, by, route, strict, once = false) =>
487
+ p && list.push({ prize: p, where, by, route, strict, once });
488
+ for (const g of m.grants)
489
+ add(
490
+ prizeLine(g.prize ?? g.gives),
491
+ `economy.grants.${g.key}.${g.prize ? 'prize' : 'gives'}`,
492
+ `grant:${g.key}`,
493
+ 'grant',
494
+ true,
495
+ Number(g.limits?.total ?? 0) === 1,
496
+ );
497
+ for (const l of m.lotteries)
498
+ (l.places ?? []).forEach((p, i) =>
499
+ add(
500
+ placeOf(p).prize,
501
+ `economy.lotteries.${l.key}.places[${i}].prize`,
502
+ `lottery:${l.key}`,
503
+ `place ${i + 1}`,
504
+ false,
505
+ !l.every,
506
+ ),
507
+ );
508
+ for (const c of m.contests)
509
+ add(prizeLine(c.prize), `economy.contests.${c.key}.prize`, `contest:${c.key}`, 'contest', false);
510
+ for (const f of m.farms)
511
+ (f.tiers ?? []).forEach((t, i) =>
512
+ add(prizeLine(t.prize), `economy.farms.${f.key}.tiers[${i}].prize`, `farm:${f.key}`, 'mission', false),
513
+ );
514
+ for (const r of m.recipes)
515
+ (r.out ?? []).forEach((o, i) =>
516
+ o.gives !== 'roll' || o.on === undefined
517
+ ? add(o, `economy.recipes.${r.key}.out[${i}]`, `recipe:${r.key}`, 'recipe', true)
518
+ : null,
519
+ );
399
520
  return list;
400
521
  }
401
522
 
@@ -406,8 +527,10 @@ export function prizes(m) {
406
527
  * canopy, bytes }
407
528
  */
408
529
  export function compressedTree(m) {
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;
530
+ const boxGives = m.allBoxes.some(b =>
531
+ (b.outcomes ?? []).flatMap(outcomeParts).some(o => o.gives === 'item' && o.compressed),
532
+ );
533
+ if (!boxGives && !prizes(m).some(p => p.prize.gives === 'item' && p.prize.compressed)) return null;
411
534
  const depth = Math.min(20, Math.max(14, Math.ceil(Math.log2(Math.max(1, m.treeHolds)))));
412
535
  const canopy = Math.min(depth - 10, 8);
413
536
  return { depth, buffer: 64, canopy, bytes: treeSize(depth, 64, canopy) };
@@ -418,11 +541,17 @@ export function compressedTree(m) {
418
541
  * mission's roll on the NFT leaving it and a recipe's roll on a collectible it keeps. [{ box, by, route }]
419
542
  */
420
543
  export function prizeRolls(m) {
421
- const rolls = prizes(m).filter((p) => p.prize.gives === 'roll').map((p) => ({ box: p.prize.box, by: p.by, route: p.route }));
422
- for (const a of m.auctions) if (a.lot?.roll) rolls.push({ box: a.lot.roll, by: `auction:${a.key}`, route: 'auction' });
423
- for (const f of m.farms) for (const t of f.tiers ?? []) if (t.roll) rolls.push({ box: t.roll, by: `farm:${f.key}`, route: 'mission' });
424
- for (const r of m.recipes) for (const o of r.out ?? []) if (o.gives === 'roll' && o.on !== undefined) rolls.push({ box: o.box, by: `recipe:${r.key}`, route: 'recipe' });
425
- return rolls.filter((r) => r.box);
544
+ const rolls = prizes(m)
545
+ .filter(p => p.prize.gives === 'roll')
546
+ .map(p => ({ box: p.prize.box, by: p.by, route: p.route }));
547
+ for (const a of m.auctions)
548
+ if (a.lot?.roll) rolls.push({ box: a.lot.roll, by: `auction:${a.key}`, route: 'auction' });
549
+ for (const f of m.farms)
550
+ for (const t of f.tiers ?? []) if (t.roll) rolls.push({ box: t.roll, by: `farm:${f.key}`, route: 'mission' });
551
+ for (const r of m.recipes)
552
+ for (const o of r.out ?? [])
553
+ if (o.gives === 'roll' && o.on !== undefined) rolls.push({ box: o.box, by: `recipe:${r.key}`, route: 'recipe' });
554
+ return rolls.filter(r => r.box);
426
555
  }
427
556
 
428
557
  /**
@@ -433,9 +562,21 @@ export function prizeRolls(m) {
433
562
  */
434
563
  export function rollsOf(m) {
435
564
  const rolls = [...prizeRolls(m)];
436
- for (const b of m.allBoxes) for (const o of b.outcomes ?? []) for (const x of outcomeParts(o)) if (x.gives === 'roll' && x.box) rolls.push({ box: x.box, by: `box:${b.key}`, route: 'box' });
437
- const seen = new Set(rolls.map((r) => `${r.box}|${r.by}|${r.route}`));
438
- const rollsFrom = (key) => [...new Set(m.box.get(key) ? (m.box.get(key).outcomes ?? []).flatMap(outcomeParts).filter((x) => x.gives === 'roll' && x.box).map((x) => x.box) : [])];
565
+ for (const b of m.allBoxes)
566
+ for (const o of b.outcomes ?? [])
567
+ for (const x of outcomeParts(o))
568
+ if (x.gives === 'roll' && x.box) rolls.push({ box: x.box, by: `box:${b.key}`, route: 'box' });
569
+ const seen = new Set(rolls.map(r => `${r.box}|${r.by}|${r.route}`));
570
+ const rollsFrom = key => [
571
+ ...new Set(
572
+ m.box.get(key)
573
+ ? (m.box.get(key).outcomes ?? [])
574
+ .flatMap(outcomeParts)
575
+ .filter(x => x.gives === 'roll' && x.box)
576
+ .map(x => x.box)
577
+ : [],
578
+ ),
579
+ ];
439
580
  // A breadth-first walk: each roll of a box is also a roll of every box it rolls, once for each route.
440
581
  for (let i = 0; i < rolls.length; i++) {
441
582
  const r = rolls[i];
@@ -453,15 +594,20 @@ export function rollsOf(m) {
453
594
  export function prizeWords(p, m) {
454
595
  const line = prizeLine(p);
455
596
  if (!line) return 'nothing';
456
- const sym = (key) => m.tokens.get(key)?.symbol ?? key;
457
- if (line.gives === 'item') return `one ${m.item.get(line.item)?.name ?? line.item}${line.compressed ? ' (compressed)' : ''}`;
597
+ const sym = key => m.tokens.get(key)?.symbol ?? key;
598
+ if (line.gives === 'item')
599
+ return `one ${m.item.get(line.item)?.name ?? line.item}${line.compressed ? ' (compressed)' : ''}`;
458
600
  if (line.gives === 'raffled') return 'an NFT drawn at random from the lottery’s own stash';
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)`;
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}”`;
601
+ if (line.gives === 'nft' && line.held)
602
+ 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)`;
603
+ if (line.gives === 'nft')
604
+ return `the NFT ${String(line.nft).slice(0, 4)}…${String(line.nft).slice(-4)} of “${m.stash.get(line.stash)?.name ?? line.stash}”`;
461
605
  if (line.gives === 'roll') return `a roll of “${m.box.get(line.box)?.name ?? line.box}”`;
462
606
  const pool = prizePool(line);
463
607
  const split = line.split ? ', split by what each winner put in' : '';
464
- return pool ? `${line.amount} ${sym(m.pool.get(pool)?.currency)} from “${pool}”${split}` : `${line.amount} ${sym(line.currency ?? m.currencyKey)}, minted${split}`;
608
+ return pool
609
+ ? `${line.amount} ${sym(m.pool.get(pool)?.currency)} from “${pool}”${split}`
610
+ : `${line.amount} ${sym(line.currency ?? m.currencyKey)}, minted${split}`;
465
611
  }
466
612
 
467
613
  /**
@@ -474,17 +620,23 @@ export function outcomeParts(o) {
474
620
  if (!o) return [];
475
621
  if (o.gives === 'bundle') return (o.prizes ?? []).flatMap(outcomeParts);
476
622
  if (o.gives === 'nothing') return [];
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 }];
623
+ if (o.gives === 'tokens')
624
+ return [
625
+ o.from?.startsWith('pool:')
626
+ ? { gives: 'pay', pool: o.from.slice(5), amount: o.amount }
627
+ : { gives: 'mint', currency: o.currency, amount: o.amount },
628
+ ];
478
629
  return [o];
479
630
  }
480
631
 
481
632
  /** Whether a box leaves its outcome to the oracle (catalog.rs `rolls`): more than one outcome, or an NFT at random. */
482
633
  /** 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);
634
+ export const drawsOf = b => ((b.slots ?? []).length ? b.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
484
635
 
485
636
  /** 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
637
  * (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);
638
+ export const holdsRent = (p, m) =>
639
+ p.gives === 'item' && (!p.compressed || (m?.item.get(p.item)?.drawn ?? []).length > 0);
488
640
 
489
641
  /**
490
642
  * The units of `SETTLE_RENT_UNIT` a settle of box `b`'s order may spend (`loot.settleRent`), which js/play funds its
@@ -493,14 +645,23 @@ export const holdsRent = (p, m) => p.gives === 'item' && (!p.compressed || (m?.i
493
645
  * pay, and the fate of the NFT it rolls on.
494
646
  */
495
647
  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 ?? ''}`));
648
+ const opens = p => holdsRent(p, m) || p.gives === 'stash' || p.gives === 'nft';
649
+ const parts = (b.outcomes ?? []).map(o => outcomeParts(o));
650
+ const most = Math.max(0, ...parts.map(ps => ps.filter(opens).length));
651
+ const sources = new Set(
652
+ parts
653
+ .flat()
654
+ .filter(p => ['tokens', 'pay', 'mint'].includes(p.gives))
655
+ .map(p => prizePool(p) ?? `mint:${p.currency ?? ''}`),
656
+ );
500
657
  return drawsOf(b) * most + sources.size + (b.rollsOn ? 1 : 0);
501
658
  }
502
659
 
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));
660
+ export const boxRolls = (b, m) =>
661
+ (b.outcomes ?? []).length > 1 ||
662
+ (b.outcomes ?? [])
663
+ .flatMap(outcomeParts)
664
+ .some(p => p.gives === 'stash' || (p.gives === 'item' && (m?.item.get(p.item)?.drawn ?? []).length > 0));
504
665
 
505
666
  /** 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
667
  * a sale (a fixed pack: its prizes given once for each draw). */
@@ -518,13 +679,20 @@ const RENEW_BY = -2_147_483_647;
518
679
  * { needs, raises }, in the program's shapes.
519
680
  */
520
681
  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)));
682
+ const places = (r.in ?? []).flatMap(l =>
683
+ l.takes === 'pay' ? [] : Array(l.takes === 'burn' ? (l.count ?? 1) : 1).fill(l),
684
+ );
522
685
  const needs = [];
523
686
  const raises = [];
524
687
  places.forEach((l, place) => {
525
688
  const it = l.item ? m.item.get(l.item) : null;
526
689
  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)];
690
+ const [ready, lasts, uses, cooldown] = [
691
+ toSeconds(it.life.readyAfter ?? 0),
692
+ toSeconds(it.life.lasts ?? 0),
693
+ it.life.uses ?? 0,
694
+ toSeconds(it.life.cooldown ?? 0),
695
+ ];
528
696
  if (ready) needs.push({ place, key: 'made', test: { Older: [ready] } });
529
697
  if (lasts) needs.push({ place, key: 'made', test: { Within: [lasts] } });
530
698
  if (l.takes === 'renew') raises.push({ place, shift: { key: 'used', by: RENEW_BY, most: 0, from_now: false } });
@@ -538,8 +706,11 @@ export function lifeLines(r, m) {
538
706
  return { needs, raises };
539
707
  }
540
708
 
541
- /** A lottery's place as { percent, prize }: a number is a share of the pot and no prize. */
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 });
709
+ /** A lottery's place as { percent, prize }: a number is a percent of what the round pays from, and no prize. */
710
+ export const placeOf = p =>
711
+ typeof p === 'object' && p !== null
712
+ ? { percent: p.percent ?? 0, prize: prizeLine(p.prize), times: p.times ?? 1 }
713
+ : { percent: p, prize: null, times: 1 };
543
714
 
544
715
  /** "pool:house" → { kind: 'pool', key: 'house' }; "burn" → { kind: 'burn' }. */
545
716
  export function destination(to) {
@@ -551,13 +722,14 @@ export function destination(to) {
551
722
  }
552
723
 
553
724
  /** The share with its basis points, or null bps when the percent is not whole hundredths. */
554
- export const sharesOf = (split) => (split ?? []).map((s) => ({ ...destination(s.to), to: s.to, percent: s.percent, bps: toBps(s.percent) }));
725
+ export const sharesOf = split =>
726
+ (split ?? []).map(s => ({ ...destination(s.to), to: s.to, percent: s.percent, bps: toBps(s.percent) }));
555
727
 
556
728
  /**
557
729
  * The program's `To` for a destination, with pool indexes resolved (the IDL's own shape). A wallet ("wallet:<address>")
558
730
  * 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"
559
731
  * (or "chosen:<item>") is the one the buyer names when they buy; "lottery:<key>" is that lottery's pot
560
- * ("@pot:<id>") and "farm:<key>" that farm's rewards vault ("@rewards:<id>"), which the plan resolves once the game is
732
+ * ("@pot:<lottery id>:<pot id>") and "farm:<key>" that farm's rewards vault ("@rewards:<id>"), which the plan resolves once the game is
561
733
  * known.
562
734
  */
563
735
  export function toEnum(to, m) {
@@ -567,7 +739,8 @@ export function toEnum(to, m) {
567
739
  if (d.kind === 'pool') return { Pool: [m.pool.get(d.key)?.index ?? 0] };
568
740
  if (d.kind === 'account') return { Account: [d.key] };
569
741
  if (d.kind === 'wallet') return { Wallet: [d.key] };
570
- if (d.kind === 'lottery') return { Account: [`@pot:${m.lottery.get(d.key)?.id ?? 0}`] };
742
+ if (d.kind === 'lottery')
743
+ return { Account: [`@pot:${m.lottery.get(d.key)?.id ?? 0}:${m.lottery.get(d.key)?.pot ?? 0}`] };
571
744
  if (d.kind === 'farm') return { Account: [`@rewards:${m.farm.get(d.key)?.id ?? 0}`] };
572
745
  if (d.kind === 'chosen') return { Chosen: { item: d.key ? (m.item.get(d.key)?.id ?? 0) : null } };
573
746
  return { Holder: [d.key] };
@@ -580,13 +753,28 @@ export function toEnum(to, m) {
580
753
  export function ruleOf(rule, m) {
581
754
  if (!rule) return null;
582
755
  const time = { ahead: 'Ahead', passed: 'Passed' }[rule.time];
583
- const test = rule.atLeast !== undefined ? { AtLeast: [rule.atLeast] } : rule.equals !== undefined ? { Equals: { text: String(rule.equals) } } : time ?? 'Any';
584
- const of = rule.item ? { Item: [m.item.get(rule.item)?.id ?? 0] } : { Collection: [rule.collection ?? '@collection'] };
756
+ // Any of several items: the game's own collection, its `item_id` one of theirs.
757
+ if (rule.items)
758
+ return {
759
+ of: { Collection: ['@collection'] },
760
+ key: 'item_id',
761
+ test: { OneOf: { texts: rule.items.map(k => String(m.item.get(k)?.id ?? 0)) } },
762
+ };
763
+ const texts = rule.oneOf ?? (rule.equals !== undefined ? [rule.equals] : null);
764
+ const test =
765
+ rule.atLeast !== undefined
766
+ ? { AtLeast: [rule.atLeast] }
767
+ : texts
768
+ ? { OneOf: { texts: texts.map(String) } }
769
+ : (time ?? 'Any');
770
+ const of = rule.item
771
+ ? { Item: [m.item.get(rule.item)?.id ?? 0] }
772
+ : { Collection: [rule.collection ?? '@collection'] };
585
773
  return { of, key: rule.attribute ?? '', test };
586
774
  }
587
775
 
588
776
  /** One thing or a list of them, as a list: a farm's `gate` and `growth` take either. */
589
- export const listOf = (x) => (x == null ? [] : Array.isArray(x) ? x : [x]);
777
+ export const listOf = x => (x == null ? [] : Array.isArray(x) ? x : [x]);
590
778
 
591
779
  /**
592
780
  * The program's `Gate` for a spec gate: a few wallets (`wallets`), a list of any size (`list`: an allowlist, its root and
@@ -598,7 +786,12 @@ export const gateOf = (gate, m) => {
598
786
  if (gate.wallets) return { Wallets: [gate.wallets] };
599
787
  if (gate.staked) {
600
788
  const farm = m.farm.get(gate.staked.farm);
601
- return { Staked: { farm: `@farm:${farm?.id ?? 0}`, at_least: toUnits(gate.staked.atLeast ?? 0, m.tokens.get(farm?.stakes)?.decimals ?? 0) } };
789
+ return {
790
+ Staked: {
791
+ farm: `@farm:${farm?.id ?? 0}`,
792
+ at_least: toUnits(gate.staked.atLeast ?? 0, m.tokens.get(farm?.stakes)?.decimals ?? 0),
793
+ },
794
+ };
602
795
  }
603
796
  if (gate.list) {
604
797
  const { root, depth } = gateList(gate.list);
@@ -608,39 +801,55 @@ export const gateOf = (gate, m) => {
608
801
  };
609
802
 
610
803
  /** The program's `PerWallet` for a part's "perWallet" (in all) and "perWalletEach" ({ count, every }). */
611
- export const perWalletOf = (part) => ({
804
+ export const perWalletOf = part => ({
612
805
  total: part?.perWallet ?? 0,
613
806
  per_window: part?.perWalletEach?.count ?? 0,
614
807
  window: part?.perWalletEach ? toSeconds(part.perWalletEach.every ?? '1d') : 0,
615
808
  });
616
809
 
617
810
  /** The program's `Window` for a part's "opens" and "closes" (times; 0 leaves a side open). */
618
- export const windowOf = (part, now) => ({ opens_at: BigInt(part?.opens ? toTime(part.opens, now) : 0), closes_at: BigInt(part?.closes ? toTime(part.closes, now) : 0) });
811
+ export const windowOf = (part, now) => ({
812
+ opens_at: BigInt(part?.opens ? toTime(part.opens, now) : 0),
813
+ closes_at: BigInt(part?.closes ? toTime(part.closes, now) : 0),
814
+ });
815
+
816
+ /** Whether lottery `l` pays its places from a pot it keeps (`"pot"`, `PaysFrom::Pot`); false when each round pays from
817
+ * its own ticket money and sends what its places leave to `"rest"` (`PaysFrom::Round`). */
818
+ export const keepsPot = l => Number.isInteger(l?.pot);
819
+
820
+ /** The program's `PaysFrom` for a lottery: `{ Pot: [id] }` or `{ Round: { rest } }` (its `rest` as `toEnum` gives it). */
821
+ export const paysFromOf = (l, m) => (keepsPot(l) ? { Pot: [l.pot] } : { Round: { rest: toEnum(l.rest, m) } });
619
822
 
620
823
  /** A lottery's split of ticket money: its own, and "cutPercent" as a share to the treasury. */
621
- export const lotterySplit = (l) => [...(l.split ?? []), ...(l.cutPercent ? [{ to: 'treasury', percent: l.cutPercent }] : [])];
824
+ export const lotterySplit = l => [
825
+ ...(l.split ?? []),
826
+ ...(l.cutPercent ? [{ to: 'treasury', percent: l.cutPercent }] : []),
827
+ ];
622
828
 
623
829
  /** The program's `Effect` for a spec effect: a flat share, or a share per point of the attribute. */
624
830
  export function effectOf(effect) {
625
- if (effect?.perPointPercent !== undefined) return { PerPoint: { bps: Math.round(effect.perPointPercent * 100), most: effect.upTo ?? 4_294_967_295 } };
831
+ if (effect?.perPointPercent !== undefined)
832
+ return { PerPoint: { bps: Math.round(effect.perPointPercent * 100), most: effect.upTo ?? 4_294_967_295 } };
626
833
  return { Flat: [Math.round((effect?.addPercent ?? 0) * 100)] };
627
834
  }
628
835
 
629
836
  /** A box's outcomes with their program weights: chances become whole weights (hundredths of a basis point). */
630
837
  export function weights(box) {
631
838
  const outcomes = box.outcomes ?? [];
632
- if (outcomes.every((o) => Number.isInteger(o.weight))) return outcomes.map((o) => o.weight);
633
- const raw = outcomes.map((o) => Math.round((o.chance ?? 0) * 10_000));
839
+ if (outcomes.every(o => Number.isInteger(o.weight))) return outcomes.map(o => o.weight);
840
+ const raw = outcomes.map(o => Math.round((o.chance ?? 0) * 10_000));
634
841
  const gcd = (a, b) => (b ? gcd(b, a % b) : a);
635
842
  const g = raw.reduce((a, b) => gcd(a, b), 0) || 1;
636
- return raw.map((w) => w / g);
843
+ return raw.map(w => w / g);
637
844
  }
638
845
 
639
846
  /** Every place the spec names a pool as somewhere money leaves from, with what takes it. */
640
847
  export function poolTakers(m) {
641
848
  const takers = [];
642
849
  const add = (pool, by, how) => pool && takers.push({ pool, by, how });
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');
850
+ for (const b of m.allBoxes)
851
+ for (const o of (b.outcomes ?? []).flatMap(outcomeParts))
852
+ if (o.gives === 'pay') add(o.pool, `box:${b.key}`, 'pays prizes');
644
853
  for (const p of prizes(m)) add(prizePool(p.prize), p.by, `pays prizes (${p.route})`);
645
854
  for (const s of m.schedules) {
646
855
  if (s.does === 'burn') add(s.pool, `schedule:${s.key}`, 'burns a share');
@@ -649,7 +858,13 @@ export function poolTakers(m) {
649
858
  // from a vault of its own and names no pool.
650
859
  if (s.does === 'pay') add(s.from, `schedule:${s.key}`, 'pays out on a timer');
651
860
  }
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');
861
+ for (const d of m.decisions)
862
+ if (d.does === 'payout' || d.does === 'burn')
863
+ add(
864
+ d.pool,
865
+ `decision:${d.key}`,
866
+ d.does === 'payout' ? 'pays out on the authority’s word' : 'burns on the authority’s word',
867
+ );
653
868
  return takers;
654
869
  }
655
870
 
@@ -657,11 +872,16 @@ export function poolTakers(m) {
657
872
  export function poolFillers(m) {
658
873
  const fillers = [];
659
874
  const add = (pool, by, how) => pool && fillers.push({ pool, by, how });
660
- const fromSplit = (split, by) => sharesOf(split).forEach((s) => s.kind === 'pool' && add(s.key, by, `${s.percent}% of each payment`));
661
- for (const p of m.pools) if (p.seed !== undefined && Number(p.seed) > 0) add(p.key, `pool:${p.key}`, 'the owner’s seed');
875
+ const fromSplit = (split, by) =>
876
+ sharesOf(split).forEach(s => s.kind === 'pool' && add(s.key, by, `${s.percent}% of each payment`));
877
+ for (const p of m.pools)
878
+ if (p.seed !== undefined && Number(p.seed) > 0) add(p.key, `pool:${p.key}`, 'the owner’s seed');
662
879
  for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
663
880
  for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
664
- for (const r of m.recipes) for (const i of r.in ?? []) if (i.takes === 'pay' && destination(i.to).kind === 'pool') add(destination(i.to).key, `recipe:${r.key}`, 'a craft pays in');
881
+ for (const r of m.recipes)
882
+ for (const i of r.in ?? [])
883
+ if (i.takes === 'pay' && destination(i.to).kind === 'pool')
884
+ add(destination(i.to).key, `recipe:${r.key}`, 'a craft pays in');
665
885
  for (const s of m.schedules) {
666
886
  if (s.does === 'issue') add(s.pool, `schedule:${s.key}`, 'mints into it');
667
887
  if (s.does === 'move') add(s.to, `schedule:${s.key}`, 'moves a share in');
@@ -682,15 +902,18 @@ export function farmFillers(m) {
682
902
  const d = destination(to ?? '');
683
903
  if (d.kind === 'farm') fillers.push({ farm: d.key, by, how });
684
904
  };
685
- const fromSplit = (split, by) => sharesOf(split).forEach((s) => add(s.to, by, `${s.percent}% of each payment`));
905
+ const fromSplit = (split, by) => sharesOf(split).forEach(s => add(s.to, by, `${s.percent}% of each payment`));
686
906
  for (const f of m.farms) {
687
907
  if (Number(f.seed ?? 0) > 0) fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'the owner’s seed' });
688
908
  if (f.exitFeeTo === 'rewards') fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'early-exit fees' });
689
909
  }
690
910
  for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
691
911
  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');
912
+ for (const r of m.recipes)
913
+ for (const i of r.in ?? []) if (i.takes === 'pay') add(i.to, `recipe:${r.key}`, 'a craft pays in');
914
+ for (const s of m.schedules)
915
+ if (s.does === 'pay' || s.does === 'issue')
916
+ add(s.to, `schedule:${s.key}`, s.does === 'issue' ? 'mints into it on a timer' : 'pays in on a timer');
694
917
  for (const l of m.lotteries) fromSplit(lotterySplit(l), `lottery:${l.key}`);
695
918
  for (const c of m.contests) fromSplit(c.split, `contest:${c.key}`);
696
919
  for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);