@xgamesfun/solana 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/DISCLAIMER.md +1 -1
  2. package/NOTICE +4 -4
  3. package/README.md +100 -52
  4. package/bin/xsolana.mjs +32 -11
  5. package/examples/blind-forge.json +2 -4
  6. package/examples/coin-flip-arena.json +2 -5
  7. package/examples/duel-arena.json +1 -4
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/hero-trials.json +14 -2
  10. package/examples/ideas.json +788 -788
  11. package/examples/nft-raffle.json +1 -0
  12. package/examples/partner-vault.json +19 -3
  13. package/examples/sacrifice-raffle.json +2 -10
  14. package/examples/shelter-fair.json +1 -0
  15. package/examples/stakers-draw.json +4 -7
  16. package/examples/trade-in-shop.json +2 -3
  17. package/js/approve/details.mjs +117 -48
  18. package/js/approve/ed25519.mjs +5 -1
  19. package/js/approve/live-check.mjs +83 -26
  20. package/js/approve/page.html +217 -78
  21. package/js/approve/page.js +43 -15
  22. package/js/approve/server.mjs +276 -74
  23. package/js/chain/base58.js +7 -1
  24. package/js/chain/borsh.js +15 -11
  25. package/js/chain/bytes.js +11 -8
  26. package/js/chain/core.js +21 -5
  27. package/js/chain/curve.js +1 -1
  28. package/js/chain/lookup.js +118 -36
  29. package/js/chain/mint.js +6 -2
  30. package/js/chain/pda.js +2 -2
  31. package/js/chain/programs.js +6 -6
  32. package/js/chain/rpc.js +40 -14
  33. package/js/chain/token.js +13 -2
  34. package/js/chain/token2022.js +55 -16
  35. package/js/chain/tx.js +32 -22
  36. package/js/client/accounts.js +270 -80
  37. package/js/client/compressed.js +4 -3
  38. package/js/client/grant.js +165 -40
  39. package/js/client/idl/loot.js +1 -1
  40. package/js/client/idl/loot.json +104 -16
  41. package/js/client/ix.js +2121 -457
  42. package/js/client/pda.js +20 -18
  43. package/js/client/policy.js +12 -5
  44. package/js/client/random.js +2 -2
  45. package/js/client/sizes.js +220 -65
  46. package/js/client/traits.js +14 -5
  47. package/js/connect/connect.mjs +59 -17
  48. package/js/economy/check.mjs +3884 -690
  49. package/js/economy/cost.mjs +381 -57
  50. package/js/economy/examples.mjs +176 -35
  51. package/js/economy/explain.mjs +468 -110
  52. package/js/economy/guide.mjs +282 -59
  53. package/js/economy/legal.mjs +12 -6
  54. package/js/economy/live.mjs +62 -20
  55. package/js/economy/model.mjs +320 -97
  56. package/js/economy/plan.mjs +1022 -203
  57. package/js/economy/review.mjs +760 -0
  58. package/js/economy/schema.json +134 -372
  59. package/js/economy/secrets.mjs +17 -7
  60. package/js/economy/simulate.mjs +771 -178
  61. package/js/economy/trust.mjs +15 -0
  62. package/js/economy/txsize.mjs +15 -3
  63. package/js/economy/validate.mjs +61 -24
  64. package/js/index.js +1 -0
  65. package/js/mcp/server.mjs +43 -15
  66. package/js/mcp/tools.mjs +318 -80
  67. package/js/play/index.js +1960 -461
  68. package/js/play/keys.js +35 -9
  69. package/js/results/index.js +225 -32
  70. package/js/results/server.js +230 -0
  71. package/js/results/wire.js +64 -16
  72. package/package.json +6 -4
  73. package/release.json +5 -5
  74. package/skills/xsolana-economy/SKILL.md +52 -22
  75. package/skills/xsolana-economy/references/after-launch.md +32 -21
  76. package/skills/xsolana-economy/references/apps.md +47 -36
  77. package/skills/xsolana-economy/references/sentence-to-spec.md +149 -129
  78. package/skills/xsolana-economy/references/telling-the-person.md +15 -6
  79. package/skills/xsolana-economy/references/trust-and-cheating.md +126 -0
  80. package/types/chain/core.d.ts +1 -1
  81. package/types/chain/lookup.d.ts +1 -1
  82. package/types/client/accounts.d.ts +24 -3
  83. package/types/client/grant.d.ts +2 -2
  84. package/types/client/idl/loot.d.ts +22 -0
  85. package/types/client/ix.d.ts +54 -45
  86. package/types/client/pda.d.ts +3 -1
  87. package/types/economy/cost.d.mts +1 -1
  88. package/types/economy/explain.d.mts +3 -0
  89. package/types/economy/guide.d.mts +5 -0
  90. package/types/economy/model.d.mts +48 -3
  91. package/types/economy/plan.d.mts +44 -2
  92. package/types/economy/review.d.mts +45 -0
  93. package/types/economy/trust.d.mts +8 -0
  94. package/types/index.d.ts +1 -0
  95. package/types/mcp/tools.d.mts +62 -0
  96. package/types/play/index.d.ts +14 -25
  97. package/types/results/index.d.ts +8 -7
  98. package/types/results/server.d.ts +126 -0
  99. package/types/results/wire.d.ts +1 -1
