@xgamesfun/solana 0.20.1 → 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 (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
@@ -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;
@@ -30,7 +31,7 @@ export const ACCOUNT_OVERHEAD = 128n;
30
31
  * devnet and mainnet lowered it (a local validator and LiteSVM still charge it). An RPC that asks more is not believed.
31
32
  */
32
33
  export const MOST_RENT_PER_BYTE = 6_960n;
33
- export const rent = (bytes) => (BigInt(bytes) + ACCOUNT_OVERHEAD) * RENT_PER_BYTE;
34
+ export const rent = bytes => (BigInt(bytes) + ACCOUNT_OVERHEAD) * RENT_PER_BYTE;
34
35
  /**
35
36
  * A collectible's Core account, paid from the SOL its order holds (or by whoever gives it): its rent (a Core asset of
36
37
  * about 190 bytes) plus Metaplex Core's creation fee of 0.0015 SOL, which the account keeps. The full simulation
@@ -41,10 +42,38 @@ 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,
50
+ tiers: 256,
51
+ collectiblesPerCraft: 255,
52
+ vouchers: 3,
53
+ compressedNameBytes: 32,
54
+ compressedUriBytes: 200,
55
+ maxBoostBps: 1_000_000,
56
+ maxTierBps: 110_000,
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.
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,
73
+ });
74
+ /** Attribute names the program writes itself, which a recipe cannot copy and `Set` cannot change (craft.rs, make.rs),
75
+ * and the two a collectible's life keeps (`lifeLines`: its uses and its next use). */
76
+ export const RESERVED_ATTRIBUTES = Object.freeze(['made', 'used', 'next_use', 'item_id', 'rarity', 'serial', 'origin']);
48
77
  export const RARITIES = Object.freeze(['Common', 'Uncommon', 'Rare', 'Epic', 'Legendary', 'Mythic']);
49
78
 
50
79
  // ---------------------------------------------------------------- units
@@ -61,7 +90,8 @@ export class SpecError extends Error {
61
90
  /** A plain decimal string for an amount written as a number or text (no exponent). */
62
91
  export function decimalText(amount) {
63
92
  if (typeof amount === 'string') return amount;
64
- 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".');
65
95
  const text = String(amount);
66
96
  if (!/e/i.test(text)) return text;
67
97
  return amount.toFixed(20).replace(/\.?0+$/, '');
@@ -70,12 +100,19 @@ export function decimalText(amount) {
70
100
  /** "0.1" of a currency with 9 decimals → 100000000n. Refuses more decimals than the currency has. */
71
101
  export function toUnits(amount, decimals) {
72
102
  const text = decimalText(amount ?? 0);
73
- 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".');
74
105
  const [whole, fraction = ''] = text.split('.');
75
106
  const trimmed = fraction.replace(/0+$/, '');
76
- 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
+ );
77
113
  const units = BigInt(whole + trimmed.padEnd(decimals, '0'));
78
- 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.');
79
116
  return units;
80
117
  }
81
118
 
@@ -90,7 +127,7 @@ export function fromUnits(units, decimals) {
90
127
 
91
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. */
92
129
  export function wholeCount(value) {
93
- 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');
94
131
  return typeof text === 'string' && /^\d{1,19}$/.test(text) && BigInt(text) < 2n ** 64n ? BigInt(text) : null;
95
132
  }
96
133
 
@@ -106,15 +143,16 @@ export function toSeconds(duration) {
106
143
  if (duration === undefined || duration === null) return 0;
107
144
  if (typeof duration === 'number') return duration;
108
145
  const m = /^(\d+(?:\.\d+)?)(s|m|h|d|w)$/.exec(duration);
109
- 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".');
110
148
  return Math.round(Number(m[1]) * UNIT[m[2]]);
111
149
  }
112
150
 
113
151
  /**
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.
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
153
+ * lands, `farm.rs` `release_span`): its longest lock tier, and a day at least.
116
154
  */
117
- 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)));
118
156
 
119
157
  /** A farm's release span in words: "a day", "30 days", "36 hours". */
120
158
  export function releaseWords(f) {
@@ -123,16 +161,68 @@ export function releaseWords(f) {
123
161
  return s % 3_600 === 0 ? `${s / 3_600} hours` : `${s} seconds`;
124
162
  }
125
163
 
164
+ /**
165
+ * What a run sent late does, in words (schedule.rs `catches_up`): a sealed schedule's fixed amount pays every run due;
166
+ * any other run pays once, for the latest run due, and the runs before it lapse.
167
+ */
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';
172
+
173
+ /**
174
+ * Whether a farm's boost reads a number its own growth writes (`refresh_seat` writes it): each level counts from when it is
175
+ * written, so a page or a keeper should refresh seats.
176
+ */
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
+ );
187
+
188
+ /** A box's roll against what is in stock, in words (order.rs): the odds stay as written, whatever is left. */
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';
191
+
192
+ /**
193
+ * What a box that rolls does once an outcome can't be given (`whenOut`, order.rs `WhenOut`), in words. `sold`: it is
194
+ * sold (an open checks the stock); a box only other routes roll (a prize, a mission) never stops a sale.
195
+ */
196
+ export function whenOutWords(b, { sold = b.active !== false } = {}) {
197
+ if (b.whenOut === 'drawLeft' && !sold)
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';
199
+ if (b.whenOut === 'drawLeft')
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)';
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';
204
+ }
205
+
126
206
  /**
127
207
  * 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.
208
+ * 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
209
+ * a seat forfeits after the end.
130
210
  */
