@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,36 +3,67 @@
3
3
  //
4
4
  // Rent is (bytes + 128) × 5,080 lamports (model.mjs); the loot program's own records are sized byte for byte from
5
5
  // their Borsh layout, others (a Core collection, a farm) roughly, and marked so.
6
- import { plan as compile, contestBytes } from './plan.mjs';
7
- import { ACCOUNT_BYTES } from '../client/index.js';
8
- import { boxBuys, boxRolls, COLLECTIBLE_RENT, outcomeParts, ESCROW_LAMPORTS, FEES, LAMPORTS_PER_SOL, ORACLE_FEE, RENT_PER_BYTE, SIGNATURE_FEE, model, prizeLine, prizeRolls, prizes, prizeWords, rent, toUnits } from './model.mjs';
6
+ import { boxOrderBytes, plan as compile, contestBytes, farmEnds, roundRentOf } from './plan.mjs';
7
+ import {
8
+ ACCOUNT_BYTES,
9
+ CONTEST_ENTRY_BYTES,
10
+ END_BYTES,
11
+ entryWidth,
12
+ LIMIT_RECORD_BYTES,
13
+ ROUND_RECORD_BYTES,
14
+ } from '../client/index.js';
15
+ import {
16
+ boxBuys,
17
+ COLLECTIBLE_RENT,
18
+ drawsOf,
19
+ settleUnits,
20
+ holdsRent,
21
+ outcomeParts,
22
+ SETTLE_RENT_UNIT,
23
+ FEES,
24
+ LAMPORTS_PER_SOL,
25
+ ORACLE_FEE,
26
+ RENT_PER_BYTE,
27
+ SIGNATURE_FEE,
28
+ model,
29
+ lotterySplit,
30
+ prizeLine,
31
+ prizeRolls,
32
+ prizes,
33
+ prizeWords,
34
+ rent,
35
+ toUnits,
36
+ } from './model.mjs';
9
37
  import { inAll, round, simulate } from './simulate.mjs';
10
38
 
11
39
  // The options of a call made without any: read-only, and typed so the generated declarations take any option.
12
40
  const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
13
41
 
14
- const sol = (lamports) => round(Number(lamports) / Number(LAMPORTS_PER_SOL));
42
+ const sol = lamports => round(Number(lamports) / Number(LAMPORTS_PER_SOL));
15
43
  /** Setup steps that are no loot instruction, and so charge no fee: a token minted, the collection made, SOL put in a
16
44
  * 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:)/;
45
+ const NO_FEE_STEP = /^(token:|collection$|fund:|rent:)/;
18
46
  /** Setup cost from a plan (plan.mjs `plan`). */
19
47
  export function setupCost(plan, economy) {
20
48
  const m = model(economy);
21
49
  // 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)) }));
50
+ const rentOf = c => (c.grows ? BigInt(c.bytes) * RENT_PER_BYTE : rent(c.bytes));
51
+ const accounts = plan.transactions
52
+ .flatMap(t => t.steps)
53
+ .flatMap(s => s.creates ?? [])
54
+ .map(c => ({ ...c, lamports: rentOf(c).toString(), sol: sol(rentOf(c)) }));
24
55
  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));
56
+ const charged = plan.transactions.flatMap(t => t.steps).filter(s => !NO_FEE_STEP.test(s.id));
26
57
  const feeLamports = BigInt(charged.length) * FEES.project;
27
58
  const networkLamports = plan.transactions.reduce((n, t) => n + BigInt(t.signers.length) * SIGNATURE_FEE, 0n);
28
59
  // 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));
60
+ const steps = new Set(plan.transactions.flatMap(t => t.steps).map(s => s.id));
30
61
  const seeds = {};
31
62
  let seedLamports = 0n;
