@xgamesfun/solana 0.20.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
@@ -3,12 +3,33 @@
3
3
  // going live costs, and what the owner must know. Deterministic. Pure.
4
4
  import { check, describe } from './check.mjs';
5
5
  import { cost } from './cost.mjs';
6
- import { boxBuys, farmEndWords, listOf, lotterySplit, model, outcomeBurns, outcomeParts, placeOf, prizeLine, prizeRolls, prizes, prizeWords, rollsOf, sharesOf, stashNfts, toSeconds } from './model.mjs';
7
- import { playSteps } from './plan.mjs';
6
+ import {
7
+ boostReadsGrowth,
8
+ boxBuys,
9
+ destination,
10
+ farmEndWords,
11
+ keepsPot,
12
+ lateRunWords,
13
+ listOf,
14
+ lotterySplit,
15
+ model,
16
+ outcomeParts,
17
+ placeOf,
18
+ prizeLine,
19
+ prizeRolls,
20
+ prizes,
21
+ prizeWords,
22
+ rollsOf,
23
+ sharesOf,
24
+ stashNfts,
25
+ toSeconds,
26
+ whenOutWords,
27
+ } from './model.mjs';
28
+ import { farmEnds, playSteps } from './plan.mjs';
8
29
  import { inAll, poolFate, round, simulate, unnamedExceptions, unnamedWords } from './simulate.mjs';
9
30
 
10
31
  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));
32
+ const num = x => (Number.isFinite(x) ? String(round(x, 6)) : String(x));
12
33
 
13
34
  /**
14
35
  * Where a split's shares send the money, and where what they leave goes: to the treasury (`treasury`: how to name it),
@@ -19,217 +40,551 @@ function splitWords(split, treasury = 'your treasury', rest = `to ${treasury}`,
19
40
  const shares = sharesOf(split);
20
41
  if (!shares.length) return `all of it ${rest}`;
21
42
  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}` : ''}`;
43
+ const to = s => toWords(s, treasury, payer);
44
+ return `${shares.map(s => `${s.percent}% ${to(s)}`).join(', ')}${left > 0 ? `, ${num(left)}% ${rest}` : ''}`;
24
45
  }
25
46
 
26
- const ruleWords = (r) => {
47
+ /** Where one destination (model `destination`) sends money, in words. */
48
+ const toWords = (s, treasury = 'your treasury', payer = ['buyer', 'sale']) =>
49
+ s.kind === 'burn'
50
+ ? 'burned'
51
+ : s.kind === 'treasury'
52
+ ? `to ${treasury}`
53
+ : s.kind === 'pool'
54
+ ? `to the pool “${s.key}”`
55
+ : s.kind === 'holder'
56
+ ? 'to whoever holds that collectible'
57
+ : s.kind === 'lottery'
58
+ ? `into the pot of the lottery “${s.key}”`
59
+ : s.kind === 'farm'
60
+ ? `to the stakers of the farm “${s.key}”`
61
+ : s.kind === 'wallet'
62
+ ? `to the wallet ${s.key.slice(0, 4)}…`
63
+ : s.kind === 'chosen'
64
+ ? s.key
65
+ ? `to whoever holds the “${s.key}” the ${payer[0]} names (each ${payer[1]} its own)`
66
+ : `to the wallet the ${payer[0]} names (each ${payer[1]} its own: a referrer; with none named, to ${treasury})`
67
+ : `to the account ${s.key.slice(0, 4)}…`;
68
+
69
+ /**
70
+ * Where a lottery's places are paid from, in words (`"pot"` or `"rest"`): a pot counts whatever reaches it and what
71
+ * earlier rounds left, and the next round sees what this one leaves; a round's own money can't be changed from outside.
72
+ */
73
+ const paysWords = (l, treasury) =>
74
+ keepsPot(l)
75
+ ? `its places are paid from its pot (an account of its own, which only its winners are paid from): each place takes its percent of what the pot holds when the draw is asked, with the round’s ticket money, so whatever reaches the pot counts (the seed, a sponsor, shares of other sales, yield) and so does what earlier rounds’ places left; what this round’s places leave stays in the pot, and the next round always starts with it`
76
+ : `its places are paid from each round’s own ticket money alone, which nothing sent from outside can change (no seed, no gift, no carry-over); what the places leave, and the part of places nobody won, goes ${toWords(destination(l.rest), treasury)}${l.rest === 'burn' ? ' when the round closes' : ''}`;
77
+
78
+ const ruleWords = r => {
27
79
  if (!r) return '';
28
80
  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)}…` : '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}` : '')}`;
81
+ if (r.items) return `any of ${r.items.map(k => `“${k}”`).join(', ')}`;
82
+ const what = r.item
83
+ ? `a “${r.item}”`
84
+ : r.collection
85
+ ? `an NFT of the collection ${r.collection.slice(0, 4)}… (not a Token Metadata print)`
86
+ : 'one of the game’s collectibles';
87
+ const time = {
88
+ ahead: ` whose ${r.attribute} is a time still to come`,
89
+ passed: ` whose ${r.attribute} is a time past (or none)`,
90
+ }[r.time];
91
+ return `${what}${r.atLeast !== undefined ? ` with ${r.attribute} at least ${r.atLeast}` : r.equals !== undefined ? ` whose ${r.attribute} is “${r.equals}”` : r.oneOf ? ` whose ${r.attribute} is one of ${r.oneOf.map(t => `“${t}”`).join(', ')}` : (time ?? (r.attribute ? ` with a ${r.attribute}` : ''))}`;
32
92
  };
33
93
  /** A stake gate in words: "at least 100 of “Staking” staked …", the farm by its name when `m` has it. */
34
94
  const stakeWords = (staked, m) => {
35
95
  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(' ', ' ');
96
+ 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(
97
+ ' ',
98
+ ' ',
99
+ );
37
100
  };
38
101
  /** 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}`);