131
211
  export function farmEndWords(f) {
132
212
  if (!f.runs || f.rate) return null;
133
213
  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` : ''}`;
214
+ 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)`;
215
+ }
216
+
217
+ /**
218
+ * The token a farm's rewards are in (`rewards`, a token key): by default its staked token, or for a farm of
219
+ * collectibles the project's currency. One that names a pool (as farms did before they had a vault of their own) is
220
+ * read as that pool's token, and the checker refuses it.
221
+ */
222
+ export function farmRewardsToken(f, tokens, currencyKey, poolCurrency = new Map()) {
223
+ if (f.rewards && tokens.has(f.rewards)) return f.rewards;
224
+ if (f.rewards && poolCurrency.has(f.rewards)) return poolCurrency.get(f.rewards);
225
+ return f.stakes && f.stakes !== 'collectibles' ? f.stakes : currencyKey;
136
226
  }
137
227
 
138
228
  /** A raise's or a shift's `by`: a number, or for a time a duration, which may be negative ("-1d"). */
@@ -147,21 +237,27 @@ export function toTime(time, now) {
147
237
  const m = /^publish\+(.+)$/.exec(time);
148
238
  if (m) return now + toSeconds(m[1]);
149
239
  const at = Date.parse(time);
150
- 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
+ );
151
246
  return Math.floor(at / 1000);
152
247
  }
153
248
 
154
- export const rarityNumber = (rarity) => (typeof rarity === 'number' ? rarity : Math.max(0, RARITIES.indexOf(rarity ?? 'Common')));
155
- export const utf8Bytes = (s) => new TextEncoder().encode(s ?? '').length;
156
- 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);
157
253
 
158
254
  // ---------------------------------------------------------------- the model
159
255
 
160
256
  /** Gives every part without one the next free program id (pools from 0, the rest from 1). */
161
257
  function assignIds(list, field, first) {
162
- 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)));
163
259
  let next = first;
164
- return list.map((x) => {
260
+ return list.map(x => {
165
261
  if (Number.isInteger(x[field])) return x;
166
262
  while (used.has(next)) next += 1;
167
263
  used.add(next);
@@ -181,7 +277,17 @@ export const spendOf = (then, m) => {
181
277
  function tokenOf(given) {
182
278
  // "project" is the spec's word for a token the project mints; "game" is the older spelling of it.
183
279
  const t = given.source === 'project' ? { ...given, source: 'game' } : given;
184
- 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
+ };
185
291
  const decimals = t.decimals ?? (t.source === 'game' ? 0 : undefined);
186
292
  return {
187
293
  ...t,
@@ -203,29 +309,37 @@ function tokenOf(given) {
203
309
  */
204
310
  export function model(economy, { now = 0 } = {}) {
205
311
  const e = economy ?? {};
206
- 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)]));
207
313
  const currencyKey = e.currency ?? (tokens.size === 1 ? [...tokens.keys()][0] : undefined);
208
314
  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]));
221
- const assigned = assignIds(order.map((i) => listed[i]), 'index', 0);
315
+ // Numbers are given in this order: the pools that have one, then the rest in the spec's order.
316
+ const listed = e.pools ?? [];
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
+ );
222
325
  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 }));
326
+ // A farm's rewards land in a vault of its own: `rewardsToken` is their token.
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
+ }));
224
332
  // A raffle: its NFT is first place's prize, held in the lottery's own stash.
225
- const lotteries = (e.lotteries ?? []).map((l) => {
333
+ const lotteries = (e.lotteries ?? []).map(l => {
226
334
  if (!l.raffle) return l;
227
335
  const { nft, collection } = typeof l.raffle === 'string' ? { nft: l.raffle } : l.raffle;
228
- 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
+ };
229
343
  });
230
344
  const m = {
231
345
  now,
@@ -237,7 +351,15 @@ export function model(economy, { now = 0 } = {}) {
237
351
  currencyKey,
238
352
  currency,
239
353
  /** The collection the setup makes (null when the spec says `"collection": false`: a project of tokens only). */
240
- 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
+ },
241
363
  /** Core collections the owner already has that the project also takes (its update authority is the owner). */
242
364
  collections: e.collections ?? [],
243
365
  /** How many compressed NFTs the project's tree holds (`compressedTree`), when a prize gives one. */
@@ -247,6 +369,7 @@ export function model(economy, { now = 0 } = {}) {
247
369
  items: assignIds(e.items ?? [], 'id', 1),
248
370
  pools,
249
371
  stashes: assignIds(e.stashes ?? [], 'id', 1),
372
+ tables: assignIds(e.tables ?? [], 'id', 1),
250
373
  boxes: [],
251
374
  payments: e.payments ?? [],
252
375
  recipes: assignIds(e.recipes ?? [], 'id', 1),
@@ -265,8 +388,8 @@ export function model(economy, { now = 0 } = {}) {
265
388
  // A shop item is a box of one entry (the program sells items only through boxes). Its box takes the item's price,
266
389
  // rise, fall, split and gate; it is numbered with the boxes, after the ones the spec numbered itself.
267
390
  const shop = m.items
268
- .filter((it) => Number(it.price ?? 0) > 0 || it.free)
269
- .map((it) => ({
391
+ .filter(it => Number(it.price ?? 0) > 0 || it.free)
392
+ .map(it => ({
270
393
  key: `shop-${it.key}`,
271
394
  ...(Number.isInteger(it.shopBox) ? { id: it.shopBox } : {}),
272
395
  name: it.name,
@@ -276,20 +399,23 @@ export function model(economy, { now = 0 } = {}) {
276
399
  ...(it.split ? { split: it.split } : {}),
277
400
  ...(it.gate ? { gate: it.gate } : {}),
278
401
  // When it sells, and how many one wallet may buy.
279
- ...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
+ ),
280
405
  active: it.active !== false,
281
406
  outcomes: [{ gives: 'item', item: it.key, weight: 1 }],
282
407
  shopOf: it.key,
283
408
  }));
284
409
  const numbered = assignIds([...(e.boxes ?? []), ...shop], 'id', 1);
285
- m.boxes = numbered.filter((b) => !b.shopOf);
286
- m.shop = numbered.filter((b) => b.shopOf);
410
+ m.boxes = numbered.filter(b => !b.shopOf);
411
+ m.shop = numbered.filter(b => b.shopOf);
287
412
  /** Every box the program holds: the spec's own and the shop's. */
288
413
  m.allBoxes = numbered;
289
- const index = (list) => new Map(list.map((x) => [x.key, x]));
414
+ const index = list => new Map(list.map(x => [x.key, x]));
290
415
  m.item = index(m.items);
291
416
  m.pool = index(m.pools);
292
417
  m.stash = index(m.stashes);
418
+ m.table = index(m.tables);
293
419
  m.box = index(m.allBoxes);
294
420
  m.recipe = index(m.recipes);
295
421
  m.farm = index(m.farms);
@@ -299,9 +425,9 @@ export function model(economy, { now = 0 } = {}) {
299
425
  m.auction = index(m.auctions);
300
426
  m.hold = index(m.holds);
301
427
  /** The token a pool holds. */
302
- m.poolToken = (key) => tokens.get(m.pool.get(key)?.currency);
428
+ m.poolToken = key => tokens.get(m.pool.get(key)?.currency);
303
429
  /** The token a part is priced in (its own `currency`, else the game's). */
304
- m.priceToken = (part) => tokens.get(part?.currency ?? currencyKey);
430
+ m.priceToken = part => tokens.get(part?.currency ?? currencyKey);
305
431
  return m;
306
432
  }
307
433
 
@@ -311,17 +437,25 @@ export function model(economy, { now = 0 } = {}) {
311
437
  * NFT (`compressed`, its asset id).
312
438
  */
313
439
  export function stashNfts(s) {
314
- 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 };
440
+ return (s.nfts ?? []).map(n => {
441
+ if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null, weight: 1 };
442
+ const weight = n.weight ?? 1;
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
+ };
451
+ if (n.compressed) return { kind: 'compressed', asset: n.compressed, weight };
452
+ return { kind: 'core', asset: n.asset, collection: n.collection ?? s.collection ?? null, weight };
319
453
  });
320
454
  }
321
455
 
322
456
  /**
323
457
  * 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`),