32
- const funded = m.grants.filter((g) => Number(g.fund ?? 0) > 0 && steps.has(`fund:${g.key}`));
63
+ const funded = m.grants.filter(g => Number(g.fund ?? 0) > 0 && steps.has(`fund:${g.key}`));
33
64
  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 sealed schedule's
35
- // funds (into its vault, from the wallet).
65
+ // 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
66
+ // rewards vault), a sealed schedule's funds (into its vault, from the wallet).
36
67
  const putIn = (key, amount, step) => {
37
68
  if (!(Number(amount ?? 0) > 0) || !steps.has(step)) return;
38
69
  const t = m.tokens.get(key ?? m.currencyKey);
@@ -41,12 +72,27 @@ export function setupCost(plan, economy) {
41
72
  };
42
73
  for (const p of m.pools) putIn(p.currency, p.seed, `seed:${p.key}`);
43
74
  for (const l of m.lotteries) putIn(l.currency, l.seed, `seed:lottery:${l.key}`);
44
- for (const sc of m.schedules) if (sc.sealed && (sc.fundFrom ?? 'wallet') === 'wallet') putIn(sc.currency, sc.fund, `fund:${sc.key}`);
75
+ for (const f of m.farms) putIn(f.rewardsToken, f.seed, `seed:farm:${f.key}`);
76
+ // SOL a lottery holds for its rounds' rent: lent to each round, back at its close.
77
+ for (const l of m.lotteries) if (steps.has(`rent:lottery:${l.key}`)) seedLamports += roundRentOf(l, m);
78
+ for (const sc of m.schedules)
79
+ if (sc.sealed && (sc.fundFrom ?? 'wallet') === 'wallet') putIn(sc.currency, sc.fund, `fund:${sc.key}`);
45
80
  const total = rentLamports + feeLamports + networkLamports + seedLamports;
46
81
  const notes = [];
47
- if (accounts.some((a) => a.approx)) notes.push('Accounts marked “about” are sized roughly (a Core collection or an external token account).');
48
- 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’ and lotteries’ seeds, and sealed schedules’ funds.');
49
- 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.');
82
+ if (accounts.some(a => a.approx))
83
+ notes.push('Accounts marked “about” are sized roughly (a Core collection or an external token account).');
84
+ if (Object.entries(seeds).some(([symbol]) => [...m.tokens.values()].some(t => t.wsol && t.symbol === symbol)))
85
+ notes.push(
86
+ 'The total includes the SOL you put in: pools’, lotteries’ and farms’ seeds, and sealed schedules’ funds.',
87
+ );
88
+ if (m.lotteries.some(l => steps.has(`rent:lottery:${l.key}`)))
89
+ notes.push(
90
+ '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.',
91
+ );
92
+ if (funded.length)
93
+ notes.push(
94
+ 'The total includes the SOL put in grants’ funds, which pays their claims; what is left goes back to you when a grant closes.',
95
+ );
50
96
  notes.push('The network fee is 5,000 lamports a signature; a wallet may add a priority fee of its own.');
51
97
  return {
52
98
  accounts,
@@ -64,80 +110,356 @@ export function setupCost(plan, economy) {
64
110
  /**
65
111
  * What each action costs whoever signs it while the economy runs: games.fun's fee, the network's, and for anything
66
112
  * that asks MagicBlock's oracle for a random number (a box's open, a lottery's draw, the authority's `roll`) the
67
- * oracle's own 0.0005 SOL (`oracleSol`, model.mjs ORACLE_FEE).
113
+ * oracle's own fee (`oracleSol`, model.mjs ORACLE_FEE).
68
114
  */
69
115
  export function actionCosts(economy, { now } = NO_OPTIONS) {
70
116
  const m = model(economy);
71
117
  // A player's real cost of one action, from the simulation (simulate.mjs realCostOf): the number to quote.
72
118
  const sim = simulate(economy, { now });
73
- 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])]);
119
+ const real = new Map([
120
+ ...sim.items.map(x => [`item:${x.key}`, x.realCost]),
121
+ ...sim.boxes.map(x => [`box:${x.key}`, x.realCost]),
122
+ ...sim.payments.map(x => [`payment:${x.key}`, x.realCost]),
123
+ ...sim.lotteries.map(x => [`lottery:${x.key}`, x.realCost]),
124
+ ]);
74
125
  const rows = [];