102
+ const gateWords = (gate, verb, m) =>
103
+ !gate
104
+ ? ''
105
+ : gate.wallets
106
+ ? `Only the ${gate.wallets.length} wallets it names can ${verb}`
107
+ : gate.list
108
+ ? `Only the ${gate.list.length} wallets on its list can ${verb} (each passes their proof)`
109
+ : gate.staked
110
+ ? `Only wallets with ${stakeWords(gate.staked, m)} can ${verb}`
111
+ : `Only players who show ${ruleWords(gate)} can ${verb}`;
40
112
  /** 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);
113
+ const limitWords = part => {
114
+ const words = [
115
+ part.opens && `from ${part.opens}`,
116
+ part.closes && `until ${part.closes}`,
117
+ part.perWallet && `${part.perWallet} per wallet`,
118
+ part.perWalletEach && `${part.perWalletEach.count} per wallet every ${part.perWalletEach.every ?? '1d'}`,
119
+ ].filter(Boolean);
43
120
  return words.length ? ` ${words.join(', ')}.` : '';
44
121
  };
45
122
  /** What one outcome of a box that rolls on an NFT does, in words: "+1 level", "the NFT burned", "nothing changes". */
46
123
  const fateWords = (o, b, m) => {
47
124
  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}`);
125
+ const fate =
126
+ !then || then === 'back'
127
+ ? null
128
+ : then === 'burn'
129
+ ? 'the NFT burned'
130
+ : then.stash
131
+ ? `the NFT kept in “${m.stash.get(then.stash)?.name ?? then.stash}”`
132
+ : 'the NFT given away';
133
+ const shifts = (o.shifts ?? []).map(
134
+ x => `${typeof x.by === 'string' ? x.by : x.by > 0 ? `+${x.by}` : x.by} ${x.attribute}`,
135
+ );
50
136
  // 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)));
137
+ const won = outcomeParts(o).map(x =>
138
+ x.gives === 'stash'
139
+ ? `an NFT of “${m.stash.get(x.stash)?.name ?? x.stash}”`
140
+ : prizeWords(
141
+ x.gives === 'pay'
142
+ ? { gives: 'tokens', amount: x.amount, from: `pool:${x.pool}` }
143
+ : x.gives === 'mint'
144
+ ? { gives: 'tokens', amount: x.amount, currency: x.currency, from: 'mint' }
145
+ : x,
146
+ m,
147
+ ),
148
+ );
52
149
  const all = [...shifts, ...won, ...(fate ? [fate] : [])];
53
150
  return all.length ? all.join(' and ') : 'nothing changes';
54
151
  };
55
- const effectWords = (e) => (e?.perPointPercent !== undefined ? `+${e.perPointPercent}% per point${e.upTo ? ` (up to ${e.upTo})` : ''}` : `+${e?.addPercent ?? 0}%`);
152
+ const effectWords = e =>
153
+ e?.perPointPercent !== undefined
154
+ ? `+${e.perPointPercent}% per point${e.upTo ? ` (up to ${e.upTo})` : ''}`
155
+ : `+${e?.addPercent ?? 0}%`;
56
156
 
57
157
  export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) } = {}) {
58
158
  const verdict = check(economy, { now });
59
- if (!verdict.ok) return { ok: false, refusals: verdict.refusals, warnings: verdict.warnings, text: describe({ refusals: verdict.refusals, warnings: [] }) };
159
+ if (!verdict.ok)
160
+ return {
161
+ ok: false,
162
+ refusals: verdict.refusals,
163
+ warnings: verdict.warnings,
164
+ text: describe({ refusals: verdict.refusals, warnings: [] }),
165
+ };
60
166
  const m = model(economy, { now });
61
167
  /** "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';
168
+ const treasuryWords = m.treasuryWallet
169
+ ? `the treasury (the wallet ${m.treasuryWallet.slice(0, 4)}…${m.treasuryWallet.slice(-4)})`
170
+ : 'your treasury';
63
171
  const splitOf = (split, payer) => splitWords(split, treasuryWords, undefined, payer);
64
172
  const sim = simulate(economy, { now });
65
173
  const c = cost(economy, { now });
66
174
  const play = playSteps(economy);
67
- const sym = (key) => m.tokens.get(key)?.symbol ?? key;
175
+ const sym = key => m.tokens.get(key)?.symbol ?? key;
68
176
  const lines = [];
69
177
  const section = (title, rows) => {
70
- if (rows.length) lines.push('', title, ...rows.map((r) => `- ${r}`));
178
+ if (rows.length) lines.push('', title, ...rows.map(r => `- ${r}`));
71
179
  };
72
180
 
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.`);
181
+ const ONE = {
182
+ tokens: 'token',
183
+ items: 'item',
184
+ pools: 'pool',
185
+ stashes: 'stash',
186
+ boxes: 'box',
187
+ payments: 'payment',
188
+ recipes: 'recipe',
189
+ schedules: 'schedule',
190
+ lotteries: 'lottery',
191
+ farms: 'farm',
192
+ contests: 'contest',
193
+ auctions: 'auction',
194
+ holds: 'hold',
195
+ grants: 'grant',
196
+ decisions: 'decision',
197
+ };
198
+ const counted = Object.keys(ONE)
199
+ .filter(k => economy[k]?.length)
200
+ .map(k => plural(economy[k].length, ONE[k], k));
201
+ lines.push(
202
+ `${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.`,
203
+ );
76
204
  if (economy.prompt) lines.push(`Asked for: “${economy.prompt}”`);