458
+ * "item" (`item`), "tokens" (`amount`, `from` "mint" with `currency`, or "pool:<key>"),
325
459
  * "nft" (`nft`, an address the stash `stash` lists) or "roll" (a roll of `box`; in a recipe, on the collectible kept
326
460
  * at `on`). A grant's older `gives` ({ item }, { pool, amount }, { roll }) reads as its line. Null for none.
327
461
  */
@@ -334,11 +468,12 @@ export function prizeLine(p) {
334
468
  return null;
335
469
  }
336
470
 
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);
471
+ /** The pool a prize line pays from: tokens' "pool:<key>". */
472
+ export const prizePool = p => (p?.gives === 'tokens' && p.from?.startsWith('pool:') ? p.from.slice(5) : null);
339
473
 
340
474
  /** The token a prize line pays in: its pool's, or the one it mints. */
341
- 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;
342
477
 
343
478
  /**
344
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
@@ -348,12 +483,40 @@ export const prizeToken = (p, m) => (prizePool(p) ? m.poolToken(prizePool(p)) :
348
483
  */
349
484
  export function prizes(m) {
350
485
  const list = [];
351
- const add = (p, where, by, route, strict, once = false) => p && list.push({ prize: p, where, by, route, strict, once });
352
- 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);
353
- 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));
354
- for (const c of m.contests) add(prizeLine(c.prize), `economy.contests.${c.key}.prize`, `contest:${c.key}`, 'contest', false);
355
- 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));
356
- 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
+ );
357
520
  return list;