75
126
  const row = (part, action, who, fee, deposits = [], extra, oracle = 0n) => {
76
127
  const r = real.get(part);
77
- 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) } : {}) });
128
+ rows.push({
129
+ part,
130
+ action,
131
+ who,
132
+ feeSol: sol(fee),
133
+ ...(oracle ? { oracleSol: sol(oracle) } : {}),
134
+ networkSol: sol(SIGNATURE_FEE),
135
+ deposits,
136
+ ...(extra ? { note: extra } : {}),
137
+ ...(r ? { inAllSol: r.totalSol ?? null, onTopSol: r.extrasSol, holdSol: r.holdSol, inAll: inAll(r) } : {}),
138
+ });
78
139
  };
79
140
  const back = (what, lamports, when) => ({ what, sol: sol(lamports), back: when });
80
- // An order holds a collectible's rent for each item the richest outcome gives (order.rs `escrow`).
81
- const itemsOf = (b) => Math.max(0, ...(b.outcomes ?? []).map((o) => outcomeParts(o).filter((p) => p.gives === 'item').length));
82
- const order = (b) => [
83
- back('the order', rent(ACCOUNT_BYTES.Order), 'at settle or refund'),
84
- ...(itemsOf(b) ? [back(`the SOL the order holds for ${itemsOf(b) === 1 ? 'a collectible’s' : `${itemsOf(b)} collectibles’`} rent (the player fee is paid on top of it)`, ESCROW_LAMPORTS * BigInt(itemsOf(b)), `at settle, less each collectible’s rent (about ${sol(COLLECTIBLE_RENT)} SOL) it gives; all of it, and the fee, on a refund`)] : []),
141
+ // An order holds its rent and the fee; the open lends its throwaway settle key what the settle may open or mint (a
142
+ // collectible's rent for each item the richest outcome gives, each card of a pack), back unspent.
143
+ const itemsOf = b =>
144
+ drawsOf(b) * Math.max(0, ...(b.outcomes ?? []).map(o => outcomeParts(o).filter(p => holdsRent(p, m)).length));
145
+ const order = b => [
146
+ back('the order', rent(boxOrderBytes(b, m, 1, { opened: true })), 'at settle or refund'),
147
+ ...(settleUnits(b, m)
148
+ ? [
149
+ back(
150
+ `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)`,
151
+ SETTLE_RENT_UNIT * BigInt(settleUnits(b, m)),
152
+ `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`,
153
+ ),
154
+ ]
155
+ : []),
85
156
  ];
86
157
  // A box that needs no roll is bought in one step (`buy`): no order, and a collectible's rent is paid at once.
87
- 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)`)] : []);
88
- 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.');
158
+ const bought = b =>
159
+ itemsOf(b)
160
+ ? [
161
+ back(
162
+ `${itemsOf(b)} new collectible${itemsOf(b) > 1 ? 's' : ''}`,
163
+ COLLECTIBLE_RENT * BigInt(itemsOf(b)),
164
+ `never: it is the collectible’s own rent (about ${sol(COLLECTIBLE_RENT)} SOL each)`,
165
+ ),
166
+ ]
167
+ : [];
168
+ for (const b of m.shop)
169
+ row(
170
+ `item:${b.shopOf}`,
171
+ `buy “${b.name}”`,
172
+ 'player',
173
+ FEES.player,
174
+ bought(b),
175
+ 'One transaction: the price is split and the item given at once.',
176
+ );
89
177
  for (const b of m.boxes) {
90
- if (boxBuys(b)) 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.');
91
- else row(`box:${b.key}`, `open “${b.name}”`, 'player', FEES.player, order(b), boxRolls(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.' : undefined, boxRolls(b) ? ORACLE_FEE : 0n);
178
+ if (boxBuys(b, m))
179
+ row(
180
+ `box:${b.key}`,
181
+ `buy “${b.name}”`,
182
+ 'player',
183
+ FEES.player,
184
+ bought(b),
185
+ 'One transaction: the price is split and what it gives handed over at once.',
186
+ );
187
+ else
188
+ row(
189
+ `box:${b.key}`,
190
+ `open “${b.name}”`,
191
+ 'player',
192
+ FEES.player,
193
+ order(b),
194
+ 'The oracle’s fee is paid by whoever asks for the roll (the player, at the open) and a refund doesn’t return it.',
195
+ ORACLE_FEE,
196
+ );
92
197
  }
93
198
  for (const p of m.payments) row(`payment:${p.key}`, `pay “${p.name}”`, 'player', FEES.player);
94
199
  for (const r of m.recipes) {
95
- const made = (r.out ?? []).filter((l) => l.gives === 'item' && !l.compressed).length;
96
- 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)`)] : []);
200
+ const made = (r.out ?? []).filter(l => l.gives === 'item' && !l.compressed).length;
201
+ row(
202
+ `recipe:${r.key}`,
203
+ `craft “${r.name}”`,
204
+ 'player',
205
+ FEES.player,
206
+ made
207
+ ? [
208
+ back(
209
+ `${made} new collectible${made > 1 ? 's' : ''}`,
210
+ COLLECTIBLE_RENT * BigInt(made),
211
+ `never: it is the collectible’s own rent (about ${sol(COLLECTIBLE_RENT)} SOL each)`,
212
+ ),
213
+ ]
214
+ : [],
215
+ );
216
+ }
217
+ for (const l of m.lotteries) {
218
+ // A purchase adds its entry's bytes to the round. In a round the lottery opened, the SOL it holds for its rounds
219
+ // (`roundRent`, on by default) pays them, back to it at the close: the buyer pays only the fee and the price. Without
220
+ // it, or once it runs short, the buyer pays them (to the treasury at the close), and a round's first buyer fronts its
221
+ // header. A share the buyer chooses makes each entry keep what arrived.
222
+ const chosen = lotterySplit(l).some(s => String(s.to).startsWith('chosen'));
223
+ const entry = BigInt(entryWidth(chosen, Boolean(l.ticketNft))) * RENT_PER_BYTE;
224
+ const held = roundRentOf(l, m);
225
+ const funded = held > 0n;
226
+ row(
227
+ `lottery:${l.key}`,
228
+ `buy tickets for “${l.name}”`,
229
+ 'player',
230
+ FEES.player,
231
+ funded
232
+ ? []
233
+ : [
234
+ back(
235
+ 'the purchase’s entry in the round (its bytes)',
236
+ entry,
237
+ 'never: it goes to the treasury when the round closes',
238
+ ),
239
+ ],
240
+ funded
241
+ ? `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.`
242
+ : '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.',
243
+ );
97
244
  }
