@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,185 @@
1
+ // What an economy costs: going live (each account's rent, the program's fees, network fees, the seeds), and each
2
+ // action while it runs, for whoever signs it. In SOL unless a seed is in another token. Pure.
3
+ //
4
+ // Rent is (bytes + 128) × 5,080 lamports (model.mjs); the loot program's own records are sized byte for byte from
5
+ // their Borsh layout, others (a Core collection, a farm) roughly, and marked so.
6
+ import { boxOrderBytes, plan as compile, contestBytes, farmEnds, roundRentOf } from './plan.mjs';
7
+ import { ACCOUNT_BYTES, CONTEST_ENTRY_BYTES, END_BYTES, entryWidth, LIMIT_RECORD_BYTES, ROUND_RECORD_BYTES } from '../client/index.js';
8
+ import { boxBuys, COLLECTIBLE_RENT, drawsOf, settleUnits, holdsRent, outcomeParts, SETTLE_RENT_UNIT, FEES, LAMPORTS_PER_SOL, ORACLE_FEE, RENT_PER_BYTE, SIGNATURE_FEE, model, lotterySplit, prizeLine, prizeRolls, prizes, prizeWords, rent, toUnits } from './model.mjs';
9
+ import { inAll, round, simulate } from './simulate.mjs';
10
+
11
+ // The options of a call made without any: read-only, and typed so the generated declarations take any option.
12
+ const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
13
+
14
+ const sol = (lamports) => round(Number(lamports) / Number(LAMPORTS_PER_SOL));
15
+ /** Setup steps that are no loot instruction, and so charge no fee: a token minted, the collection made, SOL put in a
16
+ * grant's funds. Every other step is the owner or the authority calling loot, which charges the project fee. */
17
+ const NO_FEE_STEP = /^(token:|collection$|fund:|rent:)/;
18
+ /** Setup cost from a plan (plan.mjs `plan`). */
19
+ export function setupCost(plan, economy) {
20
+ const m = model(economy);
21
+ // An account that grows (a live stash taking more NFTs) pays rent for its new bytes only.
22
+ const rentOf = (c) => (c.grows ? BigInt(c.bytes) * RENT_PER_BYTE : rent(c.bytes));
23
+ const accounts = plan.transactions.flatMap((t) => t.steps).flatMap((s) => s.creates ?? []).map((c) => ({ ...c, lamports: rentOf(c).toString(), sol: sol(rentOf(c)) }));
24
+ const rentLamports = accounts.reduce((n, a) => n + BigInt(a.lamports), 0n);
25
+ const charged = plan.transactions.flatMap((t) => t.steps).filter((s) => !NO_FEE_STEP.test(s.id));
26
+ const feeLamports = BigInt(charged.length) * FEES.project;
27
+ const networkLamports = plan.transactions.reduce((n, t) => n + BigInt(t.signers.length) * SIGNATURE_FEE, 0n);
28
+ // Only the seeds and grant funds this plan puts in: a change to a live economy doesn't pay its seeds again.
29
+ const steps = new Set(plan.transactions.flatMap((t) => t.steps).map((s) => s.id));
30
+ const seeds = {};
31
+ let seedLamports = 0n;
32
+ const funded = m.grants.filter((g) => Number(g.fund ?? 0) > 0 && steps.has(`fund:${g.key}`));
33
+ for (const g of funded) seedLamports += toUnits(g.fund, 9);
34
+ // What the owner puts in from their own wallet: a pool's seed, a lottery's seed (into its pot), a farm's seed (into its
35
+ // rewards vault), a sealed schedule's funds (into its vault, from the wallet).
36
+ const putIn = (key, amount, step) => {
37
+ if (!(Number(amount ?? 0) > 0) || !steps.has(step)) return;
38
+ const t = m.tokens.get(key ?? m.currencyKey);
39
+ seeds[t.symbol] = round((seeds[t.symbol] ?? 0) + Number(amount));
40
+ if (t.wsol) seedLamports += toUnits(amount, 9);
41
+ };
42
+ for (const p of m.pools) putIn(p.currency, p.seed, `seed:${p.key}`);
43
+ for (const l of m.lotteries) putIn(l.currency, l.seed, `seed:lottery:${l.key}`);
44
+ for (const f of m.farms) putIn(f.rewardsToken, f.seed, `seed:farm:${f.key}`);
45
+ // SOL a lottery holds for its rounds' rent: lent to each round, back at its close.
46
+ for (const l of m.lotteries) if (steps.has(`rent:lottery:${l.key}`)) seedLamports += roundRentOf(l, m);
47
+ for (const sc of m.schedules) if (sc.sealed && (sc.fundFrom ?? 'wallet') === 'wallet') putIn(sc.currency, sc.fund, `fund:${sc.key}`);
48
+ const total = rentLamports + feeLamports + networkLamports + seedLamports;
49
+ const notes = [];
50
+ if (accounts.some((a) => a.approx)) notes.push('Accounts marked “about” are sized roughly (a Core collection or an external token account).');
51
+ if (Object.entries(seeds).some(([symbol]) => [...m.tokens.values()].some((t) => t.wsol && t.symbol === symbol))) notes.push('The total includes the SOL you put in: pools’, lotteries’ and farms’ seeds, and sealed schedules’ funds.');
52
+ if (m.lotteries.some((l) => steps.has(`rent:lottery:${l.key}`))) notes.push('The total includes the SOL lotteries hold for their rounds’ rent and their tickets’ entries (`roundRent`, sized by default for the rounds and purchases expected at once): each round borrows it and gives it back when it closes; it comes back to you when the lottery closes.');
53
+ if (funded.length) notes.push('The total includes the SOL put in grants’ funds, which pays their claims; what is left goes back to you when a grant closes.');
54
+ notes.push('The network fee is 5,000 lamports a signature; a wallet may add a priority fee of its own.');
55
+ return {
56
+ accounts,
57
+ rentSol: sol(rentLamports),
58
+ programFeesSol: sol(feeLamports),
59
+ networkFeesSol: sol(networkLamports),
60
+ seeds,
61
+ totalSol: sol(total),
62
+ transactions: plan.transactions.length,
63
+ approvals: plan.approvals,
64
+ notes,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * What each action costs whoever signs it while the economy runs: games.fun's fee, the network's, and for anything
70
+ * that asks MagicBlock's oracle for a random number (a box's open, a lottery's draw, the authority's `roll`) the
71
+ * oracle's own fee (`oracleSol`, model.mjs ORACLE_FEE).
72
+ */
73
+ export function actionCosts(economy, { now } = NO_OPTIONS) {
74
+ const m = model(economy);
75
+ // A player's real cost of one action, from the simulation (simulate.mjs realCostOf): the number to quote.
76
+ const sim = simulate(economy, { now });
77
+ const real = new Map([...sim.items.map((x) => [`item:${x.key}`, x.realCost]), ...sim.boxes.map((x) => [`box:${x.key}`, x.realCost]), ...sim.payments.map((x) => [`payment:${x.key}`, x.realCost]), ...sim.lotteries.map((x) => [`lottery:${x.key}`, x.realCost])]);
78
+ const rows = [];
79
+ const row = (part, action, who, fee, deposits = [], extra, oracle = 0n) => {
80
+ const r = real.get(part);
81
+ rows.push({ part, action, who, feeSol: sol(fee), ...(oracle ? { oracleSol: sol(oracle) } : {}), networkSol: sol(SIGNATURE_FEE), deposits, ...(extra ? { note: extra } : {}), ...(r ? { inAllSol: r.totalSol ?? null, onTopSol: r.extrasSol, holdSol: r.holdSol, inAll: inAll(r) } : {}) });
82
+ };
83
+ const back = (what, lamports, when) => ({ what, sol: sol(lamports), back: when });
84
+ // An order holds its rent and the fee; the open lends its throwaway settle key what the settle may open or mint (a
85
+ // collectible's rent for each item the richest outcome gives, each card of a pack), back unspent.
86
+ const itemsOf = (b) => drawsOf(b) * Math.max(0, ...(b.outcomes ?? []).map((o) => outcomeParts(o).filter((p) => holdsRent(p, m)).length));
87
+ const order = (b) => [
88
+ back('the order', rent(boxOrderBytes(b, m, 1, { opened: true })), 'at settle or refund'),
89
+ ...(settleUnits(b, m) ? [back(`the SOL the open lends the throwaway key that settles it, for what its settle may open (${settleUnits(b, m)} × ${sol(SETTLE_RENT_UNIT)} SOL: each collectible, NFT of a stash, coin it may pay and NFT it rolls on)`, SETTLE_RENT_UNIT * BigInt(settleUnits(b, m)), `once the key has settled, less what it opened (a collectible’s rent is about ${sol(COLLECTIBLE_RENT)} SOL); all of it when the roll fails`)] : []),
90
+ ];
91
+ // A box that needs no roll is bought in one step (`buy`): no order, and a collectible's rent is paid at once.
92
+ const bought = (b) => (itemsOf(b) ? [back(`${itemsOf(b)} new collectible${itemsOf(b) > 1 ? 's' : ''}`, COLLECTIBLE_RENT * BigInt(itemsOf(b)), `never: it is the collectible’s own rent (about ${sol(COLLECTIBLE_RENT)} SOL each)`)] : []);
93
+ for (const b of m.shop) row(`item:${b.shopOf}`, `buy “${b.name}”`, 'player', FEES.player, bought(b), 'One transaction: the price is split and the item given at once.');
94
+ for (const b of m.boxes) {
95
+ if (boxBuys(b, m)) row(`box:${b.key}`, `buy “${b.name}”`, 'player', FEES.player, bought(b), 'One transaction: the price is split and what it gives handed over at once.');
96
+ else row(`box:${b.key}`, `open “${b.name}”`, 'player', FEES.player, order(b), 'The oracle’s fee is paid by whoever asks for the roll (the player, at the open) and a refund doesn’t return it.', ORACLE_FEE);
97
+ }
98
+ for (const p of m.payments) row(`payment:${p.key}`, `pay “${p.name}”`, 'player', FEES.player);
99
+ for (const r of m.recipes) {
100
+ const made = (r.out ?? []).filter((l) => l.gives === 'item' && !l.compressed).length;
101
+ row(`recipe:${r.key}`, `craft “${r.name}”`, 'player', FEES.player, made ? [back(`${made} new collectible${made > 1 ? 's' : ''}`, COLLECTIBLE_RENT * BigInt(made), `never: it is the collectible’s own rent (about ${sol(COLLECTIBLE_RENT)} SOL each)`)] : []);
102
+ }
103
+ for (const l of m.lotteries) {
104
+ // A purchase adds its entry's bytes to the round. In a round the lottery opened, the SOL it holds for its rounds
105
+ // (`roundRent`, on by default) pays them, back to it at the close: the buyer pays only the fee and the price. Without
106
+ // it, or once it runs short, the buyer pays them (to the treasury at the close), and a round's first buyer fronts its
107
+ // header. A share the buyer chooses makes each entry keep what arrived.
108
+ const chosen = lotterySplit(l).some((s) => String(s.to).startsWith('chosen'));
109
+ const entry = BigInt(entryWidth(chosen, Boolean(l.ticketNft))) * RENT_PER_BYTE;
110
+ const held = roundRentOf(l, m);
111
+ const funded = held > 0n;
112
+ row(`lottery:${l.key}`, `buy tickets for “${l.name}”`, 'player', FEES.player, funded ? [] : [back('the purchase’s entry in the round (its bytes)', entry, 'never: it goes to the treasury when the round closes')], funded ? `A round’s record, its token accounts and each purchase’s entry are paid from the SOL the lottery holds for them (${sol(held)} SOL), back to it when the round closes: a buyer pays no program rent. When that runs short, a buyer pays their entry’s bytes (about ${sol(entry)} SOL, to the treasury at the close) and a round’s first buyer fronts its header, back to them at the close.` : 'A round’s first buyer also fronts its record’s header and token accounts (about 0.003 SOL and up, more with many places), back to them when the round closes, once every part is pulled; `roundRent` (on by default) has the lottery pay them, and each entry too.');
113
+ }
114
+ for (const f of m.farms) {
115
+ // A seat that can stop earning writes its end in the farm's record: a new time costs whoever writes it, spare rent first.
116
+ const endRent = BigInt(END_BYTES) * RENT_PER_BYTE;
117
+ row(`farm:${f.key}`, `stake in “${f.name}”`, 'player', FEES.player, [back('the seat', rent(ACCOUNT_BYTES.Seat), 'when they take everything out'), ...(farmEnds(f) ? [back(`a new end time in the farm’s record of ends (${END_BYTES} bytes; none when another seat ends at that second, or spare rent in the record covers it)`, endRent, 'never: it stays with the record for the next time written')] : [])], farmEnds(f) ? 'Whatever writes a new end time (a stake, a booster put on, a refresh that reads new upkeep) pays its bytes, its sender’s.' : undefined);
118
+ row(`farm:${f.key}`, `unstake or harvest from “${f.name}”`, 'player', FEES.player, [], [
119
+ (f.tiers ?? []).some((t) => t.exitFee) && 'Leaving a tier before its lock leaves its exit fee of what is taken out, and keeps the rewards the seat was credited.',
120
+ ].filter(Boolean).join(' ') || undefined);
121
+ }
122
+ for (const h of m.holds) row(`hold:${h.key}`, `hand in for “${h.name}”`, 'player', FEES.player, [back('the hold', rent(ACCOUNT_BYTES.Hold), 'when it is released')]);
123
+ // What giving a prize opens, paid by whoever sends the instruction that gives it (prize.rs).
124
+ const opens = (p) => prizeLine(p)?.compressed ? ', and the compressed NFT, which has no rent' : ({ item: ', plus the collectible’s rent', tokens: ', plus the winner’s token account for the prize when they have none', share: ', plus the winner’s token account for the prize when they have none', nft: ', and the NFT is handed over in the claim (a Token Metadata NFT opens the winner’s token account of it)', roll: ', plus the roll’s order (below)' })[prizeLine(p)?.gives] ?? '';
125
+ for (const g of m.grants) row(`grant:${g.key}`, `claim “${g.name}”`, 'the winner, who signs every claim (anyone may pay its fees)', FEES.player, [], `Only the winning wallet can claim: it signs, whatever proves it won (a voucher, a list, an open grant). Whoever pays the transaction pays the player fee from its own SOL; its records (${LIMIT_RECORD_BYTES} bytes a wallet, ${ROUND_RECORD_BYTES} a round it is paid)${opens(g.prize ?? g.gives)} come from the grant’s funds (the sender pays what they can’t), and go back to the owner when its shards close after the grant: xsolana_simulate gives what to put in its funds per claim.`);
126
+ // A prize of a collectible given when a place is paid or a winning entry claimed: its rent, from whoever sends it.
127
+ for (const p of prizes(m).filter((p) => p.prize.gives === 'item' && !p.prize.compressed && !['grant', 'recipe'].includes(p.route)))
128
+ row(p.by, `give ${prizeWords(p.prize, m)} (${p.route})`, p.route === 'mission' ? 'the player leaving the mission' : p.route === 'contest' ? 'whoever claims the winning entry' : 'whoever pays the place', 0n, [back('the new collectible', COLLECTIBLE_RENT, 'never: it is the collectible’s own rent')]);
129
+ for (const c of m.contests) {
130
+ row(`contest:${c.key}`, `back a side in “${c.name}”`, 'player', FEES.player, [back('the entry’s bytes in the contest’s list', BigInt(c.stakeNft ? CONTEST_ENTRY_BYTES.nfts : CONTEST_ENTRY_BYTES.tokens) * RENT_PER_BYTE, 'never: they go to the treasury when the contest closes')], 'A loser of a contest of tokens sends nothing more; every other entry is paid by a claim anyone may send.');
131
+ row(`contest:${c.key}`, `open “${c.name}”`, 'authority', FEES.project, [back('the contest and its pot', rent(contestBytes(c, m)) + rent(165), 'when it closes')]);
132
+ row(`contest:${c.key}`, `decide “${c.name}”`, 'authority', FEES.project);
133
+ }
134
+ for (const a of m.auctions) {
135
+ row(`auction:${a.key}`, `bid on “${a.name}”`, 'player', FEES.player, [], 'Nothing to deposit: the bid waits in the game’s vault, and goes back at once when it is topped.');
136
+ const minted = a.lot?.item && (m.item.get(a.lot.item)?.kind ?? 'collectible') === 'collectible';
137
+ row(`auction:${a.key}`, `end “${a.name}”`, 'anyone (the winner, a keeper)', 0n, minted ? [back('the new collectible', COLLECTIBLE_RENT, 'never: it is the collectible’s own rent, paid by whoever ends it')] : [], 'Its own rent goes back to whoever opened it.');
138
+ }
139
+ for (const s of m.stashes) row(`stash:${s.key}`, `put an NFT in “${s.name}”, or take one out`, 'the owner or the authority', FEES.project, [back('32 bytes more of the stash for each NFT in it', 32n * RENT_PER_BYTE, 'to whoever takes one out; a roll leaves it with the stash, for the next one put in')]);
140
+ // A prize that is a roll of a box: whoever sends the claim, the payment, the end (or a grant's funds) makes its order.
141
+ for (const r of prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route))) {
142
+ const name = m.box.get(r.box)?.name ?? r.box;
143
+ const who = r.route === 'grant' ? 'the grant’s funds (the sender, what they can’t)' : r.route === 'auction' ? 'whoever ends it' : r.route === 'contest' ? 'whoever claims the winning entry' : 'whoever pays the place';
144
+ // Whoever settles it pays a collectible's rent when its box gives one (an item outcome).
145
+ const mints = (m.box.get(r.box)?.outcomes ?? []).flatMap(outcomeParts).some((o) => holdsRent(o, m));
146
+ row(r.by, `make a winner’s roll of “${name}” (${r.route})`, who, 0n, [back('the roll’s order', rent(boxOrderBytes(m.box.get(r.box), m)), 'to the winner when it is settled')], `Then the winner or the project’s keys ask the oracle for the roll (its fee, below) and settle it${mints ? `, paying the collectible’s rent when it gives one (about ${sol(COLLECTIBLE_RENT)} SOL)` : ''}.`);
147
+ row(r.by, `ask for the roll of “${name}”`, 'the winner or the project’s keys', 0n, [], 'It asks MagicBlock’s oracle, which takes its fee from whoever asks; a refund doesn’t return it.', ORACLE_FEE);
148
+ }
149
+ for (const d of m.decisions) row(`decision:${d.key}`, d.when, 'authority', FEES.project, [], d.does === 'roll' ? 'The roll asks MagicBlock’s oracle, which takes its own fee from the authority.' : undefined, d.does === 'roll' ? ORACLE_FEE : 0n);
150
+ if (m.allBoxes.some((b) => !boxBuys(b, m))) row('orders', 'settle an order', 'the player, a key they named, or the project’s keys', 0n, [], 'The sender pays what the settle opens or mints (a collectible’s rent, a token account the player lacks); the order’s rent goes back to the player. js/play’s throwaway key sends it with what the open lent it.');
151
+ if (m.schedules.length) row('schedules', 'run a schedule when it is due', 'the project’s keys, or who it pays (a sealed one’s recipient, from their page)', 0n, [], `A run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse.${m.schedules.some((sc) => sc.sealed) ? ' A sealed schedule pays from its own vault, and its fixed amount pays every run due, in one transaction.' : ''}`);
152
+ if (m.lotteries.length) {
153
+ row('lotteries', 'ask for a draw', 'anyone (a keeper, or a player)', 0n, [], 'The draw asks MagicBlock’s oracle for the winning numbers, and fixes the round’s money first (the next round sells at once); the oracle takes its fee from whoever sends the draw. An oracle that never answers within about a week: the next ask cancels the round, and every ticket is refunded.', ORACLE_FEE);
154
+ row('lotteries', 'pull a part: a refund, a place’s part of the pot or its prize, a share owed in common', 'anyone (the winner’s page, a keeper)', 0n, [], 'Each part is pulled by itself, in any order, and the pull that leaves nothing owed closes the round; whoever pulls opens the payee’s token account when it is missing.');
155
+ if (m.lotteries.some((l) => l.minSold)) row('lotteries', 'a short round', 'anyone', 0n, [], 'A round that sold fewer tickets than its least is cancelled at its draw, with no oracle fee, and every ticket refunded.');
156
+ }
157
+ return rows;
158
+ }
159
+
160
+ /** Setup and play costs for a spec, or its refusals. */
161
+ /** `only` and `live`: a change to a live economy, priced as only what it publishes (live.mjs `changedParts`). */
162
+ export function cost(economy, { now = Math.floor(Date.now() / 1000), only = null, live = null } = {}) {
163
+ const plan = compile(economy, { now, only, live });
164
+ if (!plan.ok) return plan;
165
+ return { ok: true, setup: setupCost(plan, economy), actions: actionCosts(economy, { now }), fees: { playerSol: sol(FEES.player), projectSol: sol(FEES.project), oracleSol: sol(ORACLE_FEE) } };
166
+ }
167
+
168
+ /** The cost in plain words. */
169
+ export function explainCost(c) {
170
+ const s = c.setup;
171
+ const lines = [`Going live takes ${s.transactions} transaction${s.transactions === 1 ? '' : 's'} (${s.approvals} wallet approval${s.approvals === 1 ? '' : 's'}) and about ${s.totalSol} SOL:`];
172
+ lines.push(` rent ${s.rentSol} SOL for ${s.accounts.length} accounts; program fees ${s.programFeesSol} SOL; network fees ${s.networkFeesSol} SOL${Object.keys(s.seeds).length ? `; seeds ${Object.entries(s.seeds).map(([k, v]) => `${v} ${k}`).join(', ')}` : ''}.`);
173
+ for (const a of s.accounts) lines.push(` ${a.approx ? 'about ' : ''}${a.sol} SOL: ${a.what} (${a.bytes} bytes)`);
174
+ lines.push('While it runs (for a player, quote the “in all” number: it is what leaves their wallet for good):');
175
+ for (const r of c.actions) {
176
+ const deposits = r.deposits.map((d) => `${d.sol} SOL for ${d.what} (back: ${d.back})`).join('; ');
177
+ if (r.inAll) {
178
+ lines.push(` ${r.action} (${r.who}): ${r.inAll}${r.note ? `. ${r.note}` : ''}`);
179
+ continue;
180
+ }
181
+ lines.push(` ${r.action} (${r.who}): ${r.feeSol ? `${r.feeSol} SOL fee + ` : ''}${r.oracleSol ? `${r.oracleSol} SOL to the oracle + ` : ''}${r.networkSol} SOL network${deposits ? `; ${deposits}` : ''}${r.note ? `. ${r.note}` : ''}`);
182
+ }
183
+ for (const n of s.notes) lines.push(n);
184
+ return lines.join('\n');
185
+ }
@@ -0,0 +1,114 @@
1
+ // The worked examples (whole specs in examples/ that check, simulate and go live in the tests), the ideas (real
2
+ // economies rated against the current loot, in examples/ideas.json) and what loot can't do exactly (with its nearest
3
+ // versions), searchable by a person's own words. Node only (reads the files).
4
+ import { readdirSync, readFileSync } from 'node:fs';
5
+ import { CANNOT, HOW } from './guide.mjs';
6
+
7
+ // The options of a call made without any: read-only, and typed so the generated declarations take any option.
8
+ const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
9
+
10
+ const DIR = new URL('../../examples/', import.meta.url);
11
+ const IDEAS = JSON.parse(readFileSync(new URL('ideas.json', DIR), 'utf8')).ideas;
12
+
13
+ /** Every worked example: { name, spec }. */
14
+ export function examples() {
15
+ return readdirSync(DIR)
16
+ .filter((f) => f.endsWith('.json') && f !== 'ideas.json')
17
+ .sort()
18
+ .map((f) => ({ name: f.replace(/\.json$/, ''), spec: JSON.parse(readFileSync(new URL(f, DIR), 'utf8')) }));
19
+ }
20
+
21
+ /** One example's whole spec, by name. */
22
+ export function example(name) {
23
+ const n = String(name).replace(/\.json$/, '');
24
+ const found = examples().find((e) => e.name === n);
25
+ if (!found) throw new Error(`No example called “${name}”. xsolana_examples lists them.`);
26
+ return found;
27
+ }
28
+
29
+ export const ideas = () => IDEAS;
30
+
31
+ const SYNONYMS = {
32
+ gacha: ['box', 'boxes', 'pull'], loot: ['box', 'boxes'], mystery: ['box', 'boxes'], pack: ['box', 'boxes'], packs: ['box', 'boxes'], roll: ['box', 'boxes'], chest: ['box', 'boxes'],
33
+ staking: ['farm', 'farms', 'stake'], stake: ['farm', 'farms'], yield: ['farm', 'farms'], earn: ['farm', 'farms'], boost: ['farm', 'farms', 'odds'],
34
+ raffle: ['lottery', 'lotteries'], lottery: ['lottery', 'lotteries'], draw: ['lottery', 'lotteries'], jackpot: ['lottery', 'lotteries', 'pot'],
35
+ craft: ['recipe', 'recipes'], crafting: ['recipe', 'recipes'], merge: ['recipe', 'recipes'], upgrade: ['recipe', 'recipes'], breed: ['recipe', 'recipes', 'life'], breeding: ['recipe', 'recipes', 'life'],
36
+ season: ['schedule', 'schedules'], seasons: ['schedule', 'schedules'], halving: ['schedule', 'schedules'], emission: ['schedule', 'schedules'], emissions: ['schedule', 'schedules'], vesting: ['schedule', 'schedules'],
37
+ bet: ['contest', 'contests'], betting: ['contest', 'contests'], duel: ['contest', 'contests'], wager: ['contest', 'contests'], prediction: ['contest', 'contests'], fantasy: ['contest', 'contests'],
38
+ shop: ['item', 'items'], store: ['item', 'items'], nft: ['item', 'items'], nfts: ['item', 'items'], collectible: ['item', 'items'], burn: ['burn'], deflation: ['burn'], deflationary: ['burn'],
39
+ airdrop: ['grant', 'grants'], reward: ['grant', 'grants'], rewards: ['grant', 'grants'], quest: ['grant', 'grants'], quests: ['grant', 'grants'], giveaway: ['grant', 'grants'], allowlist: ['grant', 'grants'], whitelist: ['grant', 'grants'],
40
+ gated: ['gate'], gate: ['gate'], 'no-loss': ['no-loss', 'lottery'], lossless: ['no-loss', 'lottery'], launchpad: ['staked', 'gate'], tiers: ['staked'], rank: ['grant', 'grants'], holders: ['gate', 'holder'], pfp: ['gate', 'collection'], 'bring-your-own': ['gate', 'collection'],
41
+ auction: ['auction', 'auctions', 'bid'], auctions: ['auction', 'auctions', 'bid'], bid: ['auction', 'auctions'], bids: ['auction', 'auctions'], bidding: ['auction', 'auctions'], highest: ['auction', 'auctions'], outbid: ['auction', 'auctions'], english: ['auction', 'auctions'],
42
+ dutch: ['fall', 'falling', 'drop'], descending: ['fall', 'falling'],
43
+ referral: ['chosen', 'referred', 'referrer'], referrals: ['chosen', 'referred', 'referrer'], referrer: ['chosen', 'referred'], affiliate: ['chosen', 'referred'], affiliates: ['chosen', 'referred'], refer: ['chosen', 'referred'],
44
+ rent: ['holder', 'chosen', 'rent'], landlord: ['holder', 'chosen', 'rent'], royalty: ['holder', 'share'], plot: ['chosen', 'holder', 'plot'],
45
+ crowdfund: ['contest', 'goal', 'minpot'], crowdfunding: ['contest', 'goal', 'minpot'], pledge: ['contest', 'goal', 'minpot'], kickstarter: ['contest', 'goal', 'minpot'],
46
+ leaderboard: ['decision', 'decisions'], tournament: ['contest', 'decision', 'decisions'], prize: ['pool', 'pools'], flip: ['coin', 'flip', 'box', 'boxes'],
47
+ };
48
+ const words = (s) => String(s ?? '').toLowerCase().match(/[a-z0-9][a-z0-9.+-]*/g) ?? [];
49
+ const STOP = new Set(['a', 'an', 'the', 'and', 'or', 'of', 'to', 'in', 'for', 'with', 'my', 'our', 'that', 'is', 'it', 'on', 'every', 'each', 'make', 'me', 'game', 'i', 'want', 'like', 'build']);
50
+
51
+ /** The parts a spec uses, and features it shows. */
52
+ export function partsUsed(spec) {
53
+ const out = ['items', 'pools', 'boxes', 'payments', 'recipes', 'schedules', 'lotteries', 'farms', 'contests', 'auctions', 'holds', 'grants', 'decisions'].filter((k) => Array.isArray(spec?.[k]) && spec[k].length);
54
+ const all = JSON.stringify(spec ?? {}).replace(/\s/g, '');
55
+ for (const [f, re] of [['rise', /"rise"/], ['fall', /"fall"/], ['life', /"life"/], ['holder', /holder:/], ['burn', /"burn"/], ['gate', /"gate"/], ['odds', /"odds"/], ['token-2022', /token-2022/], ['staked', /"staked":\{/], ['no-loss', /"noLoss":true/], ['chosen', /"to":"chosen/], ['minpot', /"minPot":/], ['shared-clock', /"clock":"shared"/]]) if (re.test(all)) out.push(f);
56
+ return out;
57
+ }
58
+
59
+ function score(text, query, parts = []) {
60
+ const have = new Set(words(text));
61
+ const uses = new Set(parts);
62
+ let s = 0;
63
+ for (const w of words(query).filter((x) => !STOP.has(x))) {
64
+ if (have.has(w)) s += 2;
65
+ else if ([...have].some((h) => h.length > 3 && w.length > 3 && (h.startsWith(w) || w.startsWith(h)))) s += 1;
66
+ for (const syn of SYNONYMS[w] ?? []) s += (have.has(syn) ? 1 : 0) + (uses.has(syn) ? 2 : 0);
67
+ }
68
+ return s;
69
+ }
70
+
71
+ const FIT_BONUS = { great: 0.5, some: 0.25, 'a-lot': 0, cannot: 0 };
72
+
73
+ /** Each entry of `list` scored against `query` by its words, the closest first, keeping those that match well enough. */
74
+ function closest(list, query, limit) {
75
+ const scored = list.map((e) => ({ e, s: score(e.words.replace(/-/g, ' '), query) })).sort((a, b) => b.s - a.s);
76
+ const best = scored[0]?.s ?? 0;
77
+ return scored.filter((x) => x.s >= 2 && x.s * 2 >= best).slice(0, limit);
78
+ }
79
+
80
+ /**
81
+ * What loot can't do exactly that `query` asks for, with the nearest versions: the closest few. `atLeast`: only limits
82
+ * that match at least this well (search passes the best matching capability's score: when a capability matches the
83
+ * words better, its "how" is the answer and a weaker "can't" beside it only misleads; a tie shows both).
84
+ */
85
+ export function limits(query, { limit = 3, atLeast = 0 } = {}) {
86
+ return closest(CANNOT, query, limit)
87
+ .filter((x) => x.s >= atLeast)
88
+ .map(({ e }) => ({ what: e.what, why: e.why, nearest: e.nearest }));
89
+ }
90
+
91
+ /** Asks that look impossible but are one field or a short combination, closest to `query`. */
92
+ export function hows(query, { limit = 3 } = {}) {
93
+ return closest(HOW, query, limit).map(({ e }) => ({ what: e.what, how: e.how }));
94
+ }
95
+
96
+ /** The examples, ideas, hows and limits closest to `query`. */
97
+ export function search(query, { limit = 6, fit } = NO_OPTIONS) {
98
+ const ex = examples().map(({ name, spec }) => ({ name, title: spec.name, prompt: spec.prompt, parts: partsUsed(spec) }));
99
+ const exampleHits = ex
100
+ .map((e) => ({ e, s: score(`${e.name} ${e.title} ${e.prompt}`, query, e.parts) }))
101
+ .filter((x) => x.s > 0)
102
+ .sort((a, b) => b.s - a.s)
103
+ .slice(0, limit)
104
+ .map((x) => x.e);
105
+ const ideaHits = IDEAS.filter((i) => !fit || i.fit === fit)
106
+ .map((i) => ({ i, s: score(`${i.name} ${i.section} ${i.how}`, query) + FIT_BONUS[i.fit] }))
107
+ .filter((x) => x.s > 0.5)
108
+ .sort((a, b) => b.s - a.s)
109
+ .slice(0, limit)
110
+ .map(({ i }) => i);
111
+ // A "can't" shows only when it matches at least as well as the best matching capability.
112
+ const how = closest(HOW, query, 3);
113
+ return { examples: exampleHits, ideas: ideaHits, hows: how.map(({ e }) => ({ what: e.what, how: e.how })), limits: limits(query, { atLeast: how[0]?.s ?? 0 }) };
114
+ }
@@ -0,0 +1,239 @@
1
+ // The economy in plain words, from the checker, the simulation, the cost and the play-time steps: what players do
2
+ // and what it costs them, how the pools fill and drain, what runs by itself, what the authority must sign, what
3
+ // going live costs, and what the owner must know. Deterministic. Pure.
4
+ import { check, describe } from './check.mjs';
5
+ import { cost } from './cost.mjs';
6
+ import { boostReadsGrowth, boxBuys, farmEndWords, lateRunWords, listOf, lotterySplit, model, outcomeParts, placeOf, prizeLine, prizeRolls, prizes, prizeWords, rollsOf, sharesOf, stashNfts, toSeconds, whenOutWords } from './model.mjs';
7
+ import { farmEnds, playSteps } from './plan.mjs';
8
+ import { inAll, poolFate, round, simulate, unnamedExceptions, unnamedWords } from './simulate.mjs';
9
+
10
+ const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
11
+ const num = (x) => (Number.isFinite(x) ? String(round(x, 6)) : String(x));
12
+
13
+ /**
14
+ * Where a split's shares send the money, and where what they leave goes: to the treasury (`treasury`: how to name it),
15
+ * or `rest` ("into the pot" for a lottery's ticket money). `payer`: who names a "chosen" share, and what each one is
16
+ * ("buyer", "sale"; "payer", "payment").
17
+ */
18
+ function splitWords(split, treasury = 'your treasury', rest = `to ${treasury}`, payer = ['buyer', 'sale']) {
19
+ const shares = sharesOf(split);
20
+ if (!shares.length) return `all of it ${rest}`;
21
+ const left = 100 - shares.reduce((n, s) => n + Number(s.percent), 0);
22
+ const to = (s) => (s.kind === 'burn' ? 'burned' : s.kind === 'treasury' ? `to ${treasury}` : s.kind === 'pool' ? `to the pool “${s.key}”` : s.kind === 'holder' ? 'to whoever holds that collectible' : s.kind === 'lottery' ? `into the pot of the lottery “${s.key}”` : s.kind === 'wallet' ? `to the wallet ${s.key.slice(0, 4)}…` : s.kind === 'chosen' ? (s.key ? `to whoever holds the “${s.key}” the ${payer[0]} names (each ${payer[1]} its own)` : `to the wallet the ${payer[0]} names (each ${payer[1]} its own: a referrer; with none named, to ${treasury})`) : `to the account ${s.key.slice(0, 4)}…`);
23
+ return `${shares.map((s) => `${s.percent}% ${to(s)}`).join(', ')}${left > 0 ? `, ${num(left)}% ${rest}` : ''}`;
24
+ }
25
+
26
+ const ruleWords = (r) => {
27
+ if (!r) return '';
28
+ if (r.wallets) return `they are one of the ${r.wallets.length} wallets it names`;
29
+ const what = r.item ? `a “${r.item}”` : r.collection ? `an NFT of the collection ${r.collection.slice(0, 4)}… (not a Token Metadata print)` : 'one of the game’s collectibles';
30
+ const time = { ahead: ` whose ${r.attribute} is a time still to come`, passed: ` whose ${r.attribute} is a time past (or none)` }[r.time];
31
+ return `${what}${r.atLeast !== undefined ? ` with ${r.attribute} at least ${r.atLeast}` : r.equals !== undefined ? ` whose ${r.attribute} is “${r.equals}”` : time ?? (r.attribute ? ` with a ${r.attribute}` : '')}`;
32
+ };
33
+ /** A stake gate in words: "at least 100 of “Staking” staked …", the farm by its name when `m` has it. */
34
+ const stakeWords = (staked, m) => {
35
+ const f = m?.farm.get(staked.farm);
36
+ return `at least ${staked.atLeast} ${f ? m.tokens.get(f.stakes)?.symbol ?? f.stakes : ''} staked and locked in “${f?.name ?? staked.farm}” (a stake counts while its lock runs, from the second after it is made)`.replace(' ', ' ');
37
+ };
38
+ /** A gate in a sentence: "Only players who show …", "Only the wallets it names …", "Only wallets with … staked …". */
39
+ const gateWords = (gate, verb, m) => (!gate ? '' : gate.wallets ? `Only the ${gate.wallets.length} wallets it names can ${verb}` : gate.list ? `Only the ${gate.list.length} wallets on its list can ${verb} (each passes their proof)` : gate.staked ? `Only wallets with ${stakeWords(gate.staked, m)} can ${verb}` : `Only players who show ${ruleWords(gate)} can ${verb}`);
40
+ /** When a part sells, and how many one wallet may: "from …, until …; 1 per wallet, 2 a day per wallet". */
41
+ const limitWords = (part) => {
42
+ const words = [part.opens && `from ${part.opens}`, part.closes && `until ${part.closes}`, part.perWallet && `${part.perWallet} per wallet`, part.perWalletEach && `${part.perWalletEach.count} per wallet every ${part.perWalletEach.every ?? '1d'}`].filter(Boolean);
43
+ return words.length ? ` ${words.join(', ')}.` : '';
44
+ };
45
+ /** What one outcome of a box that rolls on an NFT does, in words: "+1 level", "the NFT burned", "nothing changes". */
46
+ const fateWords = (o, b, m) => {
47
+ const then = o.then ?? b.then;
48
+ const fate = !then || then === 'back' ? null : then === 'burn' ? 'the NFT burned' : then.stash ? `the NFT kept in “${m.stash.get(then.stash)?.name ?? then.stash}”` : 'the NFT given away';
49
+ const shifts = (o.shifts ?? []).map((x) => `${typeof x.by === 'string' ? x.by : x.by > 0 ? `+${x.by}` : x.by} ${x.attribute}`);
50
+ // A part as a prize line (`prizeWords`), or a stash's NFT at random.
51
+ const won = outcomeParts(o).map((x) => (x.gives === 'stash' ? `an NFT of “${m.stash.get(x.stash)?.name ?? x.stash}”` : prizeWords(x.gives === 'pay' ? { gives: 'tokens', amount: x.amount, from: `pool:${x.pool}` } : x.gives === 'mint' ? { gives: 'tokens', amount: x.amount, currency: x.currency, from: 'mint' } : x, m)));
52
+ const all = [...shifts, ...won, ...(fate ? [fate] : [])];
53
+ return all.length ? all.join(' and ') : 'nothing changes';
54
+ };
55
+ const effectWords = (e) => (e?.perPointPercent !== undefined ? `+${e.perPointPercent}% per point${e.upTo ? ` (up to ${e.upTo})` : ''}` : `+${e?.addPercent ?? 0}%`);
56
+
57
+ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) } = {}) {
58
+ const verdict = check(economy, { now });
59
+ if (!verdict.ok) return { ok: false, refusals: verdict.refusals, warnings: verdict.warnings, text: describe({ refusals: verdict.refusals, warnings: [] }) };
60
+ const m = model(economy, { now });
61
+ /** "your treasury", or the other wallet the spec names as the treasury. */
62
+ const treasuryWords = m.treasuryWallet ? `the treasury (the wallet ${m.treasuryWallet.slice(0, 4)}…${m.treasuryWallet.slice(-4)})` : 'your treasury';
63
+ const splitOf = (split, payer) => splitWords(split, treasuryWords, undefined, payer);
64
+ const sim = simulate(economy, { now });
65
+ const c = cost(economy, { now });
66
+ const play = playSteps(economy);
67
+ const sym = (key) => m.tokens.get(key)?.symbol ?? key;
68
+ const lines = [];
69
+ const section = (title, rows) => {
70
+ if (rows.length) lines.push('', title, ...rows.map((r) => `- ${r}`));
71
+ };
72
+
73
+ const ONE = { tokens: 'token', items: 'item', pools: 'pool', stashes: 'stash', boxes: 'box', payments: 'payment', recipes: 'recipe', schedules: 'schedule', lotteries: 'lottery', farms: 'farm', contests: 'contest', auctions: 'auction', holds: 'hold', grants: 'grant', decisions: 'decision' };
74
+ const counted = Object.keys(ONE).filter((k) => economy[k]?.length).map((k) => plural(economy[k].length, ONE[k], k));
75
+ lines.push(`${economy.name ?? 'This economy'}: ${counted.join(', ') || 'nothing yet'}. The project’s currency is ${m.currency?.symbol ?? '(none)'}${m.currency ? ` (${m.currency.source === 'sol' ? 'SOL' : m.currency.source === 'game' ? 'a token the project mints' : `an existing ${m.currency.program === 'token-2022' ? 'Token-2022 ' : ''}mint`})` : ''}. It runs on the shared loot program, devnet only: no contract of its own.`);
76
+ if (economy.prompt) lines.push(`Asked for: “${economy.prompt}”`);
77
+
78
+ const fee = num(sim.fees.playerSol);
79
+ const actions = [];
80
+ for (const it of sim.items) {
81
+ const src = m.item.get(it.key);
82
+ actions.push(`Buy “${it.name}”: ${num(it.price)} ${sym(it.token)}, ${inAll(it.realCost)}${src.supply ? `; ${src.supply} exist` : ''}${src.rise ? '; the price rises after every sale' : ''}${src.fall ? `; the price falls ${src.fall.step} every ${src.fall.every}${src.fall.floor !== undefined ? ` to at least ${src.fall.floor}` : ''}` : ''}${src.soulbound ? '; soulbound' : ''}${src.gate ? `; ${gateWords(src.gate, 'buy it', m).replace(/^Only/, 'only')}` : ''}. The money: ${splitOf(src.split)}.`);
83
+ }
84
+ for (const b of sim.boxes) {
85
+ const src = m.box.get(b.key);
86
+ const edge = b.houseEdgePercent !== null ? `; the house keeps ${num(b.houseEdgePercent)}%` : b.unpriced.length ? `, plus what ${b.unpriced.join(', ')} ${b.unpriced.length === 1 ? 'is' : 'are'} worth` : '';
87
+ const back = b.returnPercent !== null ? `gives back ${num(b.expectedBack)} ${sym(b.token)} on average (${num(b.returnPercent)}% of its price${edge})` : 'gives back what its items are worth to the player';
88
+ const part = (o) => (o.gives === 'pay' ? `${num(o.amount)} ${sym(o.token)} from “${o.pool}”` : o.gives === 'mint' ? `${num(o.amount)} ${sym(o.token)}, minted` : o.gives === 'stash' ? `an NFT of “${m.stash.get(o.stash)?.name ?? o.stash}”, picked at random` : o.gives === 'nft' ? `the NFT ${String(o.nft).slice(0, 4)}… of “${m.stash.get(o.stash)?.name ?? o.stash}”` : o.gives === 'roll' ? `a roll of “${m.box.get(o.box)?.name ?? o.box}”` : o.gives === 'item' ? `${o.item}` : o.gives === 'burn' ? 'burned' : 'nothing');
89
+ const outs = b.outcomes.map((o) => `${num(o.chance)}% ${o.gives === 'bundle' ? o.prizes.map(part).join(' + ') : part(o)}`).join(', ');
90
+ const odds = (src.odds ?? []).map((o) => `outcome ${o.outcome} ${effectWords(o.add)} for a player who shows ${ruleWords(o.when)}`);
91
+ // A roll on an NFT: what each outcome does to it.
92
+ const fates = (src.outcomes ?? []).flatMap((o, i) => {
93
+ const moves = (o.shifts ?? []).map((x) => (x.fromNow ? `${x.attribute} ${String(x.by).startsWith('-') ? `${String(x.by).slice(1)} sooner` : `${x.by} later, counted from now when it is past`}${x.upTo ? `, at most ${x.upTo} from now` : ''}` : `${x.by > 0 ? '+' : ''}${x.by} ${x.attribute}`));
94
+ const then = o.then ?? src.then;
95
+ const where = then === 'burn' ? 'it is burned' : then === 'treasury' ? 'it goes to the treasury' : then?.giveTo ? `it goes to ${then.giveTo.slice(0, 4)}…` : then?.stash ? `it goes into the stash “${m.stash.get(then.stash)?.name ?? then.stash}”` : '';
96
+ const said = [...moves, where].filter(Boolean).join(' and ');
97
+ return said ? [`outcome ${i}: ${said}`] : [];
98
+ });
99
+ const custodial = (src.outcomes ?? []).some((o) => (o.then ?? src.then) && (o.then ?? src.then) !== 'back');
100
+ const rolls = src.rollsOn
101
+ ? ` It rolls on ${src.active === false ? 'the NFT' : 'an NFT the player brings'} (${ruleWords(src.rollsOn)})${fates.length ? `: ${fates.join('; ')}` : ''}${custodial ? '; the project holds that NFT from the purchase until it is settled, so nobody can move it away from a bad roll' : '; the NFT stays in its wallet, and the numbers follow it wherever it goes'}.`
102
+ : '';
103
+ const named = { grant: 'the grant', contest: 'the contest', auction: 'the auction', mission: 'the mission', recipe: 'the recipe', box: 'the box' };
104
+ const by = [...new Set(rollsOf(m).filter((r) => r.box === src.key && !r.through).map((r) => {
105
+ const [kind, key] = r.by.split(':');
106
+ const part = { grant: m.grant, lottery: m.lottery, contest: m.contest, auction: m.auction, farm: m.farm, recipe: m.recipe, box: m.box }[kind]?.get(key);
107
+ return kind === 'lottery' ? `${r.route} of the lottery “${part?.name ?? key}”` : `${named[r.route] ?? kind} “${part?.name ?? key}”`;
108
+ }))];
109
+ if (src.active === false && (src.rollsOn || by.length)) {
110
+ actions.push(`“${b.name}” is never sold: ${by.join(' and ') || 'nothing'} rolls it. Odds: ${outs}${odds.length ? `; ${odds.join('; ')}` : ''}.${rolls} The winner or the project’s keys ask the oracle for a roll, and settle it (the oracle's fee is the asker's). A roll that holds no payment is never refunded and never lost: an edit of the box leaves it to roll on the box as it is then; one holding a payment or an NFT that the oracle leaves unanswered for 7 days can be refunded. ${whenOutWords(src, { sold: false })}.`);
111
+ continue;
112
+ }
113
+ const verb = boxBuys(src, m) ? 'Buy' : 'Open';
114
+ const oneStep = boxBuys(src, m) ? ' It sells in one step: the price is split and what it gives handed over in the same transaction.' : '';
115
+ actions.push(`${verb} “${b.name}”: ${num(b.price)} ${sym(b.token)}, ${inAll(b.realCost)}; ${back}. ${boxBuys(src, m) ? 'It gives' : 'Odds'}: ${outs}${odds.length ? `; ${odds.join('; ')}` : ''}${src.gate ? `. ${gateWords(src.gate, 'open it', m)}` : ''}.${rolls} The money: ${splitOf(src.split)}.${limitWords(src)}${src.outcomes.flatMap(outcomeParts).some((o) => o.gives === 'pay') ? (boxBuys(src, m) ? ' It sells only while its pools hold every fixed prize.' : ' A prize won is set aside for its winner at once.') : ''}${b.limitedStock ? ` ${whenOutWords(src)}.` : ''}${!boxBuys(src, m) ? ' An edit of the box’s roll fails a purchase still waiting, for its refund.' : ''}${oneStep}`);
116
+ }
117
+ for (const p of sim.payments) actions.push(`Pay “${p.name}”: ${p.atLeast ? `whatever the player chooses, at least ${num(p.amount)} ${sym(p.token)} (the page keeps the least; the chain takes any amount)` : `${num(p.amount)} ${sym(p.token)}`}, ${inAll(p.realCost)}. The money: ${splitOf(m.payments.find((x) => x.key === p.key)?.split, ['payer', 'payment'])}.`);
118
+ for (const r of sim.recipes) actions.push(`Craft “${r.name}”: ${r.takes.join(' + ') || 'nothing'} → ${r.gives.join(' + ') || 'nothing'} (+ ${fee} SOL player fee).`);
119
+ for (const l of sim.lotteries) {
120
+ const src = m.lottery.get(l.key);
121
+ // Its split (and its cut, a share to the treasury) takes its shares, and the rest goes into the pot.
122
+ const others = (src.split ?? []).length ? `; ticket money goes ${splitWords(lotterySplit(src), treasuryWords, 'into the pot')}` : '';
123
+ if (src.ticketNft) {
124
+ const goes = src.noLoss ? 'held until the round is drawn, then goes back to its buyer (no loss)' : `${!src.ticketNft.then || src.ticketNft.then === 'burn' ? 'burned' : src.ticketNft.then.stash ? `put in the stash “${src.ticketNft.then.stash}”` : 'given on'} once the round is drawn (back to its buyer only if the draw is cancelled)`;
125
+ actions.push(`Buy a ticket for “${l.name}” with one NFT that is ${ruleWords(src.ticketNft.rule)} (+ ${fee} SOL player fee): it is ${goes}. ${l.drawsOnce ? 'Drawn once, then it ends' : `Drawn every ${num(l.roundDays)} days`}; the pot is what the owner seeds and what its places give.${src.gate ? ` ${gateWords(src.gate, 'buy tickets', m)}.` : ''}`);
126
+ continue;
127
+ }
128
+ actions.push(`Buy tickets for “${l.name}”: ${num(l.price)} ${sym(l.token)} a ticket; one ticket bought alone is ${inAll(l.realCost)}, and the fee is per purchase, so several tickets at once share it. ${l.drawsOnce ? 'Drawn once, then it ends' : `Drawn every ${num(l.roundDays)} days`}; the pot (an account of its own, which only its winners are paid from) reaches about ${num(l.potBeforeDraw)} ${sym(l.potToken)} and pays ${l.prizes.map(num).join(' / ')}; ${src.noLoss ? `no loss: every ticket comes back to its buyer once the round is drawn (anyone can pull it), and the winners take only the pot, what is paid into it, so a ticket wins ${num(l.expectedPerTicket)} on average on top of its price coming back (tickets are not weighted by time: someone may buy most of them right before the draw and risk only the fees)` : `a ticket is worth ${num(l.expectedPerTicket)} on average${l.unpricedPrizes ? ', plus its chance at the places’ prizes' : ''}${l.returnPercent !== null ? ` (${num(l.returnPercent)}%)` : ''}`}${l.cutPercent && !(src.split ?? []).length ? `; ${num(l.cutPercent)}% of ticket money goes to ${treasuryWords}, the rest into the pot` : ''}${others}${src.perWallet ? `; at most ${src.perWallet} tickets per wallet a round` : ''}.${src.gate ? ` ${gateWords(src.gate, 'buy tickets', m)}.` : ''}`);
129
+ }
130
+ for (const f of sim.farms) {
131
+ const src = m.farm.get(f.key);
132
+ const tiers = f.tiers.map((t) => `x${t.multiplier} for ${t.lock}`).join(', ');
133
+ const boosts = (src.boosts ?? []).map((b) => `${effectWords(b.add)} for ${ruleWords(b.when)}`);
134
+ const custody = src.stakes !== 'collectibles' && !boosts.length ? '' : {
135
+ freeze: ' NFTs stay locked in the wallet (a Core freeze, Token Metadata’s lock, a compressed NFT’s freeze); issuer metadata growth can prevent Core from unfreezing ordinary assets.',
136
+ watch: ' NFTs stay free in the wallet: one sold while staked stops earning for its staker, and its new holder can take the seat over.',
137
+ }[src.custody] ?? ' NFTs are handed to the game until they leave; issuer-controlled transfer rules can still prevent return.';
138
+ const earns = f.rate && !(Number(f.rate.perDay) > 0)
139
+ ? 'no tokens'
140
+ : f.rate
141
+ ? `${num(f.rate.perDay)} ${sym(f.rewardsToken)} a day ${f.rate.shared ? 'for the whole farm, shared by weight while anything is staked' : `for each ${f.unit ?? (src.stakes === 'collectibles' ? 'collectible' : sym(src.stakes))} at x1, whoever else is in`}${f.rate.halveEvery ? `, halving every ${f.rate.halveEvery}` : ''} (${f.rate.minted ? 'minted when a seat is paid while the mint has room for it, never owed later past that' : 'from its rewards vault while it lasts'}); what a seat gives back (a forfeit, a watched NFT sold) is ${f.rate.minted ? 'never minted' : 'left free in its vault'}, never to the other seats`
142
+ : `${num(f.rewardsPerDay)} ${sym(f.rewardsToken)} a day, what lands in its rewards vault, shared by weight${f.perUnitPerDay !== undefined ? ` (${num(f.perUnitPerDay)} a day for each ${f.unit} at x1${f.yearlyPercent !== undefined ? `, ${num(f.yearlyPercent)}% a year` : ''})` : ''}`;
143
+ const grows = (f.growth ?? []).map((g) => `; staying in earns ${g.perDay} ${g.attribute} a day at x1, written on the collectible${g.levels ? ` with its ${g.level} (${g.levels.map((l) => `${l.level} after ${num(l.days)} days`).join(', ')})` : ''}${g.streak ? ', back to 0 when it leaves' : ', kept when it leaves'}`).join('');
144
+ const gates = listOf(src.gate);
145
+ const takes = gates.length ? `; to stake here an NFT must be ${gates.map(ruleWords).join(' and ')}` : '';
146
+ const end = farmEndWords(src);
147
+ const runs = src.runs ? `; it runs ${src.runs} from when it goes live, taking no stake at its end or after it, nor one that could not leave by then, and pays and grows nothing after${end ? `; ${end}` : ''}` : '';
148
+ const lock = src.lockAttribute ? `; each collectible’s lock is written on it under “${src.lockAttribute}” (the time it ends) and it leaves once that time has come, so a recipe or a roll that moves “${src.lockAttribute}” lets it out sooner or keeps it longer (a mission’s prize still needs the tier’s own lock)` : '';
149
+ const upkeep = src.upkeepAttribute ? `; a collectible earns and grows only until the time under its “${src.upkeepAttribute}” (its upkeep): run dry, it earns nothing more from that time until it is topped up and its seat refreshed` : '';
150
+ // A farm that keeps its record of ends: what a seat earned by its end is exact, and each new end time has a rent.
151
+ const ends = farmEnds(src) ? `; what a seat earned by its end (${(src.tiers ?? []).some((t) => t.earnsFor) ? 'its tier’s term' : 'its upkeep'}) is exact however late it is next touched, and nobody has to act at that time: the farm keeps a record of those times, each new one costing whoever writes it 44 bytes of rent (about 0.00022 SOL; spare rent in the record pays first), not refunded on leaving. A farm idle past many ends has them walked in parts first (\`app.walkFarm\`, free and anyone’s)` : '';
152
+ // A boost that reads a level its own growth writes counts each level from when it is written.
153
+ const refreshes = boostReadsGrowth(src) ? '; a boost that reads what its own growth writes counts each level from when it is written, by a seat’s refresh, so a seat refreshed often earns more than one refreshed rarely: have the page or a keeper refresh seats (anyone may, for free)' : '';
154
+ const missions = f.tiers.filter((t) => t.prize).map((t) => `${t.prize} for staying ${t.lock}`);
155
+ const results = (src.tiers ?? []).filter((t) => t.roll).map((t) => {
156
+ const box = m.box.get(t.roll);
157
+ const fates = box ? [...new Set((box.outcomes ?? []).map((o) => fateWords(o, box, m)))] : [];
158
+ return `leaving after ${t.lock}, the NFT is rolled on “${box?.name ?? t.roll}”${fates.length ? ` (its result: ${fates.length > 1 ? `${fates.slice(0, -1).join('; ')}; or ${fates.at(-1)}` : fates[0]})` : ''}`;
159
+ });
160
+ actions.push(`Stake ${src.stakes === 'collectibles' ? `collectibles${src.item ? ` (${src.item})` : ''}` : sym(src.stakes)} in “${f.name}”: ${earns}${grows}${takes}${runs}${lock}${upkeep}${ends}${refreshes}${missions.length ? `; a mission that stays its lock (topped up, not forfeited) brings back ${missions.join(', ')}, given as it leaves (a roll is settled after)` : ''}${results.length ? `; ${results.join('; ')}` : ''}; lock tiers ${tiers}${boosts.length ? `; boosts ${boosts.join(', ')}, at most +${src.maxBoostPercent ?? 0}%` : ''}. After the lock, pause and suspend cannot stop an unstake; external token or NFT controls can still block custody release.${custody}`);
161
+ }
162
+ for (const t of m.traits) actions.push(`Prove an NFT’s traits (anyone, once for each NFT, paying its Sheet’s small rent): what the off-chain metadata of the NFTs of ${t.collection.slice(0, 4)}… says, from the root the game vouched for, goes in the NFT’s Sheet, where boosts, gates and odds read it.${t.about ? ` ${t.about}` : ''}`);
163
+ for (const ct of sim.contests) {
164
+ const src = m.contest.get(ct.key);
165
+ const roll = prizeLine(src?.prize) ? ` Each winning entry also gets ${prizeWords(src.prize, m)} when it is claimed${prizeLine(src.prize).gives === 'roll' ? '' : ', in the same transaction'}.` : '';
166
+ if (src.stakeNft) {
167
+ actions.push(`Back a side in “${ct.name}” with one NFT that is ${ruleWords(src.stakeNft)} (+ ${fee} SOL player fee): the NFTs are the pot. The authority names the side that won; each winning entry takes its own NFT back and one loser’s, in the order the entries are claimed, and what the winners leave goes to whoever opened it. Called off, or not decided by its deadline, every entry takes its own back.${roll}`);
168
+ continue;
169
+ }
170
+ actions.push(`Back a side in “${ct.name}” (+ ${fee} SOL player fee): ${ct.says}; the authority names the side that won, and a contest not decided by its deadline pays everyone back.${roll}`);
171
+ }
172
+ for (const a of sim.auctions ?? []) {
173
+ const src = m.auction.get(a.key);
174
+ const lot = src.lot?.item ? `one new “${m.item.get(src.lot.item)?.name ?? src.lot.item}”` : src.lot?.roll ? `a roll of “${m.box.get(src.lot.roll)?.name ?? src.lot.roll}” for the winner (a mystery lot: what it gives is chance, and the price goes to ${treasuryWords})` : `the NFT ${src.lot?.asset?.slice(0, 4)}…`;
175
+ if (src.bidNfts) {
176
+ actions.push(`Bid on “${a.name}”, which sells ${lot}, with bundles of up to three Core or Token Metadata NFTs that are ${ruleWords(src.bidNfts)} (+ ${fee} SOL player fee a bid): the bundle with the most wins (at least ${src.reserve ?? 1}); a bundle that is topped goes back to its bidder at once, and the winning NFTs go to ${src.lot?.asset ? 'the seller' : treasuryWords}. It runs ${src.runs}.${src.gate ? ` ${gateWords(src.gate, 'bid', m)}.` : ''}`);
177
+ continue;
178
+ }
179
+ actions.push(`Bid on “${a.name}”, which sells ${lot}: bids in ${sym(a.token)}${a.reserve ? ` from ${num(a.reserve)}` : ''}, each topping the last by at least ${src.minRaise ?? 5}% (+ ${fee} SOL player fee a bid); a bid that is topped goes back to its bidder at once. It runs ${src.runs}${toSeconds(src.extendBy ?? '10m') > 0 ? `, and a bid in the last ${src.extendBy ?? '10m'} moves the end to ${src.extendBy ?? '10m'} after it` : ''}; anyone ends it then, the lot goes to the highest bidder and the money: ${splitOf(src.split)}.${src.lot?.asset ? ' A seller whose account can’t take the money then doesn’t hold up the winner: the lot goes, the auction stays open owing it, and the next end pays and closes it.' : ''} With no bid the lot goes home; an auction nobody ends waits, and sells to the highest bidder whenever anyone ends it.${src.gate ? ` ${gateWords(src.gate, 'bid', m)}.` : ''}`);
180
+ }
181
+ for (const h of m.holds) actions.push(`Hand in a ${h.item} for “${h.name}”${h.until ? ` until ${h.until}` : h.for ? ` for ${h.for}` : ''}: the authority says where it goes; after the time is up anyone can send it home.`);
182
+ if (m.payments.length || m.boxes.length) actions.push('Pay with an NFT, outside the spec (`loot.payNftIx`): any NFT a player holds, burned or given to a wallet; the app admits them by the receipt, as for a payment.');
183
+ for (const g of sim.grants) {
184
+ const src = m.grant.get(g.key);
185
+ const l = src.limits ?? {};
186
+ const limits = [l.perWallet && `${l.perWallet} per wallet`, l.perWalletEach && `${l.perWalletEach.count} per wallet every ${l.perWalletEach.every ?? '1d'}`, l.perPerson && `${l.perPerson} per person`, l.perKey && `${l.perKey} per key`, l.perDay && `${l.perDay} a day`, l.total && `${l.total} in all`].filter(Boolean).join(', ');
187
+ const signers = [(src.vouchers ?? []).length && 'a voucher key says earned it', (src.keys ?? []).length && `one of its ${src.keys.length} keys (codes) signed for`, (src.list ?? []).length && `on the list of ${src.list.length}`].filter(Boolean);
188
+ const who = signers.length ? signers.join(' or ') : 'that asks (it is open to any wallet)';
189
+ // A grant of a fixed amount of tokens, paid by voucher or list: each claim may name its own amount, up to the grant's.
190
+ const named = signers.length > 0 && src.prize?.gives === 'tokens';
191
+ actions.push(`Claim “${g.name}” (only the winning wallet claims, signing it; anyone may pay its fees): gives ${g.gives}, to a wallet ${who}${limits ? `; limits ${limits}` : '; no limits'}${src.gate ? (src.gate.wallets ? `; only the ${src.gate.wallets.length} wallets it names` : src.gate.list || src.gate.staked ? `; ${gateWords(src.gate, 'claim it', m).replace(/^Only/, 'only')}` : `; the wallet must hold ${ruleWords(src.gate)}${!signers.length ? ', and each one of those claims once too' : ''}`) : ''}${named ? `; a voucher or the list can name each claim’s own amount, up to that (a reward by rank or share)` : ''}. Each claim costs the grant’s funds about ${num(g.fundsPerClaimSol)} SOL of rent, and whoever sends it the ${fee} SOL player fee.`);
192
+ }
193
+ for (const st of sim.stashes ?? []) {
194
+ const src = m.stash.get(st.key);
195
+ const nfts = stashNfts(src);
196
+ const boxes = m.boxes.filter((b) => (b.outcomes ?? []).flatMap(outcomeParts).some((o) => o.gives === 'stash' && o.stash === st.key)).map((b) => `“${b.name}”`);
197
+ const direct = prizes(m).filter((p) => p.prize.gives === 'nft' && p.prize.stash === st.key).map((p) => `${p.by} (${prizeWords(p.prize, m)}, given at once)`);
198
+ actions.push(`The stash “${st.name}” holds ${plural(nfts.length, 'NFT')} the game gives away${boxes.length ? ` through ${boxes.join(', ')}` : ''}${direct.length ? `${boxes.length ? ' and' : ' through'} ${direct.join(', ')}` : ''}, put in by your wallet when it goes live${st.takenInPerDay ? ` and by what players pay with an NFT (about ${num(st.takenInPerDay)} a day in the scenario)` : ''}: a roll that lands on it picks one at random and takes it out at once, so two winners never get the same one, and the roll is then settled to hand it over; a prize that names an NFT hands it over in the instruction that wins it. Empty, a roll that lands on it makes no draw, and gives back that draw’s worth. Your wallet (or the authority) can put more in, or take one out, any time.${st.lastsDays !== undefined ? ` At the scenario’s rate it lasts about ${num(st.lastsDays)} days.` : ''}`);
199
+ }
200
+ // Prizes that are a roll of a box: made for the winner, then asked of the oracle and settled by the winner or the project’s keys.
201
+ const prized = prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route));
202
+ if (prized.length)
203
+ actions.push(`Prizes that are a roll of a box (${[...new Set(prized.map((r) => `${r.by} → “${m.box.get(r.box)?.name ?? r.box}”`))].join(', ')}): the winner gets an order of that box, made by whoever sends the claim, the place’s payment or the auction’s end (its rent, back to the winner when it is settled); the winner or the project’s keys then ask the oracle for the roll (its fee is the asker’s) and settle it. A roll that holds no payment is never refunded and never lost: it rolls on the box as it is when it is asked.`);
204
+ for (const l of m.lotteries) {
205
+ const rolls = (l.places ?? []).map(placeOf).map((p, i) => (p.prize ? `${p.times > 1 ? `${p.times} places from place ${i + 1} each win` : `place ${i + 1} wins`} ${prizeWords(p.prize, m)}${p.percent ? ` and ${p.percent}% of the pot` : ''}` : null)).filter(Boolean);
206
+ if (rolls.length) actions.push(`In “${l.name}”, ${rolls.join('; ')}.`);
207
+ }
208
+ section('What players do, what it costs and what it is worth', actions);
209
+
210
+ section('How the pools fill and drain', sim.pools.map((p) => {
211
+ const by = (rows) => rows.map((r) => (r.percentPerDay !== undefined ? `${r.by} ${num(r.percentPerDay)}% of what it holds a day` : `${r.by} ${num(r.perDay)} a day`)).join(', ') || 'nothing';
212
+ return `${p.pot ? `The pot of “${p.pot}”` : `“${p.key}”`} (${sym(p.token)}): starts at ${num(p.seed)}; in: ${by(p.ins)}; out: ${by(p.outs)}; it ${poolFate(p, sim.scenario.days)}.`;
213
+ }));
214
+ section('What runs on a timer', [...sim.schedules.map((s) => { const sc = m.schedules.find((x) => x.key === s.key); return `“${sc?.name ?? s.key}” ${s.says}; ${sc?.sealed ? 'its recipient runs it when it is due' : 'you, or a key of the project’s on your server, must send each run when it is due: nothing runs it otherwise'}; ${lateRunWords(sc ?? {})}.`; }), ...sim.lotteries.map((l) => `“${l.name}” is drawn ${l.drawsOnce ? 'once' : `every ${num(l.roundDays)} days`} by MagicBlock’s oracle; anyone can ask for the draw (${num(sim.fees.oracleSol)} SOL to the oracle), move the drawn round on and pull what each entry is owed.`)]);
215
+ const authority = play.filter((s) => s.signer.startsWith('authority'));
216
+ section(`What only the authority signs while it runs (${m.authority === 'owner' ? 'your wallet' : `the key ${m.authority}`})`, authority.map((s) => `${s.says} (${s.part}, ${s.ix}).`));
217
+ if (authority.length) lines.push('- Checking that a player really did what a payout is for is up to you, on your own server: a game runs in the player’s browser, and the program trusts the authority’s signature.');
218
+
219
+ const s = c.setup;
220
+ lines.push('', 'What setting it up costs');
221
+ lines.push(`- About ${num(s.totalSol)} SOL in all, in ${plural(s.transactions, 'transaction')} (${plural(s.approvals, 'wallet approval')}): rent ${num(s.rentSol)} SOL for ${plural(s.accounts.length, 'account')}, program fees ${num(s.programFeesSol)} SOL, network fees ${num(s.networkFeesSol)} SOL${Object.keys(s.seeds).length ? `, seeds ${Object.entries(s.seeds).map(([k, v]) => `${num(v)} ${k}`).join(', ')}` : ''}.`);
222
+ for (const n of s.notes) lines.push(`- ${n}`);
223
+
224
+ const d = sim.perDay;
225
+ const list = (map) => Object.entries(map).map(([k, v]) => `${num(v)} ${sym(k)}`).join(', ') || 'nothing';
226
+ const named = Object.entries(sim.scenario.perDay).map(([k, v]) => `${v} × ${k}`).join(', ');
227
+ const except = unnamedExceptions(sim);
228
+ lines.push('', `Each day, if ${named ? `${named} happen (anything else: ${unnamedWords(sim)})` : `${sim.scenario.defaultPerDay} of each thing happen${except.length ? ` (${except.join(', ')})` : ''}`}`);
229
+ lines.push(`- Players pay ${list(d.players.pay)} and get back ${list(d.players.getBack)} on average, plus ${num(d.players.playerFeesSol)} SOL in player fees${d.players.oracleFeesSol ? ` and ${num(d.players.oracleFeesSol)} SOL to MagicBlock’s oracle` : ''}.`);
230
+ lines.push(`- Your treasury gets ${list(d.creator.treasury)}; burned: ${list(d.burned)}; the platform’s fees: ${num(d.platform.totalSol)} SOL.`);
231
+ for (const n of sim.notes) lines.push(`- ${n}`);
232
+
233
+ const flagged = verdict.warnings.filter((w) => w.code === 'W-CHANCE-PRIZE' || w.code === 'W-REVENUE-SHARE');
234
+ const others = verdict.warnings.filter((w) => !flagged.includes(w));
235
+ if (flagged.length) section('Flagged for the law, once it is real money', [...new Set(flagged.map((w) => w.message))]);
236
+ section('Know before going live', others.map((w) => `${w.message}${w.fix ? ` ${w.fix}` : ''}`));
237
+ lines.push('', 'Devnet only: nothing here is real money. Going live is your approval in your own wallet: no one else signs, and no key is ever shared.');
238
+ return { ok: true, text: lines.join('\n').trim(), warnings: verdict.warnings, needsAuthority: authority.length > 0, chance: verdict.warnings.some((w) => w.code === 'W-CHANCE-PRIZE'), setupSol: s.totalSol };
239
+ }