358
521
  }
359
522
 
@@ -364,7 +527,10 @@ export function prizes(m) {
364
527
  * canopy, bytes }
365
528
  */
366
529
  export function compressedTree(m) {
367
- if (!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;
368
534
  const depth = Math.min(20, Math.max(14, Math.ceil(Math.log2(Math.max(1, m.treeHolds)))));
369
535
  const canopy = Math.min(depth - 10, 8);
370
536
  return { depth, buffer: 64, canopy, bytes: treeSize(depth, 64, canopy) };
@@ -375,11 +541,17 @@ export function compressedTree(m) {
375
541
  * mission's roll on the NFT leaving it and a recipe's roll on a collectible it keeps. [{ box, by, route }]
376
542
  */
377
543
  export function prizeRolls(m) {
378
- const rolls = prizes(m).filter((p) => p.prize.gives === 'roll').map((p) => ({ box: p.prize.box, by: p.by, route: p.route }));
379
- for (const a of m.auctions) if (a.lot?.roll) rolls.push({ box: a.lot.roll, by: `auction:${a.key}`, route: 'auction' });
380
- 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' });
381
- 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' });
382
- 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);
383
555
  }
384
556
 
385
557
  /**
@@ -390,9 +562,21 @@ export function prizeRolls(m) {
390
562
  */
391
563
  export function rollsOf(m) {
392
564
  const rolls = [...prizeRolls(m)];
393
- 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' });
394
- const seen = new Set(rolls.map((r) => `${r.box}|${r.by}|${r.route}`));
395
- 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
+ ];
396
580
  // A breadth-first walk: each roll of a box is also a roll of every box it rolls, once for each route.
397
581
  for (let i = 0; i < rolls.length; i++) {
398
582
  const r = rolls[i];
@@ -406,46 +590,127 @@ export function rollsOf(m) {
406
590
  return rolls;
407
591
  }
408
592
 
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”". */
593
+ /** 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
594
  export function prizeWords(p, m) {
411
595
  const line = prizeLine(p);
412
596
  if (!line) return 'nothing';
413
- const sym = (key) => m.tokens.get(key)?.symbol ?? key;
414
- 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}”`;
416
- 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
- 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}”`;
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)' : ''}`;
600
+ if (line.gives === 'raffled') return 'an NFT drawn at random from the lottery’s own 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}”`;
418
605
  if (line.gives === 'roll') return `a roll of “${m.box.get(line.box)?.name ?? line.box}”`;
419
606
  const pool = prizePool(line);
420
607
  const split = line.split ? ', split by what each winner put in' : '';
421
- 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}`;
422
611
  }
423
612
 
424
613
  /**
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"
614
+ * What one box outcome gives (catalog.rs), each in the box's own words: "item", "pay" (a fixed amount from a pool),
615
+ * "stash" (an NFT of a stash at random), "mint"
427
616
  * (tokens minted), "nft" (one NFT a stash lists) or "roll" (a roll of another box). A bundle gives its `prizes`; "burn"
428
617
  * and "nothing" give none.
429
618
  */
430
619
  export function outcomeParts(o) {
431
620
  if (!o) return [];
432
621
  if (o.gives === 'bundle') return (o.prizes ?? []).flatMap(outcomeParts);
433
- if (o.gives === 'burn' || o.gives === 'nothing') return [];
434
- 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 }];
622
+ if (o.gives === 'nothing') return [];
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
+ ];
435
629
  return [o];
436
630
  }