98
- for (const l of m.lotteries) row(`lottery:${l.key}`, `buy tickets for “${l.name}”`, 'player', FEES.player, [back('the purchase’s record (its tickets)', rent(ACCOUNT_BYTES.LotteryContribution), 'when the round is done with it: drawn and its place paid, or refunded')]);
99
245
  for (const f of m.farms) {
100
- row(`farm:${f.key}`, `stake in “${f.name}”`, 'player', FEES.player, [back('the seat', rent(ACCOUNT_BYTES.Seat), 'when they take everything out')]);
101
- row(`farm:${f.key}`, `unstake or harvest from “${f.name}”`, 'player', FEES.player, [], [
102
- (f.tiers ?? []).some((t) => t.exitFee) && 'Leaving a tier before its lock leaves its exit fee of what is taken out.',
103
- f.unbond && `What is taken out waits ${f.unbond} first; a later unstake (or withdraw) takes it out.`,
104
- f.toll && `${f.toll.percent}% of every payout of rewards goes ${f.toll.to === 'burn' ? 'to a burn' : `to ${String(f.toll.to).slice(5)}`}.`,
105
- ].filter(Boolean).join(' ') || undefined);
246
+ // A seat that can stop earning writes its end in the farm's record: a new time costs whoever writes it, spare rent first.
247
+ const endRent = BigInt(END_BYTES) * RENT_PER_BYTE;
248
+ row(
249
+ `farm:${f.key}`,
250
+ `stake in “${f.name}”`,
251
+ 'player',
252
+ FEES.player,
253
+ [
254
+ back('the seat', rent(ACCOUNT_BYTES.Seat), 'when they take everything out'),
255
+ ...(farmEnds(f)
256
+ ? [
257
+ back(
258
+ `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)`,
259
+ endRent,
260
+ 'never: it stays with the record for the next time written',
261
+ ),
262
+ ]
263
+ : []),
264
+ ],
265
+ farmEnds(f)
266
+ ? 'Whatever writes a new end time (a stake, a booster put on, a refresh that reads new upkeep) pays its bytes, its sender’s.'
267
+ : undefined,
268
+ );
269
+ row(
270
+ `farm:${f.key}`,
271
+ `unstake or harvest from “${f.name}”`,
272
+ 'player',
273
+ FEES.player,
274
+ [],
275
+ [
276
+ (f.tiers ?? []).some(t => t.exitFee) &&
277
+ 'Leaving a tier before its lock leaves its exit fee of what is taken out, and keeps the rewards the seat was credited.',
278
+ ]
279
+ .filter(Boolean)
280
+ .join(' ') || undefined,
281
+ );
106
282
  }
