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