77
205
 
78
206
  const fee = num(sim.fees.playerSol);
79
207
  const actions = [];
80
208
  for (const it of sim.items) {
81
209
  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)}.`);
210
+ actions.push(
211
+ `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)}.`,
212
+ );
83
213
  }
84
214
  for (const b of sim.boxes) {
85
215
  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 === 'share' ? `${o.percent}% of “${o.pool}”` : 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(' + ')}${o.burns ? ' (the payment burned)' : ''}` : part(o)}`).join(', ');
90
- const odds = (src.odds ?? []).map((o) => `outcome ${o.outcome} ${effectWords(o.add)} for a player who shows ${ruleWords(o.when)}`);
216
+ const edge =
217
+ b.houseEdgePercent !== null
218
+ ? `; the house keeps ${num(b.houseEdgePercent)}%`
219
+ : b.unpriced.length
220
+ ? `, plus what ${b.unpriced.join(', ')} ${b.unpriced.length === 1 ? 'is' : 'are'} worth`
221
+ : '';
222
+ const back =
223
+ b.returnPercent !== null
224
+ ? `gives back ${num(b.expectedBack)} ${sym(b.token)} on average (${num(b.returnPercent)}% of its price${edge})`
225
+ : 'gives back what its items are worth to the player';
226
+ const part = o =>
227
+ o.gives === 'pay'
228
+ ? `${num(o.amount)} ${sym(o.token)} from “${o.pool}”`
229
+ : o.gives === 'mint'
230
+ ? `${num(o.amount)} ${sym(o.token)}, minted`
231
+ : o.gives === 'stash'
232
+ ? `an NFT of “${m.stash.get(o.stash)?.name ?? o.stash}”, picked at random`
233
+ : o.gives === 'nft'
234
+ ? `the NFT ${String(o.nft).slice(0, 4)}… of “${m.stash.get(o.stash)?.name ?? o.stash}”`
235
+ : o.gives === 'roll'
236
+ ? `a roll of “${m.box.get(o.box)?.name ?? o.box}”`
237
+ : o.gives === 'item'
238
+ ? `${o.item}`
239
+ : o.gives === 'burn'
240
+ ? 'burned'
241
+ : 'nothing';
242
+ const outs = b.outcomes
243
+ .map(o => `${num(o.chance)}% ${o.gives === 'bundle' ? o.prizes.map(part).join(' + ') : part(o)}`)
244
+ .join(', ');
245
+ const odds = (src.odds ?? []).map(
246
+ o => `outcome ${o.outcome} ${effectWords(o.add)} for a player who shows ${ruleWords(o.when)}`,
247
+ );
91
248
  // A roll on an NFT: what each outcome does to it.
92
249
  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}`));