@@ -4,14 +4,42 @@
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
6
  import { boxOrderBytes, plan as compile, contestBytes, farmEnds, roundRentOf } from './plan.mjs';
7
- import { ACCOUNT_BYTES, CONTEST_ENTRY_BYTES, END_BYTES, entryWidth, LIMIT_RECORD_BYTES, ROUND_RECORD_BYTES } from '../client/index.js';
8
- import { boxBuys, COLLECTIBLE_RENT, drawsOf, settleUnits, holdsRent, outcomeParts, SETTLE_RENT_UNIT, FEES, LAMPORTS_PER_SOL, ORACLE_FEE, RENT_PER_BYTE, SIGNATURE_FEE, model, lotterySplit, prizeLine, prizeRolls, prizes, prizeWords, rent, toUnits } from './model.mjs';
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
45
  const NO_FEE_STEP = /^(token:|collection$|fund:|rent:)/;
@@ -19,17 +47,20 @@ const NO_FEE_STEP = /^(token:|collection$|fund:|rent:)/;
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
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
35
66
  // rewards vault), a sealed schedule's funds (into its vault, from the wallet).
@@ -44,13 +75,24 @@ export function setupCost(plan, economy) {
44
75
  for (const f of m.farms) putIn(f.rewardsToken, f.seed, `seed:farm:${f.key}`);
45
76
  // SOL a lottery holds for its rounds' rent: lent to each round, back at its close.
46
77
  for (const l of m.lotteries) if (steps.has(`rent:lottery:${l.key}`)) seedLamports += roundRentOf(l, m);
47
- for (const sc of m.schedules) if (sc.sealed && (sc.fundFrom ?? 'wallet') === 'wallet') putIn(sc.currency, sc.fund, `fund:${sc.key}`);
78
+ for (const sc of m.schedules)
79
+ if (sc.sealed && (sc.fundFrom ?? 'wallet') === 'wallet') putIn(sc.currency, sc.fund, `fund:${sc.key}`);
48
80
  const total = rentLamports + feeLamports + networkLamports + seedLamports;
49
81
  const notes = [];
50
- if (accounts.some((a) => a.approx)) notes.push('Accounts marked “about” are sized roughly (a Core collection or an external token account).');
51
- if (Object.entries(seeds).some(([symbol]) => [...m.tokens.values()].some((t) => t.wsol && t.symbol === symbol))) notes.push('The total includes the SOL you put in: pools’, lotteries’ and farms’ seeds, and sealed schedules’ funds.');
52
- if (m.lotteries.some((l) => steps.has(`rent:lottery:${l.key}`))) notes.push('The total includes the SOL lotteries hold for their rounds’ rent and their tickets’ entries (`roundRent`, sized by default for the rounds and purchases expected at once): each round borrows it and gives it back when it closes; it comes back to you when the lottery closes.');
53
- if (funded.length) notes.push('The total includes the SOL put in grants’ funds, which pays their claims; what is left goes back to you when a grant closes.');
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
+ );
54
96
  notes.push('The network fee is 5,000 lamports a signature; a wallet may add a priority fee of its own.');