437
631
 
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
632
  /** 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');
633
+ /** How many draws one open of a box makes: its slots' counts, or one. */
634
+ export const drawsOf = b => ((b.slots ?? []).length ? b.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
635
+
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
637
+ * (its traits' Sheet). A plain compressed NFT costs no rent. */
638
+ export const holdsRent = (p, m) =>
639
+ p.gives === 'item' && (!p.compressed || (m?.item.get(p.item)?.drawn ?? []).length > 0);
640
+
641
+ /**
642
+ * The units of `SETTLE_RENT_UNIT` a settle of box `b`'s order may spend (`loot.settleRent`), which js/play funds its
643
+ * throwaway settle key with and gets back unspent: for each draw, each part of the richest outcome that opens an
644
+ * account (a collectible, a fresh compressed NFT's Sheet, a stash's NFT); once an order, each source of tokens it may
645
+ * pay, and the fate of the NFT it rolls on.
646
+ */
647
+ export function settleUnits(b, m) {
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
+ );
657
+ return drawsOf(b) * most + sources.size + (b.rollsOn ? 1 : 0);
658
+ }
659
+
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));
665
+
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
667
+ * a sale (a fixed pack: its prizes given once for each draw). */
668
+ export const boxBuys = (b, m) => !boxRolls(b, m) && !b.rollsOn;
669
+
670
+ /** 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. */
671
+ const RENEW_BY = -2_147_483_647;
443
672
 
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;
673
+ /**
674
+ * What a recipe's lines owe the life of each collectible they take (an item's `life`), as the program's needs and
675
+ * raises (craft.rs `Need`, `Raise`), at each collectible's place in the recipe (each burned unit, then each kept one, in
676
+ * line order; a pay line takes none): ready after `readyAfter` (`made` older than it), for `lasts` (`made` within it),
677
+ * on every line that takes it; `uses` times (`used` raised by 1 up to it) and a `cooldown` (`next_use` passed, then
678
+ * 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.
679
+ * { needs, raises }, in the program's shapes.
680
+ */
681
+ export function lifeLines(r, m) {
682
+ const places = (r.in ?? []).flatMap(l =>
683
+ l.takes === 'pay' ? [] : Array(l.takes === 'burn' ? (l.count ?? 1) : 1).fill(l),
684
+ );
685
+ const needs = [];
686
+ const raises = [];
687
+ places.forEach((l, place) => {
688
+ const it = l.item ? m.item.get(l.item) : null;
689
+ if (!it?.life || it.kind === 'normal') return;
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
+ ];
696
+ if (ready) needs.push({ place, key: 'made', test: { Older: [ready] } });
697
+ if (lasts) needs.push({ place, key: 'made', test: { Within: [lasts] } });
698
+ if (l.takes === 'renew') raises.push({ place, shift: { key: 'used', by: RENEW_BY, most: 0, from_now: false } });
699
+ if (l.takes !== 'use' && l.takes !== 'staked') return;
700
+ if (uses) raises.push({ place, shift: { key: 'used', by: 1, most: uses, from_now: false } });
701
+ if (cooldown) {
702
+ needs.push({ place, key: 'next_use', test: 'Passed' });
703
+ raises.push({ place, shift: { key: 'next_use', by: cooldown, most: cooldown, from_now: true } });
704
+ }
705
+ });
706
+ return { needs, raises };
707
+ }
446
708
 
