@xgamesfun/solana 0.0.0-stage → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) 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 +108 -0
  9. package/examples/duel-arena.json +137 -0
  10. package/examples/elements-set.json +281 -0
  11. package/examples/event-badges.json +67 -0
  12. package/examples/expedition-guild.json +161 -0
  13. package/examples/fair-presale.json +48 -0
  14. package/examples/falling-drop.json +90 -0
  15. package/examples/guild-missions.json +144 -0
  16. package/examples/halving-seasons.json +112 -0
  17. package/examples/hero-trials.json +96 -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 +56 -0
  23. package/examples/partner-vault.json +115 -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 +125 -0
  29. package/examples/star-fleet.json +157 -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 +262 -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 +711 -0
  40. package/js/chain/base58.js +73 -0
  41. package/js/chain/borsh.js +261 -0
  42. package/js/chain/bytes.js +141 -0
  43. package/js/chain/core.js +169 -0
  44. package/js/chain/curve.js +52 -0
  45. package/js/chain/lookup.js +273 -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 +217 -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 +347 -0
  55. package/js/client/compressed.js +46 -0
  56. package/js/client/grant.js +240 -0
  57. package/js/client/idl/loot.js +2 -0
  58. package/js/client/idl/loot.json +17064 -0
  59. package/js/client/index.js +9 -0
  60. package/js/client/ix.js +2156 -0
  61. package/js/client/pda.js +110 -0
  62. package/js/client/policy.js +48 -0
  63. package/js/client/sizes.js +54 -0
  64. package/js/client/traits.js +44 -0
  65. package/js/connect/connect.mjs +182 -0
  66. package/js/economy/check.mjs +1991 -0
  67. package/js/economy/cost.mjs +170 -0
  68. package/js/economy/examples.mjs +114 -0
  69. package/js/economy/explain.mjs +235 -0
  70. package/js/economy/guide.mjs +231 -0
  71. package/js/economy/legal.mjs +15 -0
  72. package/js/economy/live.mjs +107 -0
  73. package/js/economy/model.mjs +583 -0
  74. package/js/economy/plan.mjs +887 -0
  75. package/js/economy/schema.json +2923 -0
  76. package/js/economy/secrets.mjs +60 -0
  77. package/js/economy/simulate.mjs +952 -0
  78. package/js/economy/txsize.mjs +45 -0
  79. package/js/economy/validate.mjs +85 -0
  80. package/js/index.js +10 -0
  81. package/js/mcp/server.mjs +113 -0
  82. package/js/mcp/tools.mjs +479 -0
  83. package/js/play/index.js +2109 -0
  84. package/js/results/index.js +184 -0
  85. package/js/results/wire.js +90 -0
  86. package/package.json +100 -4
  87. package/release.json +8 -0
  88. package/skills/xsolana-economy/SKILL.md +105 -0
  89. package/skills/xsolana-economy/references/after-launch.md +114 -0
  90. package/skills/xsolana-economy/references/apps.md +157 -0
  91. package/skills/xsolana-economy/references/sentence-to-spec.md +172 -0
  92. package/skills/xsolana-economy/references/telling-the-person.md +93 -0
  93. package/types/chain/base58.d.ts +10 -0
  94. package/types/chain/borsh.d.ts +23 -0
  95. package/types/chain/bytes.d.ts +45 -0
  96. package/types/chain/core.d.ts +50 -0
  97. package/types/chain/curve.d.ts +1 -0
  98. package/types/chain/lookup.d.ts +191 -0
  99. package/types/chain/mint.d.ts +25 -0
  100. package/types/chain/pda.d.ts +25 -0
  101. package/types/chain/programs.d.ts +65 -0
  102. package/types/chain/rpc.d.ts +66 -0
  103. package/types/chain/sha256.d.ts +1 -0
  104. package/types/chain/token.d.ts +141 -0
  105. package/types/chain/token2022.d.ts +53 -0
  106. package/types/chain/tx.d.ts +60 -0
  107. package/types/client/accounts.d.ts +178 -0
  108. package/types/client/compressed.d.ts +29 -0
  109. package/types/client/grant.d.ts +125 -0
  110. package/types/client/idl/loot.d.ts +4467 -0
  111. package/types/client/index.d.ts +8 -0
  112. package/types/client/ix.d.ts +1687 -0
  113. package/types/client/pda.d.ts +91 -0
  114. package/types/client/policy.d.ts +23 -0
  115. package/types/client/sizes.d.ts +13 -0
  116. package/types/client/traits.d.ts +12 -0
  117. package/types/economy/check.d.mts +105 -0
  118. package/types/economy/cost.d.mts +68 -0
  119. package/types/economy/examples.d.mts +27 -0
  120. package/types/economy/explain.d.mts +19 -0
  121. package/types/economy/guide.d.mts +140 -0
  122. package/types/economy/legal.d.mts +12 -0
  123. package/types/economy/live.d.mts +109 -0
  124. package/types/economy/model.d.mts +363 -0
  125. package/types/economy/plan.d.mts +1013 -0
  126. package/types/economy/secrets.d.mts +4 -0
  127. package/types/economy/simulate.d.mts +156 -0
  128. package/types/economy/txsize.d.mts +17 -0
  129. package/types/economy/validate.d.mts +3 -0
  130. package/types/index.d.ts +9 -0
  131. package/types/mcp/tools.d.mts +627 -0
  132. package/types/play/index.d.ts +469 -0
  133. package/types/results/index.d.ts +155 -0
  134. package/types/results/wire.d.ts +122 -0