55
97
  return {
56
98
  accounts,
@@ -66,7 +108,7 @@ export function setupCost(plan, economy) {
66
108
  }
67
109
 
68
110
  /**
69
- * What each action costs whoever signs it while the economy runs: games.fun's fee, the network's, and for anything
111
+ * What each action costs whoever signs it while the economy runs: xgames.fun's fee, the network's, and for anything
70
112
  * that asks MagicBlock's oracle for a random number (a box's open, a lottery's draw, the authority's `roll`) the
71
113
  * oracle's own fee (`oracleSol`, model.mjs ORACLE_FEE).
72
114
  */
@@ -74,85 +116,350 @@ export function actionCosts(economy, { now } = NO_OPTIONS) {
74
116
  const m = model(economy);
75
117
  // A player's real cost of one action, from the simulation (simulate.mjs realCostOf): the number to quote.
76
118
  const sim = simulate(economy, { now });
77
- const real = new Map([...sim.items.map((x) => [`item:${x.key}`, x.realCost]), ...sim.boxes.map((x) => [`box:${x.key}`, x.realCost]), ...sim.payments.map((x) => [`payment:${x.key}`, x.realCost]), ...sim.lotteries.map((x) => [`lottery:${x.key}`, x.realCost])]);
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
+ ]);
78
125
  const rows = [];
79
126
  const row = (part, action, who, fee, deposits = [], extra, oracle = 0n) => {
80
127
  const r = real.get(part);
81
- rows.push({ part, action, who, feeSol: sol(fee), ...(oracle ? { oracleSol: sol(oracle) } : {}), networkSol: sol(SIGNATURE_FEE), deposits, ...(extra ? { note: extra } : {}), ...(r ? { inAllSol: r.totalSol ?? null, onTopSol: r.extrasSol, holdSol: r.holdSol, inAll: inAll(r) } : {}) });
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
+ });
82
139
  };
83
140
  const back = (what, lamports, when) => ({ what, sol: sol(lamports), back: when });
84
141
  // An order holds its rent and the fee; the open lends its throwaway settle key what the settle may open or mint (a
85
142
  // collectible's rent for each item the richest outcome gives, each card of a pack), back unspent.
86
- const itemsOf = (b) => drawsOf(b) * Math.max(0, ...(b.outcomes ?? []).map((o) => outcomeParts(o).filter((p) => holdsRent(p, m)).length));
87
- const order = (b) => [
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 => [
88
146
  back('the order', rent(boxOrderBytes(b, m, 1, { opened: true })), 'at settle or refund'),
89
- ...(settleUnits(b, m) ? [back(`the SOL the open lends the throwaway key that settles it, for what its settle may open (${settleUnits(b, m)} × ${sol(SETTLE_RENT_UNIT)} SOL: each collectible, NFT of a stash, coin it may pay and NFT it rolls on)`, SETTLE_RENT_UNIT * BigInt(settleUnits(b, m)), `once the key has settled, less what it opened (a collectible’s rent is about ${sol(COLLECTIBLE_RENT)} SOL); all of it when the roll fails`)] : []),
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
+ : []),
90
156
  ];
91
157
  // A box that needs no roll is bought in one step (`buy`): no order, and a collectible's rent is paid at once.
92
- const bought = (b) => (itemsOf(b) ? [back(`${itemsOf(b)} new collectible${itemsOf(b) > 1 ? 's' : ''}`, COLLECTIBLE_RENT * BigInt(itemsOf(b)), `never: it is the collectible’s own rent (about ${sol(COLLECTIBLE_RENT)} SOL each)`)] : []);
93
- for (const b of m.shop) row(`item:${b.shopOf}`, `buy “${b.name}”`, 'player', FEES.player, bought(b), 'One transaction: the price is split and the item given at once.');
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
+ );
94
177
  for (const b of m.boxes) {
95
- if (boxBuys(b, m)) row(`box:${b.key}`, `buy “${b.name}”`, 'player', FEES.player, bought(b), 'One transaction: the price is split and what it gives handed over at once.');
96
- else row(`box:${b.key}`, `open “${b.name}”`, 'player', FEES.player, order(b), 'The oracle’s fee is paid by whoever asks for the roll (the player, at the open) and a refund doesn’t return it.', ORACLE_FEE);
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
+ );
97
197
  }
98
198
  for (const p of m.payments) row(`payment:${p.key}`, `pay “${p.name}”`, 'player', FEES.player);