250
+ const moves = (o.shifts ?? []).map(x =>
251
+ x.fromNow
252
+ ? `${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` : ''}`
253
+ : `${x.by > 0 ? '+' : ''}${x.by} ${x.attribute}`,
254
+ );
94
255
  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}”` : '';
256
+ const where =
257
+ then === 'burn'
258
+ ? 'it is burned'
259
+ : then === 'treasury'
260
+ ? 'it goes to the treasury'
261
+ : then?.giveTo
262
+ ? `it goes to ${then.giveTo.slice(0, 4)}…`
263
+ : then?.stash
264
+ ? `it goes into the stash “${m.stash.get(then.stash)?.name ?? then.stash}”`
265
+ : '';
96
266
  const said = [...moves, where].filter(Boolean).join(' and ');
97
267
  return said ? [`outcome ${i}: ${said}`] : [];
98
268
  });
99
- const custodial = (src.outcomes ?? []).some((o) => (o.then ?? src.then) && (o.then ?? src.then) !== 'back');
269
+ const custodial = (src.outcomes ?? []).some(o => (o.then ?? src.then) && (o.then ?? src.then) !== 'back');
100
270
  const rolls = src.rollsOn
101
271
  ? ` 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
272
  : '';
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
- }))];
273
+ const named = {
274
+ grant: 'the grant',
275
+ contest: 'the contest',
276
+ auction: 'the auction',
277
+ mission: 'the mission',
278
+ recipe: 'the recipe',
279
+ box: 'the box',
280
+ };
281
+ const by = [
282
+ ...new Set(
283
+ rollsOf(m)
284
+ .filter(r => r.box === src.key && !r.through)
285
+ .map(r => {
286
+ const [kind, key] = r.by.split(':');
287
+ const part = {
288
+ grant: m.grant,
289
+ lottery: m.lottery,
290
+ contest: m.contest,
291
+ auction: m.auction,
292
+ farm: m.farm,
293
+ recipe: m.recipe,
294
+ box: m.box,
295
+ }[kind]?.get(key);
296
+ return kind === 'lottery'
297
+ ? `${r.route} of the lottery “${part?.name ?? key}”`
298
+ : `${named[r.route] ?? kind} “${part?.name ?? key}”`;
299
+ }),
300
+ ),
301
+ ];
109
302
  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} Anyone asks the oracle for a roll and settles it (the oracle's fee is the asker's), and a roll is never refunded while it waits.`);
303
+ actions.push(
304
+ `“${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 })}.`,
305
+ );
111
306
  continue;
112
307
  }