107
- 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 ends')]);
283
+ for (const h of m.holds)
284
+ row(`hold:${h.key}`, `hand in for “${h.name}”`, 'player', FEES.player, [
285
+ back('the hold', rent(ACCOUNT_BYTES.Hold), 'when it is released'),
286
+ ]);
108
287
  // What giving a prize opens, paid by whoever sends the instruction that gives it (prize.rs).
109
- 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] ?? '';
110
- 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; a first claim’s receipt rent${opens(g.prize ?? g.gives)} comes from the grant’s funds (the sender pays what they can’t): xsolana_simulate gives what to put in its funds per claim.`);
288
+ const opens = p =>
289
+ prizeLine(p)?.compressed
290
+ ? ', and the compressed NFT, which has no rent'
291
+ : ({
292
+ item: ', plus the collectible’s rent',
293
+ tokens: ', plus the winner’s token account for the prize when they have none',
294
+ share: ', plus the winner’s token account for the prize when they have none',
295
+ nft: ', and the NFT is handed over in the claim (a Token Metadata NFT opens the winner’s token account of it)',
296
+ roll: ', plus the roll’s order (below)',
297
+ }[prizeLine(p)?.gives] ?? '');
298
+ for (const g of m.grants)
299
+ row(
300
+ `grant:${g.key}`,
301
+ `claim “${g.name}”`,
302
+ 'the winner, who signs every claim (anyone may pay its fees)',
303
+ FEES.player,
304
+ [],
305
+ `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.`,
306
+ );
111
307
  // A prize of a collectible given when a place is paid or a winning entry claimed: its rent, from whoever sends it.
112
- for (const p of prizes(m).filter((p) => p.prize.gives === 'item' && !p.prize.compressed && !['grant', 'recipe'].includes(p.route)))
113
- 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')]);
308
+ for (const p of prizes(m).filter(
309
+ p => p.prize.gives === 'item' && !p.prize.compressed && !['grant', 'recipe'].includes(p.route),
310
+ ))
311
+ row(
312
+ p.by,
313
+ `give ${prizeWords(p.prize, m)} (${p.route})`,
314
+ p.route === 'mission'
315
+ ? 'the player leaving the mission'
316
+ : p.route === 'contest'
317
+ ? 'whoever claims the winning entry'
318
+ : 'whoever pays the place',
319
+ 0n,
320
+ [back('the new collectible', COLLECTIBLE_RENT, 'never: it is the collectible’s own rent')],
321
+ );
114
322
  for (const c of m.contests) {
115
- row(`contest:${c.key}`, `back a side in “${c.name}”`, 'player', FEES.player, [back('the entry', rent(ACCOUNT_BYTES.Entry), 'at claim')]);
116
- 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')]);
323
+ row(
324
+ `contest:${c.key}`,
325
+ `back a side in “${c.name}”`,
326
+ 'player',
327
+ FEES.player,
328
+ [
329
+ back(
330
+ 'the entry’s bytes in the contest’s list',
331
+ BigInt(c.stakeNft ? CONTEST_ENTRY_BYTES.nfts : CONTEST_ENTRY_BYTES.tokens) * RENT_PER_BYTE,
332
+ 'never: they go to the treasury when the contest closes',
333
+ ),
334
+ ],
335
+ 'A loser of a contest of tokens sends nothing more; every other entry is paid by a claim anyone may send.',
336
+ );
337
+ row(`contest:${c.key}`, `open “${c.name}”`, 'authority', FEES.project, [
338
+ back('the contest and its pot', rent(contestBytes(c, m)) + rent(165), 'when it closes'),
339
+ ]);
117
340
  row(`contest:${c.key}`, `decide “${c.name}”`, 'authority', FEES.project);
118
341
  }
119
342
  for (const a of m.auctions) {
120
- 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.');
343
+ row(
344
+ `auction:${a.key}`,
345
+ `bid on “${a.name}”`,
346
+ 'player',
347
+ FEES.player,
348
+ [],
349
+ 'Nothing to deposit: the bid waits in the game’s vault, and goes back at once when it is topped.',
350
+ );
121
351
  const minted = a.lot?.item && (m.item.get(a.lot.item)?.kind ?? 'collectible') === 'collectible';
122
- 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.');
352
+ row(
353
+ `auction:${a.key}`,
354
+ `end “${a.name}”`,
355
+ 'anyone (the winner, a keeper)',
356
+ 0n,
357
+ minted
358
+ ? [
359
+ back(
360
+ 'the new collectible',
361
+ COLLECTIBLE_RENT,
362
+ 'never: it is the collectible’s own rent, paid by whoever ends it',
363
+ ),
364
+ ]
365
+ : [],
366
+ 'Its own rent goes back to whoever opened it.',
367
+ );
123
368
  }
124
- 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')]);
369
+ for (const s of m.stashes)
370
+ row(`stash:${s.key}`, `put an NFT in “${s.name}”, or take one out`, 'the owner or the authority', FEES.project, [
371
+ back(
372
+ '32 bytes more of the stash for each NFT in it',
373
+ 32n * RENT_PER_BYTE,
374
+ 'to whoever takes one out; a roll leaves it with the stash, for the next one put in',
375
+ ),
376
+ ]);
125
377
  // 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.
126
- for (const r of prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route))) {
378
+ for (const r of prizeRolls(m).filter(r => !['mission', 'recipe'].includes(r.route))) {
127
379
  const name = m.box.get(r.box)?.name ?? r.box;
128
- 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';
129
- // The order holds the SOL for a collectible's rent only when its box can give one (an item outcome).
130
- const mints = (m.box.get(r.box)?.outcomes ?? []).flatMap(outcomeParts).some((o) => o.gives === 'item');
131
- row(r.by, `make a winner’s roll of “${name}” (${r.route})`, who, 0n, [back(mints ? 'the roll’s order and the SOL it holds for a collectible' : 'the roll’s order', rent(ACCOUNT_BYTES.Order) + (mints ? ESCROW_LAMPORTS : 0n), mints ? 'to the winner when it is settled, less a collectible’s rent when it gives one' : 'to the winner when it is settled')], 'Then anyone asks the oracle for the roll (its fee, below) and anyone settles it.');
132
- row(r.by, `ask for the roll of “${name}”`, 'anyone (a keeper, or the winner)', 0n, [], 'It asks MagicBlock’s oracle, which takes its fee from whoever asks; a refund doesn’t return it.', ORACLE_FEE);
380
+ const who =
381
+ r.route === 'grant'
382
+ ? 'the grant’s funds (the sender, what they can’t)'
383
+ : r.route === 'auction'
384
+ ? 'whoever ends it'
385
+ : r.route === 'contest'
386
+ ? 'whoever claims the winning entry'
387
+ : 'whoever pays the place';
388
+ // Whoever settles it pays a collectible's rent when its box gives one (an item outcome).
389
+ const mints = (m.box.get(r.box)?.outcomes ?? []).flatMap(outcomeParts).some(o => holdsRent(o, m));
390
+ row(
391
+ r.by,
392
+ `make a winner’s roll of “${name}” (${r.route})`,
393
+ who,
394
+ 0n,
395
+ [back('the roll’s order', rent(boxOrderBytes(m.box.get(r.box), m)), 'to the winner when it is settled')],
396
+ `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)` : ''}.`,
397
+ );
398
+ row(
399
+ r.by,
400
+ `ask for the roll of “${name}”`,
401
+ 'the winner or the project’s keys',
402
+ 0n,
403
+ [],
404
+ 'It asks MagicBlock’s oracle, which takes its fee from whoever asks; a refund doesn’t return it.',
405
+ ORACLE_FEE,
406
+ );
133
407
  }