99
199
  for (const r of m.recipes) {
100
- const made = (r.out ?? []).filter((l) => l.gives === 'item' && !l.compressed).length;
101
- row(`recipe:${r.key}`, `craft “${r.name}”`, 'player', FEES.player, made ? [back(`${made} new collectible${made > 1 ? 's' : ''}`, COLLECTIBLE_RENT * BigInt(made), `never: it is the collectible’s own rent (about ${sol(COLLECTIBLE_RENT)} SOL each)`)] : []);
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
+ );
102
216
  }
103
217
  for (const l of m.lotteries) {
104
218
  // A purchase adds its entry's bytes to the round. In a round the lottery opened, the SOL it holds for its rounds
105
219
  // (`roundRent`, on by default) pays them, back to it at the close: the buyer pays only the fee and the price. Without
106
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
107
221
  // header. A share the buyer chooses makes each entry keep what arrived.
108
- const chosen = lotterySplit(l).some((s) => String(s.to).startsWith('chosen'));
222
+ const chosen = lotterySplit(l).some(s => String(s.to).startsWith('chosen'));
109
223
  const entry = BigInt(entryWidth(chosen, Boolean(l.ticketNft))) * RENT_PER_BYTE;
110
224
  const held = roundRentOf(l, m);
111
225
  const funded = held > 0n;
112
- row(`lottery:${l.key}`, `buy tickets for “${l.name}”`, 'player', FEES.player, funded ? [] : [back('the purchase’s entry in the round (its bytes)', entry, 'never: it goes to the treasury when the round closes')], funded ? `A round’s record, its token accounts and each purchase’s entry are paid from the SOL the lottery holds for them (${sol(held)} SOL), back to it when the round closes: a buyer pays no program rent. When that runs short, a buyer pays their entry’s bytes (about ${sol(entry)} SOL, to the treasury at the close) and a round’s first buyer fronts its header, back to them at the close.` : 'A round’s first buyer also fronts its record’s header and token accounts (about 0.003 SOL and up, more with many places), back to them when the round closes, once every part is pulled; `roundRent` (on by default) has the lottery pay them, and each entry too.');
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
+ );
113
244
  }
114
245
  for (const f of m.farms) {
115
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.
116
247
  const endRent = BigInt(END_BYTES) * RENT_PER_BYTE;
117
- row(`farm:${f.key}`, `stake in “${f.name}”`, 'player', FEES.player, [back('the seat', rent(ACCOUNT_BYTES.Seat), 'when they take everything out'), ...(farmEnds(f) ? [back(`a new end time in the farm’s record of ends (${END_BYTES} bytes; none when another seat ends at that second, or spare rent in the record covers it)`, endRent, 'never: it stays with the record for the next time written')] : [])], farmEnds(f) ? 'Whatever writes a new end time (a stake, a booster put on, a refresh that reads new upkeep) pays its bytes, its sender’s.' : undefined);
118
- row(`farm:${f.key}`, `unstake or harvest from “${f.name}”`, 'player', FEES.player, [], [
119
- (f.tiers ?? []).some((t) => t.exitFee) && 'Leaving a tier before its lock leaves its exit fee of what is taken out, and keeps the rewards the seat was credited.',
120
- ].filter(Boolean).join(' ') || undefined);
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
+ );
121
282
  }
122
- for (const h of m.holds) row(`hold:${h.key}`, `hand in for “${h.name}”`, 'player', FEES.player, [back('the hold', rent(ACCOUNT_BYTES.Hold), 'when it is released')]);
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
+ ]);
123
287
  // What giving a prize opens, paid by whoever sends the instruction that gives it (prize.rs).
124
- const opens = (p) => prizeLine(p)?.compressed ? ', and the compressed NFT, which has no rent' : ({ item: ', plus the collectible’s rent', tokens: ', plus the winner’s token account for the prize when they have none', share: ', plus the winner’s token account for the prize when they have none', nft: ', and the NFT is handed over in the claim (a Token Metadata NFT opens the winner’s token account of it)', roll: ', plus the roll’s order (below)' })[prizeLine(p)?.gives] ?? '';
125
- for (const g of m.grants) row(`grant:${g.key}`, `claim “${g.name}”`, 'the winner, who signs every claim (anyone may pay its fees)', FEES.player, [], `Only the winning wallet can claim: it signs, whatever proves it won (a voucher, a list, an open grant). Whoever pays the transaction pays the player fee from its own SOL; its records (${LIMIT_RECORD_BYTES} bytes a wallet, ${ROUND_RECORD_BYTES} a round it is paid)${opens(g.prize ?? g.gives)} come from the grant’s funds (the sender pays what they can’t), and go back to the owner when its shards close after the grant: xsolana_simulate gives what to put in its funds per claim.`);
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
+ );
126
307
  // A prize of a collectible given when a place is paid or a winning entry claimed: its rent, from whoever sends it.