113
- const verb = boxBuys(src) ? 'Buy' : 'Open';
114
- const oneStep = boxBuys(src) ? ' 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) ? 'It gives' : 'Odds'}: ${outs}${odds.length ? `; ${odds.join('; ')}` : ''}${src.gate ? `. ${gateWords(src.gate, 'open it', m)}` : ''}.${rolls} The money: ${src.outcomes.some(outcomeBurns) ? 'on a burn, all of it is burned; otherwise ' : ''}${splitOf(src.split)}.${limitWords(src)}${src.outcomes.flatMap(outcomeParts).some((o) => o.gives === 'pay') ? ` It sells only while its pools hold every fixed prize${boxBuys(src) ? '' : ', and a prize won is set aside for its winner at once'}.` : ''}${oneStep}`);
308
+ const verb = boxBuys(src, m) ? 'Buy' : 'Open';
309
+ const oneStep = boxBuys(src, m)
310
+ ? ' It sells in one step: the price is split and what it gives handed over in the same transaction.'
311
+ : '';
312
+ actions.push(
313
+ `${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}`,
314
+ );
116
315
  }
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).`);
316
+ for (const p of sim.payments)
317
+ actions.push(
318
+ `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'])}.`,
319
+ );
320
+ for (const r of sim.recipes)
321
+ actions.push(
322
+ `Craft “${r.name}”: ${r.takes.join(' + ') || 'nothing'} → ${r.gives.join(' + ') || 'nothing'} (+ ${fee} SOL player fee).`,
323
+ );
119
324
  for (const l of sim.lotteries) {
120
325
  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')}` : '';
326
+ // Its split (and its cut, a share to the treasury) takes its shares, and the rest goes to the places.
327
+ const rest = keepsPot(src) ? 'into the pot' : 'to the round’s places';
328
+ const others = (src.split ?? []).length
329
+ ? `; ticket money goes ${splitWords(lotterySplit(src), treasuryWords, rest)}`
330
+ : '';
123
331
  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)}.` : ''}`);
332
+ const goes = src.noLoss
333
+ ? 'held until the round is drawn, then goes back to its buyer (no loss)'
334
+ : `${!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)`;
335
+ actions.push(
336
+ `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`}; ${keepsPot(src) ? 'the pot is what the owner seeds and others send it, and what its places give' : 'no ticket money reaches the places, so they give only their prizes'}.${src.gate ? ` ${gateWords(src.gate, 'buy tickets', m)}.` : ''}`,
337
+ );
126
338
  continue;
127
339
  }
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 finish each purchase), 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)}.` : ''}`);
340
+ actions.push(
341
+ `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`}; ${paysWords(src, treasuryWords)}. ${keepsPot(src) ? 'The pot reaches' : 'A round’s money comes to'} 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 ${rest}` : ''}${others}${src.perWallet ? `; at most ${src.perWallet} tickets per wallet a round` : ''}.${src.gate ? ` ${gateWords(src.gate, 'buy tickets', m)}.` : ''}`,
342
+ );
129
343
  }
130
344
  for (const f of sim.farms) {
131
345
  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 for each ${f.unit} at x1, whoever else is in (${f.rate.minted ? 'minted while the mint has room for it, never owed later past that' : `from “${src.rewards}” while it lasts`}); what a seat gives back (a forfeit, an early exit’s waiting rewards, a watched NFT sold, a lapsed upkeep) is ${f.rate.minted ? 'burned' : `sent back to “${src.rewards}”`} in the same move, never to the other seats`
142
- : `${num(f.rewardsPerDay)} ${sym(f.rewardsToken)} a day 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('');
346
+ const tiers = f.tiers.map(t => `x${t.multiplier} for ${t.lock}`).join(', ');
347
+ const boosts = (src.boosts ?? []).map(b => `${effectWords(b.add)} for ${ruleWords(b.when)}`);
348
+ const custody =
349
+ src.stakes !== 'collectibles' && !boosts.length
350
+ ? ''
351
+ : ({
352
+ freeze:
353
+ ' 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.',
354
+ watch:
355
+ ' NFTs stay free in the wallet: one sold while staked stops earning for its staker, and its new holder can take the seat over.',
356
+ }[src.custody] ??
357
+ ' NFTs are handed to the game until they leave; issuer-controlled transfer rules can still prevent return.');
358
+ const earns =
359
+ f.rate && !(Number(f.rate.perDay) > 0)
360
+ ? 'no tokens'
361
+ : f.rate
362
+ ? `${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`
363
+ : `${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` : ''})` : ''}`;
364
+ const grows = (f.growth ?? [])
365
+ .map(
366
+ g =>
367
+ `; 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'}`,
368
+ )
369
+ .join('');
144
370
  const gates = listOf(src.gate);
145
371
  const takes = gates.length ? `; to stake here an NFT must be ${gates.map(ruleWords).join(' and ')}` : '';