447
- /** 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 });
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 };
449
714
 
450
715
  /** "pool:house" → { kind: 'pool', key: 'house' }; "burn" → { kind: 'burn' }. */
451
716
  export function destination(to) {
@@ -457,13 +722,15 @@ export function destination(to) {
457
722
  }
458
723
 
459
724
  /** The share with its basis points, or null bps when the percent is not whole hundredths. */
460
- 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) }));
461
727
 
462
728
  /**
463
729
  * The program's `To` for a destination, with pool indexes resolved (the IDL's own shape). A wallet ("wallet:<address>")
464
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"
465
731
  * (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).
732
+ * ("@pot:<lottery id>:<pot id>") and "farm:<key>" that farm's rewards vault ("@rewards:<id>"), which the plan resolves once the game is
733
+ * known.
467
734
  */
468
735
  export function toEnum(to, m) {
469
736
  const d = destination(to);
@@ -472,7 +739,9 @@ export function toEnum(to, m) {
472
739
  if (d.kind === 'pool') return { Pool: [m.pool.get(d.key)?.index ?? 0] };
473
740
  if (d.kind === 'account') return { Account: [d.key] };
474
741
  if (d.kind === 'wallet') return { Wallet: [d.key] };
475
- 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}`] };
744
+ if (d.kind === 'farm') return { Account: [`@rewards:${m.farm.get(d.key)?.id ?? 0}`] };
476
745
  if (d.kind === 'chosen') return { Chosen: { item: d.key ? (m.item.get(d.key)?.id ?? 0) : null } };
477
746
  return { Holder: [d.key] };
478
747
  }
@@ -484,13 +753,28 @@ export function toEnum(to, m) {
484
753
  export function ruleOf(rule, m) {
485
754
  if (!rule) return null;
486
755
  const time = { ahead: 'Ahead', passed: 'Passed' }[rule.time];
487
- const test = rule.atLeast !== undefined ? { AtLeast: [rule.atLeast] } : rule.equals !== undefined ? { Equals: { text: String(rule.equals) } } : time ?? 'Any';
488
- 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'] };
489
773
  return { of, key: rule.attribute ?? '', test };
490
774
  }
491
775
 
492
776
  /** One thing or a list of them, as a list: a farm's `gate` and `growth` take either. */
493
- export const listOf = (x) => (x == null ? [] : Array.isArray(x) ? x : [x]);
777
+ export const listOf = x => (x == null ? [] : Array.isArray(x) ? x : [x]);
494
778
 
495
779
  /**
496
780
  * The program's `Gate` for a spec gate: a few wallets (`wallets`), a list of any size (`list`: an allowlist, its root and
@@ -502,7 +786,12 @@ export const gateOf = (gate, m) => {
502
786
  if (gate.wallets) return { Wallets: [gate.wallets] };
503
787
  if (gate.staked) {
504
788
  const farm = m.farm.get(gate.staked.farm);
505
- 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
+ };
506
795
  }
507
796
  if (gate.list) {
508
797
  const { root, depth } = gateList(gate.list);
@@ -512,40 +801,56 @@ export const gateOf = (gate, m) => {
512
801
  };
513
802
 
514
803
  /** The program's `PerWallet` for a part's "perWallet" (in all) and "perWalletEach" ({ count, every }). */
515
- export const perWalletOf = (part) => ({
804
+ export const perWalletOf = part => ({
516
805
  total: part?.perWallet ?? 0,
517
806
  per_window: part?.perWalletEach?.count ?? 0,
518
807
  window: part?.perWalletEach ? toSeconds(part.perWalletEach.every ?? '1d') : 0,
519
808
  });
520
809
 
521
810
  /** The program's `Window` for a part's "opens" and "closes" (times; 0 leaves a side open). */
522
- 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) } });
523
822
 
524
823
  /** A lottery's split of ticket money: its own, and "cutPercent" as a share to the treasury. */
525
- 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
+ ];
526
828
 
527
829
  /** The program's `Effect` for a spec effect: a flat share, or a share per point of the attribute. */
528
830
  export function effectOf(effect) {
529
- 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 } };
530
833
  return { Flat: [Math.round((effect?.addPercent ?? 0) * 100)] };
531
834
  }
532
835
 
533
836
  /** A box's outcomes with their program weights: chances become whole weights (hundredths of a basis point). */
534
837
  export function weights(box) {
535
838
  const outcomes = box.outcomes ?? [];
536
- if (outcomes.every((o) => Number.isInteger(o.weight))) return outcomes.map((o) => o.weight);
537
- 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));
538
841
  const gcd = (a, b) => (b ? gcd(b, a % b) : a);
539
842
  const g = raw.reduce((a, b) => gcd(a, b), 0) || 1;
540
- return raw.map((w) => w / g);
843
+ return raw.map(w => w / g);
541
844
  }
542
845
 
543
846
  /** Every place the spec names a pool as somewhere money leaves from, with what takes it. */
544
847
  export function poolTakers(m) {
545
848
  const takers = [];
546
849
  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})`);
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');
853
+ for (const p of prizes(m)) add(prizePool(p.prize), p.by, `pays prizes (${p.route})`);
549
854
  for (const s of m.schedules) {
550
855
  if (s.does === 'burn') add(s.pool, `schedule:${s.key}`, 'burns a share');
551
856
  if (s.does === 'move') add(s.from, `schedule:${s.key}`, 'moves a share out');
@@ -553,13 +858,13 @@ export function poolTakers(m) {
553
858
  // from a vault of its own and names no pool.
554
859
  if (s.does === 'pay') add(s.from, `schedule:${s.key}`, 'pays out on a timer');
555
860
  }
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
- 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
+ );
563
868
  return takers;