127
- for (const p of prizes(m).filter((p) => p.prize.gives === 'item' && !p.prize.compressed && !['grant', 'recipe'].includes(p.route)))
128
- row(p.by, `give ${prizeWords(p.prize, m)} (${p.route})`, p.route === 'mission' ? 'the player leaving the mission' : p.route === 'contest' ? 'whoever claims the winning entry' : 'whoever pays the place', 0n, [back('the new collectible', COLLECTIBLE_RENT, 'never: it is the collectible’s own rent')]);
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
+ );
129
322
  for (const c of m.contests) {
130
- row(`contest:${c.key}`, `back a side in “${c.name}”`, 'player', FEES.player, [back('the entry’s bytes in the contest’s list', BigInt(c.stakeNft ? CONTEST_ENTRY_BYTES.nfts : CONTEST_ENTRY_BYTES.tokens) * RENT_PER_BYTE, 'never: they go to the treasury when the contest closes')], 'A loser of a contest of tokens sends nothing more; every other entry is paid by a claim anyone may send.');
131
- row(`contest:${c.key}`, `open “${c.name}”`, 'authority', FEES.project, [back('the contest and its pot', rent(contestBytes(c, m)) + rent(165), 'when it closes')]);
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
+ ]);
132
340
  row(`contest:${c.key}`, `decide “${c.name}”`, 'authority', FEES.project);
133
341
  }
134
342
  for (const a of m.auctions) {
135
- row(`auction:${a.key}`, `bid on “${a.name}”`, 'player', FEES.player, [], 'Nothing to deposit: the bid waits in the game’s vault, and goes back at once when it is topped.');
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
+ );
136
351
  const minted = a.lot?.item && (m.item.get(a.lot.item)?.kind ?? 'collectible') === 'collectible';
137
- row(`auction:${a.key}`, `end “${a.name}”`, 'anyone (the winner, a keeper)', 0n, minted ? [back('the new collectible', COLLECTIBLE_RENT, 'never: it is the collectible’s own rent, paid by whoever ends it')] : [], 'Its own rent goes back to whoever opened it.');
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
+ );
138
368
  }
139
- for (const s of m.stashes) row(`stash:${s.key}`, `put an NFT in “${s.name}”, or take one out`, 'the owner or the authority', FEES.project, [back('32 bytes more of the stash for each NFT in it', 32n * RENT_PER_BYTE, 'to whoever takes one out; a roll leaves it with the stash, for the next one put in')]);
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
+ ]);
140
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.
141
- 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))) {
142
379
  const name = m.box.get(r.box)?.name ?? r.box;
143
- const who = r.route === 'grant' ? 'the grant’s funds (the sender, what they can’t)' : r.route === 'auction' ? 'whoever ends it' : r.route === 'contest' ? 'whoever claims the winning entry' : 'whoever pays the place';
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';
144
388
  // Whoever settles it pays a collectible's rent when its box gives one (an item outcome).
145
- const mints = (m.box.get(r.box)?.outcomes ?? []).flatMap(outcomeParts).some((o) => holdsRent(o, m));
146
- row(r.by, `make a winner’s roll of “${name}” (${r.route})`, who, 0n, [back('the roll’s order', rent(boxOrderBytes(m.box.get(r.box), m)), 'to the winner when it is settled')], `Then the winner or the project’s keys ask the oracle for the roll (its fee, below) and settle it${mints ? `, paying the collectible’s rent when it gives one (about ${sol(COLLECTIBLE_RENT)} SOL)` : ''}.`);
147
- row(r.by, `ask for the roll of “${name}”`, 'the winner or the project’s keys', 0n, [], 'It asks MagicBlock’s oracle, which takes its fee from whoever asks; a refund doesn’t return it.', ORACLE_FEE);
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
+ );
148
407
  }