146
372
  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 until it is topped up and its seat refreshed, and what it was credited after running dry is not paid: in a farm at a fixed rate it is burned or goes back where the rate came from, never to the other seats; in a farm that shares its pool it goes back to the pool and is released again with the rest` : '';
150
- const missions = f.tiers.filter((t) => t.prize).map((t) => `${t.prize} for staying ${t.lock}`);
151
- const results = (src.tiers ?? []).filter((t) => t.roll).map((t) => {
152
- const box = m.box.get(t.roll);
153
- const fates = box ? [...new Set((box.outcomes ?? []).map((o) => fateWords(o, box, m)))] : [];
154
- 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]})` : ''}`;
155
- });
156
- actions.push(`Stake ${src.stakes === 'collectibles' ? `collectibles${src.item ? ` (${src.item})` : ''}` : sym(src.stakes)} in “${f.name}”: ${earns}${grows}${takes}${runs}${lock}${upkeep}${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}`);
373
+ const runs = src.runs
374
+ ? `; 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}` : ''}`
375
+ : '';
376
+ const lock = src.lockAttribute
377
+ ? `; 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)`
378
+ : '';
379
+ const upkeep = src.upkeepAttribute
380
+ ? `; 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`
381
+ : '';
382
+ // 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.
383
+ const ends = farmEnds(src)
384
+ ? `; 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)`
385
+ : '';
386
+ // A boost that reads a level its own growth writes counts each level from when it is written.
387
+ const refreshes = boostReadsGrowth(src)
388
+ ? '; 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)'
389
+ : '';
390
+ const missions = f.tiers.filter(t => t.prize).map(t => `${t.prize} for staying ${t.lock}`);
391
+ const results = (src.tiers ?? [])
392
+ .filter(t => t.roll)
393
+ .map(t => {
394
+ const box = m.box.get(t.roll);
395
+ const fates = box ? [...new Set((box.outcomes ?? []).map(o => fateWords(o, box, m)))] : [];
396
+ 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]})` : ''}`;
397
+ });
398
+ actions.push(
399
+ `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}`,
400
+ );
157
401
  }
158
- 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}` : ''}`);
402
+ for (const t of m.traits)
403
+ actions.push(
404
+ `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}` : ''}`,
405
+ );
159
406
  for (const ct of sim.contests) {
160
407
  const src = m.contest.get(ct.key);
161
- 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'}.` : '';
408
+ const roll = prizeLine(src?.prize)
409
+ ? ` Each winning entry also gets ${prizeWords(src.prize, m)} when it is claimed${prizeLine(src.prize).gives === 'roll' ? '' : ', in the same transaction'}.`
410
+ : '';
162
411
  if (src.stakeNft) {
163
- 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}`);
412
+ actions.push(
413
+ `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}`,
414
+ );
164
415
  continue;
165
416
  }
166
- 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}`);
417
+ actions.push(
418
+ `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}`,
419
+ );
167
420
  }
168
421
  for (const a of sim.auctions ?? []) {
169
422
  const src = m.auction.get(a.key);
170
- 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)}…`;
423
+ const lot = src.lot?.item
424
+ ? `one new “${m.item.get(src.lot.item)?.name ?? src.lot.item}”`
425
+ : src.lot?.roll
426
+ ? `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})`
427
+ : `the NFT ${src.lot?.asset?.slice(0, 4)}…`;
171
428
  if (src.bidNfts) {
172
- 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)}.` : ''}`);
429
+ actions.push(
430
+ `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)}.` : ''}`,
431
+ );
173
432
  continue;
174
433
  }
175
- 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)}. 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)}.` : ''}`);
434
+ actions.push(
435
+ `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)}.` : ''}`,
436
+ );
176
437
  }
177
- 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.`);
178
- 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.');
438
+ for (const h of m.holds)
439
+ actions.push(
440
+ `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.`,
441
+ );
442
+ if (m.payments.length || m.boxes.length)
443
+ actions.push(
444
+ '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.',
445
+ );
179
446
  for (const g of sim.grants) {
180
447
  const src = m.grant.get(g.key);
181
448
  const l = src.limits ?? {};
182
- 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(', ');
183
- 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);
449
+ const limits = [
450
+ l.perWallet && `${l.perWallet} per wallet`,
451
+ l.perWalletEach && `${l.perWalletEach.count} per wallet every ${l.perWalletEach.every ?? '1d'}`,
452
+ l.perPerson && `${l.perPerson} per person`,
453
+ l.perKey && `${l.perKey} per key`,
454
+ l.perDay && `${l.perDay} a day`,
455
+ l.total && `${l.total} in all`,
456
+ ]
457
+ .filter(Boolean)
458
+ .join(', ');
459
+ const signers = [
460
+ (src.vouchers ?? []).length && 'a voucher key says earned it',
461
+ (src.keys ?? []).length && `one of its ${src.keys.length} keys (codes) signed for`,
462
+ (src.list ?? []).length && `on the list of ${src.list.length}`,
463
+ ].filter(Boolean);
184
464
  const who = signers.length ? signers.join(' or ') : 'that asks (it is open to any wallet)';