564
869
  }
565
870
 
@@ -567,11 +872,16 @@ export function poolTakers(m) {
567
872
  export function poolFillers(m) {
568
873
  const fillers = [];
569
874
  const add = (pool, by, how) => pool && fillers.push({ pool, by, how });
570
- const fromSplit = (split, by) => sharesOf(split).forEach((s) => s.kind === 'pool' && add(s.key, by, `${s.percent}% of each payment`));
571
- 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');
572
879
  for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
573
880
  for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
574
- 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');
575
885
  for (const s of m.schedules) {
576
886
  if (s.does === 'issue') add(s.pool, `schedule:${s.key}`, 'mints into it');
577
887
  if (s.does === 'move') add(s.to, `schedule:${s.key}`, 'moves a share in');
@@ -581,3 +891,31 @@ export function poolFillers(m) {
581
891
  for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
582
892
  return fillers;
583
893
  }
894
+
895
+ /**
896
+ * Every place the spec sends money into a farm's rewards vault ("farm:<key>"): a split's share, a schedule that pays it,
897
+ * a craft that pays it, the owner's seed, an early exit's fee into its own vault. [{ farm, by, how }]
898
+ */
899
+ export function farmFillers(m) {
900
+ const fillers = [];
901
+ const add = (to, by, how) => {
902
+ const d = destination(to ?? '');
903
+ if (d.kind === 'farm') fillers.push({ farm: d.key, by, how });
904
+ };
905
+ const fromSplit = (split, by) => sharesOf(split).forEach(s => add(s.to, by, `${s.percent}% of each payment`));
906
+ for (const f of m.farms) {
907
+ if (Number(f.seed ?? 0) > 0) fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'the owner’s seed' });
908
+ if (f.exitFeeTo === 'rewards') fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'early-exit fees' });
909
+ }
910
+ for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
911
+ for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
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');
917
+ for (const l of m.lotteries) fromSplit(lotterySplit(l), `lottery:${l.key}`);
918
+ for (const c of m.contests) fromSplit(c.split, `contest:${c.key}`);
919
+ for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
920
+ return fillers;
921
+ }