149
- for (const d of m.decisions) row(`decision:${d.key}`, d.when, 'authority', FEES.project, [], d.does === 'roll' ? 'The roll asks MagicBlock’s oracle, which takes its own fee from the authority.' : undefined, d.does === 'roll' ? ORACLE_FEE : 0n);
150
- if (m.allBoxes.some((b) => !boxBuys(b, m))) row('orders', 'settle an order', 'the player, a key they named, or the project’s keys', 0n, [], 'The sender pays what the settle opens or mints (a collectible’s rent, a token account the player lacks); the order’s rent goes back to the player. js/play’s throwaway key sends it with what the open lent it.');
151
- if (m.schedules.length) row('schedules', 'run a schedule when it is due', 'the project’s keys, or who it pays (a sealed one’s recipient, from their page)', 0n, [], `A run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse.${m.schedules.some((sc) => sc.sealed) ? ' A sealed schedule pays from its own vault, and its fixed amount pays every run due, in one transaction.' : ''}`);
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
+ );
152
436
  if (m.lotteries.length) {
153
- row('lotteries', 'ask for a draw', 'anyone (a keeper, or a player)', 0n, [], 'The draw asks MagicBlock’s oracle for the winning numbers, and fixes the round’s money first (the next round sells at once); the oracle takes its fee from whoever sends the draw. An oracle that never answers within about a week: the next ask cancels the round, and every ticket is refunded.', ORACLE_FEE);
154
- row('lotteries', 'pull a part: a refund, a place’s part of the pot or its prize, a share owed in common', 'anyone (the winner’s page, a keeper)', 0n, [], 'Each part is pulled by itself, in any order, and the pull that leaves nothing owed closes the round; whoever pulls opens the payee’s token account when it is missing.');
155
- if (m.lotteries.some((l) => l.minSold)) row('lotteries', 'a short round', 'anyone', 0n, [], 'A round that sold fewer tickets than its least is cancelled at its draw, with no oracle fee, and every ticket refunded.');
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
+ );
156
463
  }
157
464
  return rows;
158
465
  }
@@ -162,23 +469,40 @@ export function actionCosts(economy, { now } = NO_OPTIONS) {
162
469
  export function cost(economy, { now = Math.floor(Date.now() / 1000), only = null, live = null } = {}) {
163
470
  const plan = compile(economy, { now, only, live });
164
471
  if (!plan.ok) return plan;
165
- return { ok: true, setup: setupCost(plan, economy), actions: actionCosts(economy, { now }), fees: { playerSol: sol(FEES.player), projectSol: sol(FEES.project), oracleSol: sol(ORACLE_FEE) } };
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
+ };
166
478
  }
167
479
 
168
480
  /** The cost in plain words. */
169
481
  export function explainCost(c) {
170
482
  const s = c.setup;
171
- const lines = [`Going live takes ${s.transactions} transaction${s.transactions === 1 ? '' : 's'} (${s.approvals} wallet approval${s.approvals === 1 ? '' : 's'}) and about ${s.totalSol} SOL:`];
172
- lines.push(` rent ${s.rentSol} SOL for ${s.accounts.length} accounts; program fees ${s.programFeesSol} SOL; network fees ${s.networkFeesSol} SOL${Object.keys(s.seeds).length ? `; seeds ${Object.entries(s.seeds).map(([k, v]) => `${v} ${k}`).join(', ')}` : ''}.`);
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
+ );
173
495
  for (const a of s.accounts) lines.push(` ${a.approx ? 'about ' : ''}${a.sol} SOL: ${a.what} (${a.bytes} bytes)`);
174
496
  lines.push('While it runs (for a player, quote the “in all” number: it is what leaves their wallet for good):');
175
497
  for (const r of c.actions) {
176
- 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('; ');
177
499
  if (r.inAll) {
178
500
  lines.push(` ${r.action} (${r.who}): ${r.inAll}${r.note ? `. ${r.note}` : ''}`);
179
501
  continue;
180
502
  }
181
- lines.push(` ${r.action} (${r.who}): ${r.feeSol ? `${r.feeSol} SOL fee + ` : ''}${r.oracleSol ? `${r.oracleSol} SOL to the oracle + ` : ''}${r.networkSol} SOL network${deposits ? `; ${deposits}` : ''}${r.note ? `. ${r.note}` : ''}`);
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
+ );
182
506
  }
183
507
  for (const n of s.notes) lines.push(n);
184
508
  return lines.join('\n');