185
465
  // 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.
186
466
  const named = signers.length > 0 && src.prize?.gives === 'tokens';
187
- 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.`);
467
+ actions.push(
468
+ `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.`,
469
+ );
188
470
  }
189
471
  for (const st of sim.stashes ?? []) {
190
472
  const src = m.stash.get(st.key);
191
473
  const nfts = stashNfts(src);
192
- const boxes = m.boxes.filter((b) => (b.outcomes ?? []).flatMap(outcomeParts).some((o) => o.gives === 'stash' && o.stash === st.key)).map((b) => `“${b.name}”`);
193
- 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)`);
194
- 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 anyone then settles the roll to hand it over; a prize that names an NFT hands it over in the instruction that wins it. Empty, it can’t be won. 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.` : ''}`);
474
+ const boxes = m.boxes
475
+ .filter(b => (b.outcomes ?? []).flatMap(outcomeParts).some(o => o.gives === 'stash' && o.stash === st.key))
476
+ .map(b => `“${b.name}”`);
477
+ const direct = prizes(m)
478
+ .filter(p => p.prize.gives === 'nft' && p.prize.stash === st.key)
479
+ .map(p => `${p.by} (${prizeWords(p.prize, m)}, given at once)`);
480
+ actions.push(
481
+ `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.` : ''}`,
482
+ );
195
483
  }
196
- // Prizes that are a roll of a box: made for the winner, then asked of the oracle and settled by anyone.
197
- const prized = prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route));
484
+ // 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.
485
+ const prized = prizeRolls(m).filter(r => !['mission', 'recipe'].includes(r.route));
198
486
  if (prized.length)
199
- 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); anyone then asks the oracle for the roll (its fee is the asker’s) and settles it. A roll is never refunded while it waits.`);
487
+ actions.push(
488
+ `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.`,
489
+ );
200
490
  for (const l of m.lotteries) {
201
- const rolls = (l.places ?? []).map(placeOf).map((p, i) => (p.prize ? `place ${i + 1} wins ${prizeWords(p.prize, m)}${p.percent ? ` and ${p.percent}% of the pot` : ''}` : null)).filter(Boolean);
491
+ const rolls = (l.places ?? [])
492
+ .map(placeOf)
493
+ .map((p, i) =>
494
+ p.prize
495
+ ? `${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 ${keepsPot(l) ? 'the pot' : 'the round’s money'}` : ''}`
496
+ : null,
497
+ )
498
+ .filter(Boolean);
202
499
  if (rolls.length) actions.push(`In “${l.name}”, ${rolls.join('; ')}.`);
203
500
  }
204
501
  section('What players do, what it costs and what it is worth', actions);
205
502
 
206
- section('How the pools fill and drain', sim.pools.map((p) => {
207
- 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';
208
- 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)}.`;
209
- }));
210
- section('What runs by itself', [...sim.schedules.map((s) => `“${m.schedules.find((x) => x.key === s.key)?.name ?? s.key}” ${s.says}; anyone can run it when it is due.`), ...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) and pay the places.`)]);
211
- const authority = play.filter((s) => s.signer.startsWith('authority'));
212
- 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}).`));
213
- 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.');
503
+ section(
504
+ 'How the pools fill and drain',
505
+ sim.pools.map(p => {
506
+ const by = rows =>
507
+ rows
508
+ .map(r =>
509
+ r.percentPerDay !== undefined
510
+ ? `${r.by} ${num(r.percentPerDay)}% of what it holds a day`
511
+ : `${r.by} ${num(r.perDay)} a day`,
512
+ )
513
+ .join(', ') || 'nothing';
514
+ 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)}.`;
515
+ }),
516
+ );
517
+ section('What runs on a timer', [
518
+ ...sim.schedules.map(s => {
519
+ const sc = m.schedules.find(x => x.key === s.key);
520
+ 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 ?? {})}.`;
521
+ }),
522
+ ...sim.lotteries.map(
523
+ l =>
524
+ `“${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.`,
525
+ ),
526
+ ]);
527
+ const authority = play.filter(s => s.signer.startsWith('authority'));
528
+ section(
529
+ `What only the authority signs while it runs (${m.authority === 'owner' ? 'your wallet' : `the key ${m.authority}`})`,
530
+ authority.map(s => `${s.says} (${s.part}, ${s.ix}).`),
531
+ );
532
+ if (authority.length)
533
+ lines.push(
534
+ '- 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.',
535
+ );
214
536
 
