@xgamesfun/solana 0.0.0-stage → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/DISCLAIMER.md +41 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +13 -0
  4. package/README.md +172 -2
  5. package/bin/xsolana.mjs +189 -0
  6. package/examples/artwork-auction.json +51 -0
  7. package/examples/blind-forge.json +128 -0
  8. package/examples/coin-flip-arena.json +103 -0
  9. package/examples/duel-arena.json +137 -0
  10. package/examples/elements-set.json +274 -0
  11. package/examples/event-badges.json +67 -0
  12. package/examples/expedition-guild.json +154 -0
  13. package/examples/fair-presale.json +48 -0
  14. package/examples/falling-drop.json +90 -0
  15. package/examples/guild-missions.json +137 -0
  16. package/examples/halving-seasons.json +107 -0
  17. package/examples/hero-trials.json +93 -0
  18. package/examples/ideas.json +789 -0
  19. package/examples/level-ladder.json +65 -0
  20. package/examples/merge-2048.json +128 -0
  21. package/examples/nft-raffle.json +33 -0
  22. package/examples/paper-hands-farm.json +44 -0
  23. package/examples/partner-vault.json +112 -0
  24. package/examples/sacrifice-raffle.json +118 -0
  25. package/examples/scorched-treasury.json +84 -0
  26. package/examples/shelter-fair.json +164 -0
  27. package/examples/spark-pity.json +153 -0
  28. package/examples/stakers-draw.json +118 -0
  29. package/examples/star-fleet.json +150 -0
  30. package/examples/starforce.json +116 -0
  31. package/examples/stud-farm.json +134 -0
  32. package/examples/team-vesting.json +70 -0
  33. package/examples/trade-in-shop.json +168 -0
  34. package/js/approve/details.mjs +263 -0
  35. package/js/approve/ed25519.mjs +36 -0
  36. package/js/approve/live-check.mjs +144 -0
  37. package/js/approve/page.html +86 -0
  38. package/js/approve/page.js +203 -0
  39. package/js/approve/server.mjs +726 -0
  40. package/js/chain/base58.js +73 -0
  41. package/js/chain/borsh.js +270 -0
  42. package/js/chain/bytes.js +141 -0
  43. package/js/chain/core.js +181 -0
  44. package/js/chain/curve.js +52 -0
  45. package/js/chain/lookup.js +279 -0
  46. package/js/chain/mint.js +96 -0
  47. package/js/chain/pda.js +66 -0
  48. package/js/chain/programs.js +102 -0
  49. package/js/chain/rpc.js +219 -0
  50. package/js/chain/sha256.js +68 -0
  51. package/js/chain/token.js +85 -0
  52. package/js/chain/token2022.js +247 -0
  53. package/js/chain/tx.js +224 -0
  54. package/js/client/accounts.js +679 -0
  55. package/js/client/compressed.js +46 -0
  56. package/js/client/grant.js +299 -0
  57. package/js/client/idl/loot.js +2 -0
  58. package/js/client/idl/loot.json +17206 -0
  59. package/js/client/index.js +10 -0
  60. package/js/client/ix.js +2887 -0
  61. package/js/client/pda.js +114 -0
  62. package/js/client/policy.js +69 -0
  63. package/js/client/random.js +70 -0
  64. package/js/client/sizes.js +297 -0
  65. package/js/client/traits.js +44 -0
  66. package/js/connect/connect.mjs +182 -0
  67. package/js/economy/check.mjs +2236 -0
  68. package/js/economy/cost.mjs +185 -0
  69. package/js/economy/examples.mjs +114 -0
  70. package/js/economy/explain.mjs +239 -0
  71. package/js/economy/guide.mjs +232 -0
  72. package/js/economy/legal.mjs +15 -0
  73. package/js/economy/live.mjs +130 -0
  74. package/js/economy/model.mjs +698 -0
  75. package/js/economy/plan.mjs +1155 -0
  76. package/js/economy/schema.json +3071 -0
  77. package/js/economy/secrets.mjs +60 -0
  78. package/js/economy/simulate.mjs +1019 -0
  79. package/js/economy/txsize.mjs +45 -0
  80. package/js/economy/validate.mjs +85 -0
  81. package/js/index.js +10 -0
  82. package/js/mcp/server.mjs +113 -0
  83. package/js/mcp/tools.mjs +485 -0
  84. package/js/play/index.js +2855 -0
  85. package/js/play/keys.js +167 -0
  86. package/js/results/index.js +184 -0
  87. package/js/results/wire.js +90 -0
  88. package/package.json +100 -4
  89. package/release.json +8 -0
  90. package/skills/xsolana-economy/SKILL.md +109 -0
  91. package/skills/xsolana-economy/references/after-launch.md +124 -0
  92. package/skills/xsolana-economy/references/apps.md +193 -0
  93. package/skills/xsolana-economy/references/sentence-to-spec.md +180 -0
  94. package/skills/xsolana-economy/references/telling-the-person.md +134 -0
  95. package/types/chain/base58.d.ts +10 -0
  96. package/types/chain/borsh.d.ts +23 -0
  97. package/types/chain/bytes.d.ts +45 -0
  98. package/types/chain/core.d.ts +52 -0
  99. package/types/chain/curve.d.ts +1 -0
  100. package/types/chain/lookup.d.ts +194 -0
  101. package/types/chain/mint.d.ts +25 -0
  102. package/types/chain/pda.d.ts +25 -0
  103. package/types/chain/programs.d.ts +65 -0
  104. package/types/chain/rpc.d.ts +68 -0
  105. package/types/chain/sha256.d.ts +1 -0
  106. package/types/chain/token.d.ts +141 -0
  107. package/types/chain/token2022.d.ts +53 -0
  108. package/types/chain/tx.d.ts +60 -0
  109. package/types/client/accounts.d.ts +318 -0
  110. package/types/client/compressed.d.ts +29 -0
  111. package/types/client/grant.d.ts +170 -0
  112. package/types/client/idl/loot.d.ts +4498 -0
  113. package/types/client/index.d.ts +9 -0
  114. package/types/client/ix.d.ts +5315 -0
  115. package/types/client/pda.d.ts +95 -0
  116. package/types/client/policy.d.ts +48 -0
  117. package/types/client/random.d.ts +26 -0
  118. package/types/client/sizes.d.ts +101 -0
  119. package/types/client/traits.d.ts +12 -0
  120. package/types/economy/check.d.mts +100 -0
  121. package/types/economy/cost.d.mts +69 -0
  122. package/types/economy/examples.d.mts +27 -0
  123. package/types/economy/explain.d.mts +19 -0
  124. package/types/economy/guide.d.mts +140 -0
  125. package/types/economy/legal.d.mts +12 -0
  126. package/types/economy/live.d.mts +124 -0
  127. package/types/economy/model.d.mts +431 -0
  128. package/types/economy/plan.d.mts +1014 -0
  129. package/types/economy/secrets.d.mts +4 -0
  130. package/types/economy/simulate.d.mts +164 -0
  131. package/types/economy/txsize.d.mts +17 -0
  132. package/types/economy/validate.d.mts +3 -0
  133. package/types/index.d.ts +9 -0
  134. package/types/mcp/tools.d.mts +635 -0
  135. package/types/play/index.d.ts +880 -0
  136. package/types/play/keys.d.ts +51 -0
  137. package/types/results/index.d.ts +155 -0
  138. package/types/results/wire.d.ts +122 -0
