@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.
- package/README.md +95 -52
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +4 -12
- package/examples/duel-arena.json +1 -4
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/fair-presale.json +1 -3
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +15 -6
- package/examples/holder-den.json +38 -0
- package/examples/holders-drop.json +105 -0
- package/examples/ideas.json +788 -788
- package/examples/match-predictions.json +24 -0
- package/examples/merge-2048.json +5 -5
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +20 -7
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -15
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +122 -52
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +290 -73
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +24 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +35 -7
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +124 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +44 -16
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +653 -131
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +267 -83
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2406 -2176
- package/js/client/index.js +1 -0
- package/js/client/ix.js +3059 -664
- package/js/client/pda.js +39 -33
- package/js/client/policy.js +52 -24
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +414 -16
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +4153 -725
- package/js/economy/cost.mjs +403 -64
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +466 -111
- package/js/economy/guide.mjs +296 -72
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +83 -19
- package/js/economy/model.mjs +462 -124
- package/js/economy/plan.mjs +1306 -219
- package/js/economy/schema.json +390 -484
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +883 -223
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/mcp/server.mjs +42 -14
- package/js/mcp/tools.mjs +307 -88
- package/js/play/index.js +2864 -619
- package/js/play/keys.js +193 -0
- package/js/results/index.js +223 -30
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +39 -21
- package/skills/xsolana-economy/references/after-launch.md +58 -38
- package/skills/xsolana-economy/references/apps.md +97 -50
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
- package/skills/xsolana-economy/references/telling-the-person.md +61 -17
- package/types/chain/core.d.ts +4 -2
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +217 -56
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +602 -549
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3884 -247
- package/types/client/pda.d.ts +22 -16
- package/types/client/policy.d.ts +39 -14
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +93 -5
- package/types/economy/check.d.mts +5 -10
- package/types/economy/cost.d.mts +2 -1
- package/types/economy/guide.d.mts +5 -0
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +151 -38
- package/types/economy/plan.d.mts +237 -194
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +453 -53
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +7 -6
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
package/js/economy/cost.mjs
CHANGED
|
@@ -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 {
|
|
8
|
-
|
|
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 =
|
|
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 =
|
|
23
|
-
const accounts = plan.transactions
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
48
|
-
|
|
49
|
-
if (
|
|
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
|
|
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([
|
|
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({
|
|
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
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
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
|
|
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 =
|
|
88
|
-
|
|
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
|
|
91
|
-
|
|
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(
|
|
96
|
-
row(
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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)
|
|
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 =
|
|
110
|
-
|
|
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(
|
|
113
|
-
|
|
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(
|
|
116
|
-
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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 =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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)
|
|
135
|
-
|
|
136
|
-
|
|
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(
|
|
139
|
-
|
|
140
|
-
|
|
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 {
|
|
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 = [
|
|
157
|
-
|
|
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(
|
|
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(
|
|
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');
|