215
537
  const s = c.setup;
216
538
  lines.push('', 'What setting it up costs');
217
- 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(', ')}` : ''}.`);
539
+ lines.push(
540
+ `- 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${
541
+ Object.keys(s.seeds).length
542
+ ? `, seeds ${Object.entries(s.seeds)
543
+ .map(([k, v]) => `${num(v)} ${k}`)
544
+ .join(', ')}`
545
+ : ''
546
+ }.`,
547
+ );
218
548
  for (const n of s.notes) lines.push(`- ${n}`);
219
549
 
220
550
  const d = sim.perDay;
221
- const list = (map) => Object.entries(map).map(([k, v]) => `${num(v)} ${sym(k)}`).join(', ') || 'nothing';
222
- const named = Object.entries(sim.scenario.perDay).map(([k, v]) => `${v} × ${k}`).join(', ');
551
+ const list = map =>
552
+ Object.entries(map)
553
+ .map(([k, v]) => `${num(v)} ${sym(k)}`)
554
+ .join(', ') || 'nothing';
555
+ const named = Object.entries(sim.scenario.perDay)
556
+ .map(([k, v]) => `${v} × ${k}`)
557
+ .join(', ');
223
558
  const except = unnamedExceptions(sim);
224
- lines.push('', `Each day, if ${named ? `${named} happen (anything else: ${unnamedWords(sim)})` : `${sim.scenario.defaultPerDay} of each thing happen${except.length ? ` (${except.join(', ')})` : ''}`}`);
225
- 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` : ''}.`);
226
- lines.push(`- Your treasury gets ${list(d.creator.treasury)}; burned: ${list(d.burned)}; the platform’s fees: ${num(d.platform.totalSol)} SOL.`);
559
+ lines.push(
560
+ '',
561
+ `Each day, if ${named ? `${named} happen (anything else: ${unnamedWords(sim)})` : `${sim.scenario.defaultPerDay} of each thing happen${except.length ? ` (${except.join(', ')})` : ''}`}`,
562
+ );
563
+ lines.push(
564
+ `- 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` : ''}.`,
565
+ );
566
+ lines.push(
567
+ `- Your treasury gets ${list(d.creator.treasury)}; burned: ${list(d.burned)}; the platform’s fees: ${num(d.platform.totalSol)} SOL.`,
568
+ );
227
569
  for (const n of sim.notes) lines.push(`- ${n}`);
228
570
 
229
- const flagged = verdict.warnings.filter((w) => w.code === 'W-CHANCE-PRIZE' || w.code === 'W-REVENUE-SHARE');
230
- const others = verdict.warnings.filter((w) => !flagged.includes(w));
231
- if (flagged.length) section('Flagged for the law, once it is real money', [...new Set(flagged.map((w) => w.message))]);
232
- section('Know before going live', others.map((w) => `${w.message}${w.fix ? ` ${w.fix}` : ''}`));
233
- 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.');
234
- 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 };
571
+ const flagged = verdict.warnings.filter(w => w.code === 'W-CHANCE-PRIZE' || w.code === 'W-REVENUE-SHARE');
572
+ const others = verdict.warnings.filter(w => !flagged.includes(w));
573
+ if (flagged.length) section('Flagged for the law, once it is real money', [...new Set(flagged.map(w => w.message))]);
574
+ section(
575
+ 'Know before going live',
576
+ others.map(w => `${w.message}${w.fix ? ` ${w.fix}` : ''}`),
577
+ );
578
+ lines.push(
579
+ '',
580
+ '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.',
581
+ );
582
+ return {
583
+ ok: true,
584
+ text: lines.join('\n').trim(),
585
+ warnings: verdict.warnings,
586
+ needsAuthority: authority.length > 0,
587
+ chance: verdict.warnings.some(w => w.code === 'W-CHANCE-PRIZE'),
588
+ setupSol: s.totalSol,
589
+ };
235
590
  }