@@ -0,0 +1,698 @@
1
+ // The economy spec in the program's own terms: amounts in base units, percents in basis points, durations in
2
+ // seconds, every reference resolved and every part given its program id. The checker, the simulation, the cost
3
+ // and the setup plan all read a spec through this. Pure.
4
+ import { gateList } from '../client/grant.js';
5
+ import { treeSize } from '../client/ix.js';
6
+
7
+ // ---------------------------------------------------------------- the program's numbers
8
+ // Each is the loot program's own unless it says otherwise.
9
+
10
+ /** The two flat fees, in lamports, as the platform sets them by default; the program caps each at 0.01 SOL. */
11
+ export const FEES = Object.freeze({ player: 2_000_000n, project: 10_000n });
12
+ /**
13
+ * What MagicBlock's VRF oracle queue (Cuj97ggr…, ephemeral-vrf-sdk 0.17.2 consts.rs DEFAULT_QUEUE) takes for each
14
+ * random number asked of it: VRF_LAMPORTS_COST = 500,000 lamports, 0.0005 SOL (magicblock-labs/ephemeral-vrf,
15
+ * api/src/consts.rs; the high-priority queue's 800,000 is not the one loot uses). Paid by whoever asks: the player
16
+ * on `open_box` (order.rs), whoever sends `draw_lottery` (lottery.rs, its `payer`), the authority on `roll`
17
+ * (roll.rs). Measured on devnet: 500,000 lamports per open.
18
+ */
19
+ export const ORACLE_FEE = 800_000n;
20
+ /**
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.
24
+ */
25
+ export const SETTLE_RENT_UNIT = 6_000_000n;
26
+ /** Rent: (bytes + 128) × 5,080 lamports, as `solana rent` reads it on devnet and mainnet-beta. */
27
+ export const RENT_PER_BYTE = 5_080n;
28
+ export const ACCOUNT_OVERHEAD = 128n;
29
+ /**
30
+ * The most rent a byte has cost on any Solana cluster: 3,480 lamports a byte-year for two years, the rate before
31
+ * devnet and mainnet lowered it (a local validator and LiteSVM still charge it). An RPC that asks more is not believed.
32
+ */
33
+ export const MOST_RENT_PER_BYTE = 6_960n;
34
+ export const rent = (bytes) => (BigInt(bytes) + ACCOUNT_OVERHEAD) * RENT_PER_BYTE;
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
37
+ * about 190 bytes) plus Metaplex Core's creation fee of 0.0015 SOL, which the account keeps. The full simulation
38
+ * measures 0.00309 to 0.00313 SOL across its fixtures.
39
+ */
40
+ export const COLLECTIBLE_RENT = 3_100_000n;
41
+ /** The network's base fee per signature. Priority fees are extra and up to the wallet. */
42
+ export const SIGNATURE_FEE = 5_000n;
43
+ export const LAMPORTS_PER_SOL = 1_000_000_000n;
44
+ export const WSOL = 'So11111111111111111111111111111111111111112';
45
+ /** The limits a spec meets: the chain's (the oracle's answer carries 21 accounts; a farm names a tier and a recipe a
46
+ * collectible's place in a byte; Bubblegum keeps a compressed NFT's link in 200 bytes and its name in 32) and the two
47
+ * caps that keep a farm's numbers from overflowing. Since loot 0.22 the program caps no count or size of its own. */
48
+ export const LIMITS = Object.freeze({
49
+ callbackAccounts: 21, tiers: 256, collectiblesPerCraft: 255, vouchers: 3, compressedNameBytes: 32, compressedUriBytes: 200, maxBoostBps: 1_000_000, maxTierBps: 110_000,
50
+ // The program's caps on a record's lists (constants.rs, not in the IDL): a spec over one is refused before anything is signed.
51
+ boxEntries: 128, boxSlots: 32, boxOdds: 32, boxFates: 64, entryPrizes: 14, itemTraits: 64, traitKeyBytes: 64, recipeLines: 48, farmRules: 32, growthSteps: 128, contestSides: 1024, deciders: 32, collections: 32, lotteryRuns: 64, farmEnds: 100_000,
52
+ });
53
+ /** Attribute names the program writes itself, which a recipe cannot copy and `Set` cannot change (craft.rs, make.rs),
54
+ * and the two a collectible's life keeps (`lifeLines`: its uses and its next use). */
55
+ export const RESERVED_ATTRIBUTES = Object.freeze(['made', 'used', 'next_use', 'item_id', 'rarity', 'serial', 'origin']);
56
+ export const RARITIES = Object.freeze(['Common', 'Uncommon', 'Rare', 'Epic', 'Legendary', 'Mythic']);
57
+
58
+ // ---------------------------------------------------------------- units
59
+
60
+ /** A refusal raised while reading a value; the checker turns it into a refusal with a code. */
61
+ export class SpecError extends Error {
62
+ constructor(code, message, fix) {
63
+ super(message);
64
+ this.code = code;
65
+ this.fix = fix;
66
+ }
67
+ }
68
+
69
+ /** A plain decimal string for an amount written as a number or text (no exponent). */
70
+ export function decimalText(amount) {
71
+ 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".');
73
+ const text = String(amount);
74
+ if (!/e/i.test(text)) return text;
75
+ return amount.toFixed(20).replace(/\.?0+$/, '');
76
+ }
77
+
78
+ /** "0.1" of a currency with 9 decimals → 100000000n. Refuses more decimals than the currency has. */
79
+ export function toUnits(amount, decimals) {
80
+ 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".');
82
+ const [whole, fraction = ''] = text.split('.');
83
+ 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.`);
85
+ 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.');
87
+ return units;
88
+ }
89
+
90
+ /** 100000000n with 9 decimals → "0.1". */
91
+ export function fromUnits(units, decimals) {
92
+ const negative = units < 0n;
93
+ const digits = (negative ? -units : units).toString().padStart(decimals + 1, '0');
94
+ const whole = digits.slice(0, digits.length - decimals);
95
+ const fraction = digits.slice(digits.length - decimals).replace(/0+$/, '');
96
+ return `${negative ? '-' : ''}${whole}${fraction ? `.${fraction}` : ''}`;
97
+ }
98
+
99
+ /** 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
+ export function wholeCount(value) {
101
+ const text = typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? String(value) : value ?? '0';
102
+ return typeof text === 'string' && /^\d{1,19}$/.test(text) && BigInt(text) < 2n ** 64n ? BigInt(text) : null;
103
+ }
104
+
105
+ /** 2.5 (percent) → 250 basis points. Null when it is not a whole number of hundredths. */
106
+ export function toBps(percent) {
107
+ const bps = Math.round(Number(percent ?? 0) * 100);
108
+ return Math.abs(bps - Number(percent ?? 0) * 100) < 1e-6 && bps >= 0 && bps <= 10_000 ? bps : null;
109
+ }
110
+
111
+ const UNIT = { s: 1, m: 60, h: 3600, d: 86_400, w: 604_800 };
112
+ /** "7d" → 604800. A number is seconds already. */
113
+ export function toSeconds(duration) {
114
+ if (duration === undefined || duration === null) return 0;
115
+ if (typeof duration === 'number') return duration;
116
+ 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".');
118
+ return Math.round(Number(m[1]) * UNIT[m[2]]);
119
+ }
120
+
121
+ /**
122
+ * How long what lands in a farm's rewards vault takes to be released to its seats, in seconds (a farm that shares what
123
+ * lands, `farm.rs` `release_span`): its longest lock tier, and a day at least.
124
+ */
125
+ export const releaseSeconds = (f) => Math.max(86_400, ...(f.tiers ?? []).map((t) => toSeconds(t.lock ?? 0)));
126
+
127
+ /** A farm's release span in words: "a day", "30 days", "36 hours". */
128
+ export function releaseWords(f) {
129
+ const s = releaseSeconds(f);
130
+ if (s % 86_400 === 0) return s === 86_400 ? 'a day' : `${s / 86_400} days`;
131
+ return s % 3_600 === 0 ? `${s / 3_600} hours` : `${s} seconds`;
132
+ }
133
+
134
+ /**
135
+ * What a run sent late does, in words (schedule.rs `catches_up`): a sealed schedule's fixed amount pays every run due;
136
+ * any other run pays once, for the latest run due, and the runs before it lapse.
137
+ */
138
+ export const lateRunWords = (s) => (s.sealed && s.percent === undefined ? 'a run sent late pays every run due since' : 'a run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse');
139
+
140
+ /**
141
+ * Whether a farm's boost reads a number its own growth writes (`refresh_seat` writes it): each level counts from when it is
142
+ * written, so a page or a keeper should refresh seats.
143
+ */
144
+ export const boostReadsGrowth = (f) => (f.boosts ?? []).some((b) => b.when?.attribute && listOf(f.growth).some((g) => b.when.attribute === (g.attribute ?? 'xp') || ((g.levels ?? []).length > 0 && b.when.attribute === (g.level ?? 'level'))));
145
+
146
+ /** A box's roll against what is in stock, in words (order.rs): the odds stay as written, whatever is left. */
147
+ export const STOCK_WORDS = 'The odds stay as written whatever is in stock: a draw that lands on an outcome that can’t be given then (its pool short of the whole amount, its item or stash gone) is not made, no other outcome takes its place, and that draw’s worth of the price goes back at the settle; with no draw made at all, a paid open fails and is refunded, and a free roll goes back to waiting';
148
+
149
+ /**
150
+ * What a box that rolls does once an outcome can't be given (`whenOut`, order.rs `WhenOut`), in words. `sold`: it is
151
+ * sold (an open checks the stock); a box only other routes roll (a prize, a mission) never stops a sale.
152
+ */
153
+ export function whenOutWords(b, { sold = b.active !== false } = {}) {
154
+ if (b.whenOut === 'drawLeft' && !sold)
155
+ return 'When an outcome runs out (an item sold out, a pool short of its prize, a stash empty), a roll draws among the outcomes still there by their written weights, so the others’ chances rise; with nothing left at all, a free roll waits to be asked again and a paid one is refunded';
156
+ if (b.whenOut === 'drawLeft')
157
+ return 'When an outcome runs out (an item sold out, a pool short of its prize, a stash empty), the box keeps selling while its first draw has anything left, and a roll draws among the outcomes still there by their written weights: the others’ chances rise as one runs out, so a player can empty a common outcome to make a rare one likelier (a box selling a rare prize should stop selling instead, the default)';
158
+ if (!sold) return STOCK_WORDS;
159
+ return 'When an outcome runs out, the box stops selling: it isn’t sold while any outcome couldn’t cover this purchase’s draws and those of purchases still waiting for the oracle (an item’s units left, a pool’s tokens, a stash’s NFTs), so every roll is on the odds as written and nobody can buy their way to a refund on every miss; it sells again once the project restocks or edits the box (an outcome whose odds shrink with stock needs no cover). An outcome emptied another way all the same (another route sharing its pool, item or stash) makes no draw, no other outcome taking its place, and gives back that draw’s worth of the price at the settle; with no draw made at all, the open is refunded';
160
+ }
161
+
162
+ /**
163
+ * What the end of a sharing farm that `runs` means, in words, or null (no end, or a fixed rate): its release keeps its
164
+ * pace, so what it has not let go by the end stays in its vault, free for the authority to take back, and so does what
165
+ * a seat forfeits after the end.
166
+ */
167
+ export function farmEndWords(f) {
168
+ if (!f.runs || f.rate) return null;
169
+ const span = releaseWords(f);
170
+ return `what is not released by its end stays in its vault, credited to nobody (the authority may take back what is free there), and so does what a seat forfeits after the end: fund it at least ${span} before its end (what lands later is released only in part, and what lands at or after the end not at all)`;
171
+ }
172
+
173
+ /**
174
+ * The token a farm's rewards are in (`rewards`, a token key): by default its staked token, or for a farm of
175
+ * collectibles the project's currency. One that names a pool (as farms did before they had a vault of their own) is
176
+ * read as that pool's token, and the checker refuses it.
177
+ */
178
+ export function farmRewardsToken(f, tokens, currencyKey, poolCurrency = new Map()) {
179
+ if (f.rewards && tokens.has(f.rewards)) return f.rewards;
180
+ if (f.rewards && poolCurrency.has(f.rewards)) return poolCurrency.get(f.rewards);
181
+ return f.stakes && f.stakes !== 'collectibles' ? f.stakes : currencyKey;
182
+ }
183
+
184
+ /** A raise's or a shift's `by`: a number, or for a time a duration, which may be negative ("-1d"). */
185
+ export function toBy(by) {
186
+ if (typeof by !== 'string') return by;
187
+ return by.startsWith('-') ? -toSeconds(by.slice(1)) : toSeconds(by);
188
+ }
189
+
190
+ /** "publish", "publish+7d" or a date → Unix seconds, counted from `now` (Unix seconds) for the first two. */
191
+ export function toTime(time, now) {
192
+ if (!time || time === 'publish') return now;
193
+ const m = /^publish\+(.+)$/.exec(time);
194
+ if (m) return now + toSeconds(m[1]);
195
+ 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".');
197
+ return Math.floor(at / 1000);
198
+ }
199
+
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);
203
+
204
+ // ---------------------------------------------------------------- the model
205
+
206
+ /** Gives every part without one the next free program id (pools from 0, the rest from 1). */
207
+ function assignIds(list, field, first) {
208
+ const used = new Set(list.map((x) => x[field]).filter((v) => Number.isInteger(v)));
209
+ let next = first;
210
+ return list.map((x) => {
211
+ if (Number.isInteger(x[field])) return x;
212
+ while (used.has(next)) next += 1;
213
+ used.add(next);
214
+ return { ...x, [field]: next };
215
+ });
216
+ }
217
+
218
+ /** Where an NFT paid goes, in the program's words (`Spend`, `Then`): "burn", "treasury" (its wallet, filled in at go-live), { giveTo }, or { stash }. */
219
+ export const spendOf = (then, m) => {
220
+ if (!then || then === 'burn') return 'Burn';
221
+ if (then === 'treasury') return { Give: ['@treasury'] };
222
+ if (then.stash) return { Stash: [m.stash.get(then.stash)?.id ?? 0] };
223
+ return { Give: [then.giveTo] };
224
+ };
225
+
226
+ /** What a token is on chain: its mint (or "@mint:<key>" until it exists), program, decimals, and who can mint it. */
227
+ function tokenOf(given) {
228
+ // "project" is the spec's word for a token the project mints; "game" is the older spelling of it.
229
+ 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 };
231
+ const decimals = t.decimals ?? (t.source === 'game' ? 0 : undefined);
232
+ return {
233
+ ...t,
234
+ symbol: t.symbol ?? t.key.toUpperCase().slice(0, 10),
235
+ name: t.name ?? t.symbol ?? t.key,
236
+ decimals,
237
+ program: t.program ?? 'spl-token',
238
+ mint: t.mint ?? `@mint:${t.key}`,
239
+ wsol: t.mint === WSOL,
240
+ // A game token with a fixed supply is minted once, to the owner, and nobody mints it after.
241
+ gameMints: (t.source === 'game' && t.supply === undefined) || ['game', 'project'].includes(t.powers?.mintAuthority),
242
+ fixedSupply: t.source === 'game' && t.supply !== undefined,
243
+ };
244
+ }
245
+
246
+ /**
247
+ * The spec with references resolved and ids given. Unknown references stay as they are (the checker reports
248
+ * them); call `check` first when it matters.
249
+ */
250
+ export function model(economy, { now = 0 } = {}) {
251
+ const e = economy ?? {};
252
+ const tokens = new Map((e.tokens ?? []).map((t) => [t.key, tokenOf(t)]));
253
+ const currencyKey = e.currency ?? (tokens.size === 1 ? [...tokens.keys()][0] : undefined);
254
+ const currency = tokens.get(currencyKey);
255
+ // Numbers are given in this order: the pools that have one, then the rest in the spec's order.
256
+ const listed = e.pools ?? [];
257
+ const order = [...listed.keys()].sort((a, b) => Number(!Number.isInteger(listed[a].index)) - Number(!Number.isInteger(listed[b].index)));
258
+ const assigned = assignIds(order.map((i) => listed[i]), 'index', 0);
259
+ const pools = listed.map((_, i) => assigned[order.indexOf(i)]);
260
+ // A farm's rewards land in a vault of its own: `rewardsToken` is their token.
261
+ const poolCurrency = new Map(pools.map((p) => [p.key, p.currency]));
262
+ const farms = (e.farms ?? []).map((f) => ({ ...f, rewardsToken: farmRewardsToken(f, tokens, currencyKey, poolCurrency) }));
263
+ // A raffle: its NFT is first place's prize, held in the lottery's own stash.
264
+ const lotteries = (e.lotteries ?? []).map((l) => {
265
+ if (!l.raffle) return l;
266
+ 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 ?? [])] };
268
+ });
269
+ const m = {
270
+ now,
271
+ name: e.name ?? 'Project',
272
+ authority: e.keys?.authority ?? 'owner',
273
+ /** The wallet whose account for the currency is the treasury, when it isn't the owner's ("wallet:<address>"). */
274
+ treasuryWallet: typeof e.treasury === 'string' && e.treasury.startsWith('wallet:') ? e.treasury.slice(7) : null,
275
+ tokens,
276
+ currencyKey,
277
+ currency,
278
+ /** 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 },
280
+ /** Core collections the owner already has that the project also takes (its update authority is the owner). */
281
+ collections: e.collections ?? [],
282
+ /** How many compressed NFTs the project's tree holds (`compressedTree`), when a prize gives one. */
283
+ treeHolds: e.compressedTree?.holds ?? 16_384,
284
+ /** The project's number among its owner's: its address is ["project", owner, number]. */
285
+ projectNumber: e.projectNumber ?? 0,
286
+ items: assignIds(e.items ?? [], 'id', 1),
287
+ pools,
288
+ stashes: assignIds(e.stashes ?? [], 'id', 1),
289
+ tables: assignIds(e.tables ?? [], 'id', 1),
290
+ boxes: [],
291
+ payments: e.payments ?? [],
292
+ recipes: assignIds(e.recipes ?? [], 'id', 1),
293
+ schedules: assignIds(e.schedules ?? [], 'id', 1),
294
+ lotteries: assignIds(lotteries, 'id', 1),
295
+ farms: assignIds(farms, 'id', 1),
296
+ contests: assignIds(e.contests ?? [], 'id', 1),
297
+ auctions: assignIds(e.auctions ?? [], 'id', 1),
298
+ holds: e.holds ?? [],
299
+ grants: assignIds(e.grants ?? [], 'id', 1),
300
+ traits: e.traits ?? [],
301
+ decisions: e.decisions ?? [],
302
+ scenario: e.scenario ?? {},
303
+ live: e.live ?? {},
304
+ };
305
+ // A shop item is a box of one entry (the program sells items only through boxes). Its box takes the item's price,
306
+ // rise, fall, split and gate; it is numbered with the boxes, after the ones the spec numbered itself.
307
+ const shop = m.items
308
+ .filter((it) => Number(it.price ?? 0) > 0 || it.free)
309
+ .map((it) => ({
310
+ key: `shop-${it.key}`,
311
+ ...(Number.isInteger(it.shopBox) ? { id: it.shopBox } : {}),
312
+ name: it.name,
313
+ price: it.price ?? 0,
314
+ ...(it.rise ? { rise: it.rise } : {}),
315
+ ...(it.fall ? { fall: it.fall } : {}),
316
+ ...(it.split ? { split: it.split } : {}),
317
+ ...(it.gate ? { gate: it.gate } : {}),
318
+ // 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]])),
320
+ active: it.active !== false,
321
+ outcomes: [{ gives: 'item', item: it.key, weight: 1 }],
322
+ shopOf: it.key,
323
+ }));
324
+ const numbered = assignIds([...(e.boxes ?? []), ...shop], 'id', 1);
325
+ m.boxes = numbered.filter((b) => !b.shopOf);
326
+ m.shop = numbered.filter((b) => b.shopOf);
327
+ /** Every box the program holds: the spec's own and the shop's. */
328
+ m.allBoxes = numbered;
329
+ const index = (list) => new Map(list.map((x) => [x.key, x]));
330
+ m.item = index(m.items);
331
+ m.pool = index(m.pools);
332
+ m.stash = index(m.stashes);
333
+ m.table = index(m.tables);
334
+ m.box = index(m.allBoxes);
335
+ m.recipe = index(m.recipes);
336
+ m.farm = index(m.farms);
337
+ m.grant = index(m.grants);
338
+ m.lottery = index(m.lotteries);
339
+ m.contest = index(m.contests);
340
+ m.auction = index(m.auctions);
341
+ m.hold = index(m.holds);
342
+ /** The token a pool holds. */
343
+ m.poolToken = (key) => tokens.get(m.pool.get(key)?.currency);
344
+ /** The token a part is priced in (its own `currency`, else the game's). */
345
+ m.priceToken = (part) => tokens.get(part?.currency ?? currencyKey);
346
+ return m;
347
+ }
348
+
349
+ /**
350
+ * The NFTs a stash lists, each as the program's `piece` takes it: a Core asset (`asset`, its `collection`: its own, the
351
+ * stash's, or the game's when neither says), a Token Metadata NFT (`mint`, `programmable`, `ruleSet`), or a compressed
352
+ * NFT (`compressed`, its asset id).
353
+ */
354
+ export function stashNfts(s) {
355
+ return (s.nfts ?? []).map((n) => {
356
+ if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null, weight: 1 };
357
+ const weight = n.weight ?? 1;
358
+ if (n.mint) return { kind: 'token-metadata', asset: n.mint, programmable: Boolean(n.programmable), ruleSet: n.ruleSet ?? null, weight };
359
+ if (n.compressed) return { kind: 'compressed', asset: n.compressed, weight };
360
+ return { kind: 'core', asset: n.asset, collection: n.collection ?? s.collection ?? null, weight };
361
+ });
362
+ }
363
+
364
+ /**
365
+ * A prize as the spec writes it (`$defs/prize`), read the one way: a line { gives, … } as a recipe's `out` lines are:
366
+ * "item" (`item`), "tokens" (`amount`, `from` "mint" with `currency`, or "pool:<key>"),
367
+ * "nft" (`nft`, an address the stash `stash` lists) or "roll" (a roll of `box`; in a recipe, on the collectible kept
368
+ * at `on`). A grant's older `gives` ({ item }, { pool, amount }, { roll }) reads as its line. Null for none.
369
+ */
370
+ export function prizeLine(p) {
371
+ if (!p || typeof p !== 'object') return null;
372
+ if (p.gives) return p;
373
+ if (p.item) return { gives: 'item', item: p.item };
374
+ if (p.roll) return { gives: 'roll', box: p.roll };
375
+ if (p.pool) return { gives: 'tokens', amount: p.amount, from: `pool:${p.pool}` };
376
+ return null;
377
+ }
378
+
379
+ /** The pool a prize line pays from: tokens' "pool:<key>". */
380
+ export const prizePool = (p) => (p?.gives === 'tokens' && p.from?.startsWith('pool:') ? p.from.slice(5) : null);
381
+
382
+ /** 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);
384
+
385
+ /**
386
+ * Every prize the spec gives (prize.rs): a grant's, a lottery place's, a contest's winners', a mission tier's and a
387
+ * recipe's `out` lines. [{ prize (a line), where, by, route, strict, once }] `strict`: the route refuses a prize it
388
+ * can't give (a grant's claim, a craft); any other goes on without it. `once`: the route gives it once at most (a
389
+ * grant of one claim in all).
390
+ */
391
+ export function prizes(m) {
392
+ 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);
399
+ return list;
400
+ }
401
+
402
+ /**
403
+ * The project's tree of compressed NFTs (`open_tree`), when a prize gives one (`"compressed": true`), else null: deep
404
+ * enough for `treeHolds` (14 to 20 levels, Bubblegum's sizes for a buffer of 64), its canopy keeping a proof at 10
405
+ * nodes or a few more (at most 8 levels: past that the rent grows faster than the proofs shrink). { depth, buffer,
406
+ * canopy, bytes }
407
+ */
408
+ 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;
411
+ const depth = Math.min(20, Math.max(14, Math.ceil(Math.log2(Math.max(1, m.treeHolds)))));
412
+ const canopy = Math.min(depth - 10, 8);
413
+ return { depth, buffer: 64, canopy, bytes: treeSize(depth, 64, canopy) };
414
+ }
415
+
416
+ /**
417
+ * Every route that gives a roll of a box, with the box it names: a prize's roll (`prizes`), an auction's lot, a
418
+ * mission's roll on the NFT leaving it and a recipe's roll on a collectible it keeps. [{ box, by, route }]
419
+ */
420
+ 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);
426
+ }
427
+
428
+ /**
429
+ * Every roll of a box any route makes, for the checks keyed on "a box something rolls": `prizeRolls`, every box
430
+ * outcome's (or bundle part's) roll of another box (route "box", made at its settle), and, followed through chains,
431
+ * each route that rolls a box whose outcome rolls another (a grant's roll of “mid”, whose outcome rolls “inner”, is a
432
+ * roll of “inner” by the grant, `through` ["mid"]). [{ box, by, route, through? }]
433
+ */
434
+ export function rollsOf(m) {
435
+ 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) : [])];
439
+ // A breadth-first walk: each roll of a box is also a roll of every box it rolls, once for each route.
440
+ for (let i = 0; i < rolls.length; i++) {
441
+ const r = rolls[i];
442
+ for (const next of rollsFrom(r.box)) {
443
+ const id = `${next}|${r.by}|${r.route}`;
444
+ if (seen.has(id)) continue;
445
+ seen.add(id);
446
+ rolls.push({ box: next, by: r.by, route: r.route, through: [...(r.through ?? []), r.box] });
447
+ }
448
+ }
449
+ return rolls;
450
+ }
451
+
452
+ /** A prize line in words: "one Badge", "100 GOLD from “rewards”", "100 GOLD, minted", "the NFT ab12…cd34 of “vault”", "a roll of “Chest”". */
453
+ export function prizeWords(p, m) {
454
+ const line = prizeLine(p);
455
+ 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)' : ''}`;
458
+ 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}”`;
461
+ if (line.gives === 'roll') return `a roll of “${m.box.get(line.box)?.name ?? line.box}”`;
462
+ const pool = prizePool(line);
463
+ 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}`;
465
+ }
466
+
467
+ /**
468
+ * What one box outcome gives (catalog.rs), each in the box's own words: "item", "pay" (a fixed amount from a pool),
469
+ * "stash" (an NFT of a stash at random), "mint"
470
+ * (tokens minted), "nft" (one NFT a stash lists) or "roll" (a roll of another box). A bundle gives its `prizes`; "burn"
471
+ * and "nothing" give none.
472
+ */
473
+ export function outcomeParts(o) {
474
+ if (!o) return [];
475
+ if (o.gives === 'bundle') return (o.prizes ?? []).flatMap(outcomeParts);
476
+ 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 }];
478
+ return [o];
479
+ }
480
+
481
+ /** Whether a box leaves its outcome to the oracle (catalog.rs `rolls`): more than one outcome, or an NFT at random. */
482
+ /** How many draws one open of a box makes: its slots' counts, or one. */
483
+ export const drawsOf = (b) => ((b.slots ?? []).length ? b.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
484
+
485
+ /** Whether a box's prize costs its settle a collectible's rent: an item minted as a Core NFT, or a fresh compressed one
486
+ * (its traits' Sheet). A plain compressed NFT costs no rent. */
487
+ export const holdsRent = (p, m) => p.gives === 'item' && (!p.compressed || (m?.item.get(p.item)?.drawn ?? []).length > 0);
488
+
489
+ /**
490
+ * The units of `SETTLE_RENT_UNIT` a settle of box `b`'s order may spend (`loot.settleRent`), which js/play funds its
491
+ * throwaway settle key with and gets back unspent: for each draw, each part of the richest outcome that opens an
492
+ * account (a collectible, a fresh compressed NFT's Sheet, a stash's NFT); once an order, each source of tokens it may
493
+ * pay, and the fate of the NFT it rolls on.
494
+ */
495
+ export function settleUnits(b, m) {
496
+ const opens = (p) => holdsRent(p, m) || p.gives === 'stash' || p.gives === 'nft';
497
+ const parts = (b.outcomes ?? []).map((o) => outcomeParts(o));
498
+ const most = Math.max(0, ...parts.map((ps) => ps.filter(opens).length));
499
+ const sources = new Set(parts.flat().filter((p) => ['tokens', 'pay', 'mint'].includes(p.gives)).map((p) => prizePool(p) ?? `mint:${p.currency ?? ''}`));
500
+ return drawsOf(b) * most + sources.size + (b.rollsOn ? 1 : 0);
501
+ }
502
+
503
+ export const boxRolls = (b, m) => (b.outcomes ?? []).length > 1 || (b.outcomes ?? []).flatMap(outcomeParts).some((p) => p.gives === 'stash' || (p.gives === 'item' && (m?.item.get(p.item)?.drawn ?? []).length > 0));
504
+
505
+ /** Whether a box sells with `buy`, in one transaction (buy.rs): it doesn't roll and rolls on no NFT, any number of draws
506
+ * a sale (a fixed pack: its prizes given once for each draw). */
507
+ export const boxBuys = (b, m) => !boxRolls(b, m) && !b.rollsOn;
508
+
509
+ /** The most a raise lowers a number by (an i32's least but one): a renew's, which counts a collectible's uses from 0. */
510
+ const RENEW_BY = -2_147_483_647;
511
+
512
+ /**
513
+ * What a recipe's lines owe the life of each collectible they take (an item's `life`), as the program's needs and
514
+ * raises (craft.rs `Need`, `Raise`), at each collectible's place in the recipe (each burned unit, then each kept one, in
515
+ * line order; a pay line takes none): ready after `readyAfter` (`made` older than it), for `lasts` (`made` within it),
516
+ * on every line that takes it; `uses` times (`used` raised by 1 up to it) and a `cooldown` (`next_use` passed, then
517
+ * raised to that far from now) on a line that uses it or takes it staked; a renew line counts its uses from 0 again.
518
+ * { needs, raises }, in the program's shapes.
519
+ */
520
+ export function lifeLines(r, m) {
521
+ const places = (r.in ?? []).flatMap((l) => (l.takes === 'pay' ? [] : Array(l.takes === 'burn' ? l.count ?? 1 : 1).fill(l)));
522
+ const needs = [];
523
+ const raises = [];
524
+ places.forEach((l, place) => {
525
+ const it = l.item ? m.item.get(l.item) : null;
526
+ if (!it?.life || it.kind === 'normal') return;
527
+ const [ready, lasts, uses, cooldown] = [toSeconds(it.life.readyAfter ?? 0), toSeconds(it.life.lasts ?? 0), it.life.uses ?? 0, toSeconds(it.life.cooldown ?? 0)];
528
+ if (ready) needs.push({ place, key: 'made', test: { Older: [ready] } });
529
+ if (lasts) needs.push({ place, key: 'made', test: { Within: [lasts] } });
530
+ if (l.takes === 'renew') raises.push({ place, shift: { key: 'used', by: RENEW_BY, most: 0, from_now: false } });
531
+ if (l.takes !== 'use' && l.takes !== 'staked') return;
532
+ if (uses) raises.push({ place, shift: { key: 'used', by: 1, most: uses, from_now: false } });
533
+ if (cooldown) {
534
+ needs.push({ place, key: 'next_use', test: 'Passed' });
535
+ raises.push({ place, shift: { key: 'next_use', by: cooldown, most: cooldown, from_now: true } });
536
+ }
537
+ });
538
+ return { needs, raises };
539
+ }
540
+
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 });
543
+
544
+ /** "pool:house" → { kind: 'pool', key: 'house' }; "burn" → { kind: 'burn' }. */
545
+ export function destination(to) {
546
+ if (to === 'burn') return { kind: 'burn' };
547
+ if (to === 'treasury') return { kind: 'treasury' };
548
+ if (to === 'chosen') return { kind: 'chosen', key: '' };
549
+ const [kind, ...rest] = String(to).split(':');
550
+ return { kind, key: rest.join(':') };
551
+ }
552
+
553
+ /** 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) }));
555
+
556
+ /**
557
+ * The program's `To` for a destination, with pool indexes resolved (the IDL's own shape). A wallet ("wallet:<address>")
558
+ * is paid at its own token account of the part's currency (the program opens it when it is not there); "treasury" is the game's treasury wallet's; "chosen"
559
+ * (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
561
+ * known.
562
+ */
563
+ export function toEnum(to, m) {
564
+ const d = destination(to);
565
+ if (d.kind === 'burn') return 'Burn';
566
+ if (d.kind === 'treasury') return 'Treasury';
567
+ if (d.kind === 'pool') return { Pool: [m.pool.get(d.key)?.index ?? 0] };
568
+ if (d.kind === 'account') return { Account: [d.key] };
569
+ if (d.kind === 'wallet') return { Wallet: [d.key] };
570
+ if (d.kind === 'lottery') return { Account: [`@pot:${m.lottery.get(d.key)?.id ?? 0}`] };
571
+ if (d.kind === 'farm') return { Account: [`@rewards:${m.farm.get(d.key)?.id ?? 0}`] };
572
+ if (d.kind === 'chosen') return { Chosen: { item: d.key ? (m.item.get(d.key)?.id ?? 0) : null } };
573
+ return { Holder: [d.key] };
574
+ }
575
+
576
+ /**
577
+ * The program's `Rule` for a spec rule: which NFTs (a collection's, or an item's the game mints), an attribute and its
578
+ * test. With neither, the collection the game has authority over: `@collection`, filled in when the economy goes live.
579
+ */
580
+ export function ruleOf(rule, m) {
581
+ if (!rule) return null;
582
+ 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'] };
585
+ return { of, key: rule.attribute ?? '', test };
586
+ }
587
+
588
+ /** 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]);
590
+
591
+ /**
592
+ * The program's `Gate` for a spec gate: a few wallets (`wallets`), a list of any size (`list`: an allowlist, its root and
593
+ * depth: the client's `gateList`), a stake (`staked`: at least `atLeast` of a farm's token in the wallet's seat; the farm's address
594
+ * is "@farm:<id>", filled in when the project is known), or a rule a collectible the player shows must pass.
595
+ */
596
+ export const gateOf = (gate, m) => {
597
+ if (!gate) return null;
598
+ if (gate.wallets) return { Wallets: [gate.wallets] };
599
+ if (gate.staked) {
600
+ 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) } };
602
+ }
603
+ if (gate.list) {
604
+ const { root, depth } = gateList(gate.list);
605
+ return { List: { root, depth } };
606
+ }
607
+ return { Holds: [ruleOf(gate, m)] };
608
+ };
609
+
610
+ /** The program's `PerWallet` for a part's "perWallet" (in all) and "perWalletEach" ({ count, every }). */
611
+ export const perWalletOf = (part) => ({
612
+ total: part?.perWallet ?? 0,
613
+ per_window: part?.perWalletEach?.count ?? 0,
614
+ window: part?.perWalletEach ? toSeconds(part.perWalletEach.every ?? '1d') : 0,
615
+ });
616
+
617
+ /** 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) });
619
+
620
+ /** 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 }] : [])];
622
+
623
+ /** The program's `Effect` for a spec effect: a flat share, or a share per point of the attribute. */
624
+ export function effectOf(effect) {
625
+ if (effect?.perPointPercent !== undefined) return { PerPoint: { bps: Math.round(effect.perPointPercent * 100), most: effect.upTo ?? 4_294_967_295 } };
626
+ return { Flat: [Math.round((effect?.addPercent ?? 0) * 100)] };
627
+ }
628
+
629
+ /** A box's outcomes with their program weights: chances become whole weights (hundredths of a basis point). */
630
+ export function weights(box) {
631
+ 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));
634
+ const gcd = (a, b) => (b ? gcd(b, a % b) : a);
635
+ const g = raw.reduce((a, b) => gcd(a, b), 0) || 1;
636
+ return raw.map((w) => w / g);
637
+ }
638
+
639
+ /** Every place the spec names a pool as somewhere money leaves from, with what takes it. */
640
+ export function poolTakers(m) {
641
+ const takers = [];
642
+ 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');
644
+ for (const p of prizes(m)) add(prizePool(p.prize), p.by, `pays prizes (${p.route})`);
645
+ for (const s of m.schedules) {
646
+ if (s.does === 'burn') add(s.pool, `schedule:${s.key}`, 'burns a share');
647
+ if (s.does === 'move') add(s.from, `schedule:${s.key}`, 'moves a share out');
648
+ // A pay schedule pays out of its pool (to a wallet, a token account, a holder or a lottery's pot); a sealed one pays
649
+ // from a vault of its own and names no pool.
650
+ if (s.does === 'pay') add(s.from, `schedule:${s.key}`, 'pays out on a timer');
651
+ }
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');
653
+ return takers;
654
+ }
655
+
656
+ /** Every place the spec sends money into a pool. */
657
+ export function poolFillers(m) {
658
+ const fillers = [];
659
+ 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');
662
+ for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
663
+ 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');
665
+ for (const s of m.schedules) {
666
+ if (s.does === 'issue') add(s.pool, `schedule:${s.key}`, 'mints into it');
667
+ if (s.does === 'move') add(s.to, `schedule:${s.key}`, 'moves a share in');
668
+ }
669
+ for (const l of m.lotteries) fromSplit(lotterySplit(l), `lottery:${l.key}`);
670
+ for (const c of m.contests) fromSplit(c.split, `contest:${c.key}`);
671
+ for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
672
+ return fillers;
673
+ }
674
+
675
+ /**
676
+ * Every place the spec sends money into a farm's rewards vault ("farm:<key>"): a split's share, a schedule that pays it,
677
+ * a craft that pays it, the owner's seed, an early exit's fee into its own vault. [{ farm, by, how }]
678
+ */
679
+ export function farmFillers(m) {
680
+ const fillers = [];
681
+ const add = (to, by, how) => {
682
+ const d = destination(to ?? '');
683
+ if (d.kind === 'farm') fillers.push({ farm: d.key, by, how });
684
+ };
685
+ const fromSplit = (split, by) => sharesOf(split).forEach((s) => add(s.to, by, `${s.percent}% of each payment`));
686
+ for (const f of m.farms) {
687
+ if (Number(f.seed ?? 0) > 0) fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'the owner’s seed' });
688
+ if (f.exitFeeTo === 'rewards') fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'early-exit fees' });
689
+ }
690
+ for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
691
+ for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
692
+ for (const r of m.recipes) for (const i of r.in ?? []) if (i.takes === 'pay') add(i.to, `recipe:${r.key}`, 'a craft pays in');
693
+ for (const s of m.schedules) if (s.does === 'pay' || s.does === 'issue') add(s.to, `schedule:${s.key}`, s.does === 'issue' ? 'mints into it on a timer' : 'pays in on a timer');
694
+ for (const l of m.lotteries) fromSplit(lotterySplit(l), `lottery:${l.key}`);
695
+ for (const c of m.contests) fromSplit(c.split, `contest:${c.key}`);
696
+ for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
697
+ return fillers;
698
+ }