134
- 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);
135
- if (m.allBoxes.some((b) => !boxBuys(b))) row('orders', 'settle an order', 'anyone (a keeper, or the player)', 0n, [], 'A roll won (an outcome that gives one) makes its order now: the sender pays its rent, which goes to the player when it settles.');
136
- if (m.schedules.length) row('schedules', 'run a schedule when it is due', 'anyone (a sealed one’s recipient, from their page)', 0n, [], m.schedules.some((sc) => sc.sealed) ? 'Several runs that fell behind go in one transaction; a sealed schedule pays from its own vault.' : undefined);
408
+ for (const d of m.decisions)
409
+ row(
410
+ `decision:${d.key}`,
411
+ d.when,
412
+ 'authority',
413
+ FEES.project,
414
+ [],
415
+ d.does === 'roll' ? 'The roll asks MagicBlock’s oracle, which takes its own fee from the authority.' : undefined,
416
+ d.does === 'roll' ? ORACLE_FEE : 0n,
417
+ );
418
+ if (m.allBoxes.some(b => !boxBuys(b, m)))
419
+ row(
420
+ 'orders',
421
+ 'settle an order',
422
+ 'the player, a key they named, or the project’s keys',
423
+ 0n,
424
+ [],
425
+ '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.',
426
+ );
427
+ if (m.schedules.length)
428
+ row(
429
+ 'schedules',
430
+ 'run a schedule when it is due',
431
+ 'the project’s keys, or who it pays (a sealed one’s recipient, from their page)',
432
+ 0n,
433
+ [],
434
+ `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.' : ''}`,
435
+ );
137
436
  if (m.lotteries.length) {
138
- row('lotteries', 'ask for a draw', 'anyone (a keeper, or a player)', 0n, [], 'The draw asks MagicBlock’s oracle for the winning numbers; the oracle takes its fee from whoever sends the draw.', ORACLE_FEE);
139
- row('lotteries', 'give a place’s prize, then route the tickets, then pay a place', 'anyone (the winner’s page)', 0n, [], 'A drawn round gives every place’s prize first; its ticket money moves only after (prizes first, then money).');
140
- 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.');
437
+ row(
438
+ 'lotteries',
439
+ 'ask for a draw',
440
+ 'anyone (a keeper, or a player)',
441
+ 0n,
442
+ [],
443
+ '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.',
444
+ ORACLE_FEE,
445
+ );
446
+ row(
447
+ 'lotteries',
448
+ 'pull a part: a refund, a place’s part of the pot or its prize, a share owed in common',
449
+ 'anyone (the winner’s page, a keeper)',
450
+ 0n,
451
+ [],
452
+ '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.',
453
+ );
454
+ if (m.lotteries.some(l => l.minSold))
455
+ row(
456
+ 'lotteries',
457
+ 'a short round',
458
+ 'anyone',
459
+ 0n,
460
+ [],
461
+ 'A round that sold fewer tickets than its least is cancelled at its draw, with no oracle fee, and every ticket refunded.',
462
+ );
141
463
  }