@@ -0,0 +1,583 @@
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 = 500_000n;
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).
23
+ */
24
+ export const ESCROW_LAMPORTS = 6_000_000n;
25
+ /** Rent: (bytes + 128) × 5,080 lamports, as `solana rent` reads it on devnet and mainnet-beta. */
26
+ export const RENT_PER_BYTE = 5_080n;
27
+ export const ACCOUNT_OVERHEAD = 128n;
28
+ /**
29
+ * The most rent a byte has cost on any Solana cluster: 3,480 lamports a byte-year for two years, the rate before
30
+ * devnet and mainnet lowered it (a local validator and LiteSVM still charge it). An RPC that asks more is not believed.
31
+ */
32
+ export const MOST_RENT_PER_BYTE = 6_960n;
33
+ export const rent = (bytes) => (BigInt(bytes) + ACCOUNT_OVERHEAD) * RENT_PER_BYTE;
34
+ /**
35
+ * A collectible's Core account, paid from the SOL its order holds (or by whoever gives it): its rent (a Core asset of
36
+ * about 190 bytes) plus Metaplex Core's creation fee of 0.0015 SOL, which the account keeps. The full simulation
37
+ * measures 0.00309 to 0.00313 SOL across its fixtures.
38
+ */
39
+ export const COLLECTIBLE_RENT = 3_100_000n;
40
+ /** The network's base fee per signature. Priority fees are extra and up to the wallet. */
41
+ export const SIGNATURE_FEE = 5_000n;
42
+ export const LAMPORTS_PER_SOL = 1_000_000_000n;
43
+ 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']);
48
+ export const RARITIES = Object.freeze(['Common', 'Uncommon', 'Rare', 'Epic', 'Legendary', 'Mythic']);
49
+
50
+ // ---------------------------------------------------------------- units
51
+
52
+ /** A refusal raised while reading a value; the checker turns it into a refusal with a code. */
53
+ export class SpecError extends Error {
54
+ constructor(code, message, fix) {
55
+ super(message);
56
+ this.code = code;
57
+ this.fix = fix;
58
+ }
59
+ }
60
+
61
+ /** A plain decimal string for an amount written as a number or text (no exponent). */
62
+ export function decimalText(amount) {
63
+ 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".');
65
+ const text = String(amount);
66
+ if (!/e/i.test(text)) return text;
67
+ return amount.toFixed(20).replace(/\.?0+$/, '');
68
+ }
69
+
70
+ /** "0.1" of a currency with 9 decimals → 100000000n. Refuses more decimals than the currency has. */
71
+ export function toUnits(amount, decimals) {
72
+ 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".');
74
+ const [whole, fraction = ''] = text.split('.');
75
+ 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.`);
77
+ 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.');
79
+ return units;
80
+ }
81
+
82
+ /** 100000000n with 9 decimals → "0.1". */
83
+ export function fromUnits(units, decimals) {
84
+ const negative = units < 0n;
85
+ const digits = (negative ? -units : units).toString().padStart(decimals + 1, '0');
86
+ const whole = digits.slice(0, digits.length - decimals);
87
+ const fraction = digits.slice(digits.length - decimals).replace(/0+$/, '');
88
+ return `${negative ? '-' : ''}${whole}${fraction ? `.${fraction}` : ''}`;
89
+ }
90
+
91
+ /** 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
+ export function wholeCount(value) {
93
+ const text = typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? String(value) : value ?? '0';
94
+ return typeof text === 'string' && /^\d{1,19}$/.test(text) && BigInt(text) < 2n ** 64n ? BigInt(text) : null;
95
+ }
96
+
97
+ /** 2.5 (percent) → 250 basis points. Null when it is not a whole number of hundredths. */
98
+ export function toBps(percent) {
99
+ const bps = Math.round(Number(percent ?? 0) * 100);
100
+ return Math.abs(bps - Number(percent ?? 0) * 100) < 1e-6 && bps >= 0 && bps <= 10_000 ? bps : null;
101
+ }
102
+
103
+ const UNIT = { s: 1, m: 60, h: 3600, d: 86_400, w: 604_800 };
104
+ /** "7d" → 604800. A number is seconds already. */
105
+ export function toSeconds(duration) {
106
+ if (duration === undefined || duration === null) return 0;
107
+ if (typeof duration === 'number') return duration;
108
+ 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".');
110
+ return Math.round(Number(m[1]) * UNIT[m[2]]);
111
+ }
112
+
113
+ /**
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.
116
+ */
117
+ export const releaseSeconds = (f) => Math.max(86_400, ...(f.tiers ?? []).map((t) => toSeconds(t.lock ?? 0)));
118
+
119
+ /** A farm's release span in words: "a day", "30 days", "36 hours". */
120
+ export function releaseWords(f) {
121
+ const s = releaseSeconds(f);
122
+ if (s % 86_400 === 0) return s === 86_400 ? 'a day' : `${s / 86_400} days`;
123
+ return s % 3_600 === 0 ? `${s / 3_600} hours` : `${s} seconds`;
124
+ }
125
+
126
+ /**
127
+ * 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.
130
+ */
131
+ export function farmEndWords(f) {
132
+ if (!f.runs || f.rate) return null;
133
+ 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` : ''}`;
136
+ }
137
+
138
+ /** A raise's or a shift's `by`: a number, or for a time a duration, which may be negative ("-1d"). */
139
+ export function toBy(by) {
140
+ if (typeof by !== 'string') return by;
141
+ return by.startsWith('-') ? -toSeconds(by.slice(1)) : toSeconds(by);
142
+ }
143
+
144
+ /** "publish", "publish+7d" or a date → Unix seconds, counted from `now` (Unix seconds) for the first two. */
145
+ export function toTime(time, now) {
146
+ if (!time || time === 'publish') return now;
147
+ const m = /^publish\+(.+)$/.exec(time);
148
+ if (m) return now + toSeconds(m[1]);
149
+ 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".');
151
+ return Math.floor(at / 1000);
152
+ }
153
+
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);
157
+
158
+ // ---------------------------------------------------------------- the model
159
+
160
+ /** Gives every part without one the next free program id (pools from 0, the rest from 1). */
161
+ function assignIds(list, field, first) {
162
+ const used = new Set(list.map((x) => x[field]).filter((v) => Number.isInteger(v)));
163
+ let next = first;
164
+ return list.map((x) => {
165
+ if (Number.isInteger(x[field])) return x;
166
+ while (used.has(next)) next += 1;
167
+ used.add(next);
168
+ return { ...x, [field]: next };
169
+ });
170
+ }
171
+
172
+ /** Where an NFT paid goes, in the program's words (`Spend`, `Then`): "burn", "treasury" (its wallet, filled in at go-live), { giveTo }, or { stash }. */
173
+ export const spendOf = (then, m) => {
174
+ if (!then || then === 'burn') return 'Burn';
175
+ if (then === 'treasury') return { Give: ['@treasury'] };
176
+ if (then.stash) return { Stash: [m.stash.get(then.stash)?.id ?? 0] };
177
+ return { Give: [then.giveTo] };
178
+ };
179
+
180
+ /** What a token is on chain: its mint (or "@mint:<key>" until it exists), program, decimals, and who can mint it. */
181
+ function tokenOf(given) {
182
+ // "project" is the spec's word for a token the project mints; "game" is the older spelling of it.
183
+ 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 };
185
+ const decimals = t.decimals ?? (t.source === 'game' ? 0 : undefined);
186
+ return {
187
+ ...t,
188
+ symbol: t.symbol ?? t.key.toUpperCase().slice(0, 10),
189
+ name: t.name ?? t.symbol ?? t.key,
190
+ decimals,
191
+ program: t.program ?? 'spl-token',
192
+ mint: t.mint ?? `@mint:${t.key}`,
193
+ wsol: t.mint === WSOL,
194
+ // A game token with a fixed supply is minted once, to the owner, and nobody mints it after.
195
+ gameMints: (t.source === 'game' && t.supply === undefined) || ['game', 'project'].includes(t.powers?.mintAuthority),
196
+ fixedSupply: t.source === 'game' && t.supply !== undefined,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * The spec with references resolved and ids given. Unknown references stay as they are (the checker reports
202
+ * them); call `check` first when it matters.
203
+ */
204
+ export function model(economy, { now = 0 } = {}) {
205
+ const e = economy ?? {};
206
+ const tokens = new Map((e.tokens ?? []).map((t) => [t.key, tokenOf(t)]));
207
+ const currencyKey = e.currency ?? (tokens.size === 1 ? [...tokens.keys()][0] : undefined);
208
+ 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);
222
+ 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 }));
224
+ // A raffle: its NFT is first place's prize, held in the lottery's own stash.
225
+ const lotteries = (e.lotteries ?? []).map((l) => {
226
+ if (!l.raffle) return l;
227
+ 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 ?? [])] };
229
+ });
230
+ const m = {
231
+ now,
232
+ name: e.name ?? 'Project',
233
+ authority: e.keys?.authority ?? 'owner',
234
+ /** The wallet whose account for the currency is the treasury, when it isn't the owner's ("wallet:<address>"). */
235
+ treasuryWallet: typeof e.treasury === 'string' && e.treasury.startsWith('wallet:') ? e.treasury.slice(7) : null,
236
+ tokens,
237
+ currencyKey,
238
+ currency,
239
+ /** 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 },
241
+ /** Core collections the owner already has that the project also takes (its update authority is the owner). */
242
+ collections: e.collections ?? [],
243
+ /** How many compressed NFTs the project's tree holds (`compressedTree`), when a prize gives one. */
244
+ treeHolds: e.compressedTree?.holds ?? 16_384,
245
+ /** The project's number among its owner's: its address is ["project", owner, number]. */
246
+ projectNumber: e.projectNumber ?? 0,
247
+ items: assignIds(e.items ?? [], 'id', 1),
248
+ pools,
249
+ stashes: assignIds(e.stashes ?? [], 'id', 1),
250
+ boxes: [],
251
+ payments: e.payments ?? [],
252
+ recipes: assignIds(e.recipes ?? [], 'id', 1),
253
+ schedules: assignIds(e.schedules ?? [], 'id', 1),
254
+ lotteries: assignIds(lotteries, 'id', 1),
255
+ farms: assignIds(farms, 'id', 1),
256
+ contests: assignIds(e.contests ?? [], 'id', 1),
257
+ auctions: assignIds(e.auctions ?? [], 'id', 1),
258
+ holds: e.holds ?? [],
259
+ grants: assignIds(e.grants ?? [], 'id', 1),
260
+ traits: e.traits ?? [],
261
+ decisions: e.decisions ?? [],
262
+ scenario: e.scenario ?? {},
263
+ live: e.live ?? {},
264
+ };
265
+ // A shop item is a box of one entry (the program sells items only through boxes). Its box takes the item's price,
266
+ // rise, fall, split and gate; it is numbered with the boxes, after the ones the spec numbered itself.
267
+ const shop = m.items
268
+ .filter((it) => Number(it.price ?? 0) > 0 || it.free)
269
+ .map((it) => ({
270
+ key: `shop-${it.key}`,
271
+ ...(Number.isInteger(it.shopBox) ? { id: it.shopBox } : {}),
272
+ name: it.name,
273
+ price: it.price ?? 0,
274
+ ...(it.rise ? { rise: it.rise } : {}),
275
+ ...(it.fall ? { fall: it.fall } : {}),
276
+ ...(it.split ? { split: it.split } : {}),
277
+ ...(it.gate ? { gate: it.gate } : {}),
278
+ // 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]])),
280
+ active: it.active !== false,
281
+ outcomes: [{ gives: 'item', item: it.key, weight: 1 }],
282
+ shopOf: it.key,
283
+ }));
284
+ const numbered = assignIds([...(e.boxes ?? []), ...shop], 'id', 1);
285
+ m.boxes = numbered.filter((b) => !b.shopOf);
286
+ m.shop = numbered.filter((b) => b.shopOf);
287
+ /** Every box the program holds: the spec's own and the shop's. */
288
+ m.allBoxes = numbered;
289
+ const index = (list) => new Map(list.map((x) => [x.key, x]));
290
+ m.item = index(m.items);
291
+ m.pool = index(m.pools);
292
+ m.stash = index(m.stashes);
293
+ m.box = index(m.allBoxes);
294
+ m.recipe = index(m.recipes);
295
+ m.farm = index(m.farms);
296
+ m.grant = index(m.grants);
297
+ m.lottery = index(m.lotteries);
298
+ m.contest = index(m.contests);
299
+ m.auction = index(m.auctions);
300
+ m.hold = index(m.holds);
301
+ /** The token a pool holds. */
302
+ m.poolToken = (key) => tokens.get(m.pool.get(key)?.currency);
303
+ /** The token a part is priced in (its own `currency`, else the game's). */
304
+ m.priceToken = (part) => tokens.get(part?.currency ?? currencyKey);
305
+ return m;
306
+ }
307
+
308
+ /**
309
+ * The NFTs a stash lists, each as the program's `piece` takes it: a Core asset (`asset`, its `collection`: its own, the
310
+ * stash's, or the game's when neither says), a Token Metadata NFT (`mint`, `programmable`, `ruleSet`), or a compressed
311
+ * NFT (`compressed`, its asset id).
312
+ */
313
+ 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 };
319
+ });
320
+ }
321
+
322
+ /**
323
+ * 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`),
325
+ * "nft" (`nft`, an address the stash `stash` lists) or "roll" (a roll of `box`; in a recipe, on the collectible kept
326
+ * at `on`). A grant's older `gives` ({ item }, { pool, amount }, { roll }) reads as its line. Null for none.
327
+ */
328
+ export function prizeLine(p) {
329
+ if (!p || typeof p !== 'object') return null;
330
+ if (p.gives) return p;
331
+ if (p.item) return { gives: 'item', item: p.item };
332
+ if (p.roll) return { gives: 'roll', box: p.roll };
333
+ if (p.pool) return { gives: 'tokens', amount: p.amount, from: `pool:${p.pool}` };
334
+ return null;
335
+ }
336
+
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);
339
+
340
+ /** 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);
342
+
343
+ /**
344
+ * Every prize the spec gives (prize.rs): a grant's, a lottery place's, a contest's winners', a mission tier's and a
345
+ * recipe's `out` lines. [{ prize (a line), where, by, route, strict, once }] `strict`: the route refuses a prize it
346
+ * can't give (a grant's claim, a craft); any other goes on without it. `once`: the route gives it once at most (a
347
+ * grant of one claim in all).
348
+ */
349
+ export function prizes(m) {
350
+ 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);
357
+ return list;
358
+ }
359
+
360
+ /**
361
+ * The project's tree of compressed NFTs (`open_tree`), when a prize gives one (`"compressed": true`), else null: deep
362
+ * enough for `treeHolds` (14 to 20 levels, Bubblegum's sizes for a buffer of 64), its canopy keeping a proof at 10
363
+ * nodes or a few more (at most 8 levels: past that the rent grows faster than the proofs shrink). { depth, buffer,
364
+ * canopy, bytes }
365
+ */
366
+ export function compressedTree(m) {
367
+ if (!prizes(m).some((p) => p.prize.gives === 'item' && p.prize.compressed)) return null;
368
+ const depth = Math.min(20, Math.max(14, Math.ceil(Math.log2(Math.max(1, m.treeHolds)))));
369
+ const canopy = Math.min(depth - 10, 8);
370
+ return { depth, buffer: 64, canopy, bytes: treeSize(depth, 64, canopy) };
371
+ }
372
+
373
+ /**
374
+ * Every route that gives a roll of a box, with the box it names: a prize's roll (`prizes`), an auction's lot, a
375
+ * mission's roll on the NFT leaving it and a recipe's roll on a collectible it keeps. [{ box, by, route }]
376
+ */
377
+ 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);
383
+ }
384
+
385
+ /**
386
+ * Every roll of a box any route makes, for the checks keyed on "a box something rolls": `prizeRolls`, every box
387
+ * outcome's (or bundle part's) roll of another box (route "box", made at its settle), and, followed through chains,
388
+ * each route that rolls a box whose outcome rolls another (a grant's roll of “mid”, whose outcome rolls “inner”, is a
389
+ * roll of “inner” by the grant, `through` ["mid"]). [{ box, by, route, through? }]
390
+ */
391
+ export function rollsOf(m) {
392
+ 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) : [])];
396
+ // A breadth-first walk: each roll of a box is also a roll of every box it rolls, once for each route.
397
+ for (let i = 0; i < rolls.length; i++) {
398
+ const r = rolls[i];
399
+ for (const next of rollsFrom(r.box)) {
400
+ const id = `${next}|${r.by}|${r.route}`;
401
+ if (seen.has(id)) continue;
402
+ seen.add(id);
403
+ rolls.push({ box: next, by: r.by, route: r.route, through: [...(r.through ?? []), r.box] });
404
+ }
405
+ }
406
+ return rolls;
407
+ }
408
+
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”". */
410
+ export function prizeWords(p, m) {
411
+ const line = prizeLine(p);
412
+ 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}”`;
418
+ if (line.gives === 'roll') return `a roll of “${m.box.get(line.box)?.name ?? line.box}”`;
419
+ const pool = prizePool(line);
420
+ 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}`;
422
+ }
423
+
424
+ /**
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"
427
+ * (tokens minted), "nft" (one NFT a stash lists) or "roll" (a roll of another box). A bundle gives its `prizes`; "burn"
428
+ * and "nothing" give none.
429
+ */
430
+ export function outcomeParts(o) {
431
+ if (!o) return [];
432
+ 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 }];
435
+ return [o];
436
+ }
437
+
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
+ /** 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');
443
+
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;
446
+
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 });
449
+
450
+ /** "pool:house" → { kind: 'pool', key: 'house' }; "burn" → { kind: 'burn' }. */
451
+ export function destination(to) {
452
+ if (to === 'burn') return { kind: 'burn' };
453
+ if (to === 'treasury') return { kind: 'treasury' };
454
+ if (to === 'chosen') return { kind: 'chosen', key: '' };
455
+ const [kind, ...rest] = String(to).split(':');
456
+ return { kind, key: rest.join(':') };
457
+ }
458
+
459
+ /** 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) }));
461
+
462
+ /**
463
+ * The program's `To` for a destination, with pool indexes resolved (the IDL's own shape). A wallet ("wallet:<address>")
464
+ * 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
+ * (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).
467
+ */
468
+ export function toEnum(to, m) {
469
+ const d = destination(to);
470
+ if (d.kind === 'burn') return 'Burn';
471
+ if (d.kind === 'treasury') return 'Treasury';
472
+ if (d.kind === 'pool') return { Pool: [m.pool.get(d.key)?.index ?? 0] };
473
+ if (d.kind === 'account') return { Account: [d.key] };
474
+ if (d.kind === 'wallet') return { Wallet: [d.key] };
475
+ if (d.kind === 'lottery') return { Account: [`@pot:${m.lottery.get(d.key)?.id ?? 0}`] };
476
+ if (d.kind === 'chosen') return { Chosen: { item: d.key ? (m.item.get(d.key)?.id ?? 0) : null } };
477
+ return { Holder: [d.key] };
478
+ }
479
+
480
+ /**
481
+ * The program's `Rule` for a spec rule: which NFTs (a collection's, or an item's the game mints), an attribute and its
482
+ * test. With neither, the collection the game has authority over: `@collection`, filled in when the economy goes live.
483
+ */
484
+ export function ruleOf(rule, m) {
485
+ if (!rule) return null;
486
+ 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'] };
489
+ return { of, key: rule.attribute ?? '', test };
490
+ }
491
+
492
+ /** 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]);
494
+
495
+ /**
496
+ * The program's `Gate` for a spec gate: a few wallets (`wallets`), a list of any size (`list`: an allowlist, its root and
497
+ * 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
498
+ * is "@farm:<id>", filled in when the project is known), or a rule a collectible the player shows must pass.
499
+ */
500
+ export const gateOf = (gate, m) => {
501
+ if (!gate) return null;
502
+ if (gate.wallets) return { Wallets: [gate.wallets] };
503
+ if (gate.staked) {
504
+ 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) } };
506
+ }
507
+ if (gate.list) {
508
+ const { root, depth } = gateList(gate.list);
509
+ return { List: { root, depth } };
510
+ }
511
+ return { Holds: [ruleOf(gate, m)] };
512
+ };
513
+
514
+ /** The program's `PerWallet` for a part's "perWallet" (in all) and "perWalletEach" ({ count, every }). */
515
+ export const perWalletOf = (part) => ({
516
+ total: part?.perWallet ?? 0,
517
+ per_window: part?.perWalletEach?.count ?? 0,
518
+ window: part?.perWalletEach ? toSeconds(part.perWalletEach.every ?? '1d') : 0,
519
+ });
520
+
521
+ /** 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) });
523
+
524
+ /** 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 }] : [])];
526
+
527
+ /** The program's `Effect` for a spec effect: a flat share, or a share per point of the attribute. */
528
+ export function effectOf(effect) {
529
+ if (effect?.perPointPercent !== undefined) return { PerPoint: { bps: Math.round(effect.perPointPercent * 100), most: effect.upTo ?? 4_294_967_295 } };
530
+ return { Flat: [Math.round((effect?.addPercent ?? 0) * 100)] };
531
+ }
532
+
533
+ /** A box's outcomes with their program weights: chances become whole weights (hundredths of a basis point). */
534
+ export function weights(box) {
535
+ 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));
538
+ const gcd = (a, b) => (b ? gcd(b, a % b) : a);
539
+ const g = raw.reduce((a, b) => gcd(a, b), 0) || 1;
540
+ return raw.map((w) => w / g);
541
+ }
542
+
543
+ /** Every place the spec names a pool as somewhere money leaves from, with what takes it. */
544
+ export function poolTakers(m) {
545
+ const takers = [];
546
+ 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})`);
549
+ for (const s of m.schedules) {
550
+ if (s.does === 'burn') add(s.pool, `schedule:${s.key}`, 'burns a share');
551
+ if (s.does === 'move') add(s.from, `schedule:${s.key}`, 'moves a share out');
552
+ // 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
553
+ // from a vault of its own and names no pool.
554
+ if (s.does === 'pay') add(s.from, `schedule:${s.key}`, 'pays out on a timer');
555
+ }
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');
563
+ return takers;
564
+ }
565
+
566
+ /** Every place the spec sends money into a pool. */
567
+ export function poolFillers(m) {
568
+ const fillers = [];
569
+ 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');
572
+ for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
573
+ 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');
575
+ for (const s of m.schedules) {
576
+ if (s.does === 'issue') add(s.pool, `schedule:${s.key}`, 'mints into it');
577
+ if (s.does === 'move') add(s.to, `schedule:${s.key}`, 'moves a share in');
578
+ }
579
+ for (const l of m.lotteries) fromSplit(lotterySplit(l), `lottery:${l.key}`);
580
+ for (const c of m.contests) fromSplit(c.split, `contest:${c.key}`);
581
+ for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
582
+ return fillers;
583
+ }