142
464
  return rows;
143
465
  }
@@ -147,23 +469,40 @@ export function actionCosts(economy, { now } = NO_OPTIONS) {
147
469
  export function cost(economy, { now = Math.floor(Date.now() / 1000), only = null, live = null } = {}) {
148
470
  const plan = compile(economy, { now, only, live });
149
471
  if (!plan.ok) return plan;
150
- return { ok: true, setup: setupCost(plan, economy), actions: actionCosts(economy, { now }), fees: { playerSol: sol(FEES.player), projectSol: sol(FEES.project), oracleSol: sol(ORACLE_FEE) } };
472
+ return {
473
+ ok: true,
474
+ setup: setupCost(plan, economy),
475
+ actions: actionCosts(economy, { now }),
476
+ fees: { playerSol: sol(FEES.player), projectSol: sol(FEES.project), oracleSol: sol(ORACLE_FEE) },
477
+ };
151
478
  }
152
479
 
153
480
  /** The cost in plain words. */
154
481
  export function explainCost(c) {
155
482
  const s = c.setup;
156
- 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:`];
157
- 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(', ')}` : ''}.`);
483
+ const lines = [
484
+ `Going live takes ${s.transactions} transaction${s.transactions === 1 ? '' : 's'} (${s.approvals} wallet approval${s.approvals === 1 ? '' : 's'}) and about ${s.totalSol} SOL:`,
485
+ ];
486
+ lines.push(
487
+ ` rent ${s.rentSol} SOL for ${s.accounts.length} accounts; program fees ${s.programFeesSol} SOL; network fees ${s.networkFeesSol} SOL${
488
+ Object.keys(s.seeds).length
489
+ ? `; seeds ${Object.entries(s.seeds)
490
+ .map(([k, v]) => `${v} ${k}`)
491
+ .join(', ')}`
492
+ : ''
493
+ }.`,
494
+ );
158
495
  for (const a of s.accounts) lines.push(` ${a.approx ? 'about ' : ''}${a.sol} SOL: ${a.what} (${a.bytes} bytes)`);
159
496
  lines.push('While it runs (for a player, quote the “in all” number: it is what leaves their wallet for good):');
160
497
  for (const r of c.actions) {
161
- const deposits = r.deposits.map((d) => `${d.sol} SOL for ${d.what} (back: ${d.back})`).join('; ');
498
+ const deposits = r.deposits.map(d => `${d.sol} SOL for ${d.what} (back: ${d.back})`).join('; ');
162
499
  if (r.inAll) {
163
500
  lines.push(` ${r.action} (${r.who}): ${r.inAll}${r.note ? `. ${r.note}` : ''}`);
164
501
  continue;
165
502
  }
166
- 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}` : ''}`);
503
+ lines.push(
504
+ ` ${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}` : ''}`,
505
+ );
167
506
  }
168
507
  for (const n of s.notes) lines.push(n);
169
508
  return lines.join('\n');