@xgamesfun/solana 0.20.1 → 0.24.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 +10 -10
- package/examples/coin-flip-arena.json +2 -7
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +1 -4
- package/examples/merge-2048.json +5 -5
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +1 -4
- package/examples/stakers-draw.json +1 -8
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/js/approve/details.mjs +8 -7
- package/js/approve/server.mjs +20 -5
- package/js/chain/borsh.js +9 -0
- package/js/chain/core.js +14 -2
- package/js/chain/lookup.js +8 -2
- package/js/chain/rpc.js +4 -2
- package/js/client/accounts.js +448 -116
- package/js/client/grant.js +125 -66
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2360 -2218
- package/js/client/index.js +1 -0
- package/js/client/ix.js +1204 -473
- package/js/client/pda.js +20 -16
- package/js/client/policy.js +44 -23
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +255 -12
- package/js/economy/check.mjs +455 -210
- package/js/economy/cost.mjs +45 -30
- package/js/economy/examples.mjs +2 -2
- package/js/economy/explain.mjs +24 -20
- package/js/economy/guide.mjs +31 -30
- package/js/economy/live.mjs +30 -7
- package/js/economy/model.mjs +171 -56
- package/js/economy/plan.mjs +380 -112
- package/js/economy/schema.json +291 -143
- package/js/economy/simulate.mjs +154 -87
- package/js/mcp/tools.mjs +14 -8
- package/js/play/index.js +1121 -375
- package/js/play/keys.js +167 -0
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +15 -11
- package/skills/xsolana-economy/references/after-launch.md +37 -27
- package/skills/xsolana-economy/references/apps.md +55 -19
- package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
- package/skills/xsolana-economy/references/telling-the-person.md +52 -11
- package/types/chain/core.d.ts +3 -1
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +195 -55
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +590 -559
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3862 -234
- package/types/client/pda.d.ts +19 -15
- 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/live.d.mts +16 -1
- package/types/economy/model.d.mts +105 -37
- package/types/economy/plan.d.mts +193 -192
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +450 -39
- package/types/play/keys.d.ts +51 -0
package/js/economy/cost.mjs
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Rent is (bytes + 128) × 5,080 lamports (model.mjs); the loot program's own records are sized byte for byte from
|
|
5
5
|
// their Borsh layout, others (a Core collection, a farm) roughly, and marked so.
|
|
6
|
-
import { plan as compile, contestBytes } from './plan.mjs';
|
|
7
|
-
import { ACCOUNT_BYTES } from '../client/index.js';
|
|
8
|
-
import { boxBuys,
|
|
6
|
+
import { boxOrderBytes, plan as compile, contestBytes, farmEnds, roundRentOf } from './plan.mjs';
|
|
7
|
+
import { ACCOUNT_BYTES, CONTEST_ENTRY_BYTES, END_BYTES, entryWidth, LIMIT_RECORD_BYTES, ROUND_RECORD_BYTES } from '../client/index.js';
|
|
8
|
+
import { boxBuys, COLLECTIBLE_RENT, drawsOf, settleUnits, holdsRent, outcomeParts, SETTLE_RENT_UNIT, FEES, LAMPORTS_PER_SOL, ORACLE_FEE, RENT_PER_BYTE, SIGNATURE_FEE, model, lotterySplit, prizeLine, prizeRolls, prizes, prizeWords, rent, toUnits } from './model.mjs';
|
|
9
9
|
import { inAll, round, simulate } from './simulate.mjs';
|
|
10
10
|
|
|
11
11
|
// The options of a call made without any: read-only, and typed so the generated declarations take any option.
|
|
@@ -14,7 +14,7 @@ const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
|
|
|
14
14
|
const sol = (lamports) => round(Number(lamports) / Number(LAMPORTS_PER_SOL));
|
|
15
15
|
/** Setup steps that are no loot instruction, and so charge no fee: a token minted, the collection made, SOL put in a
|
|
16
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:)/;
|
|
17
|
+
const NO_FEE_STEP = /^(token:|collection$|fund:|rent:)/;
|
|
18
18
|
/** Setup cost from a plan (plan.mjs `plan`). */
|
|
19
19
|
export function setupCost(plan, economy) {
|
|
20
20
|
const m = model(economy);
|
|
@@ -31,8 +31,8 @@ export function setupCost(plan, economy) {
|
|
|
31
31
|
let seedLamports = 0n;
|
|
32
32
|
const funded = m.grants.filter((g) => Number(g.fund ?? 0) > 0 && steps.has(`fund:${g.key}`));
|
|
33
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
|
|
35
|
-
// funds (into its vault, from the wallet).
|
|
34
|
+
// What the owner puts in from their own wallet: a pool's seed, a lottery's seed (into its pot), a farm's seed (into its
|
|
35
|
+
// rewards vault), a sealed schedule's funds (into its vault, from the wallet).
|
|
36
36
|
const putIn = (key, amount, step) => {
|
|
37
37
|
if (!(Number(amount ?? 0) > 0) || !steps.has(step)) return;
|
|
38
38
|
const t = m.tokens.get(key ?? m.currencyKey);
|
|
@@ -41,11 +41,15 @@ export function setupCost(plan, economy) {
|
|
|
41
41
|
};
|
|
42
42
|
for (const p of m.pools) putIn(p.currency, p.seed, `seed:${p.key}`);
|
|
43
43
|
for (const l of m.lotteries) putIn(l.currency, l.seed, `seed:lottery:${l.key}`);
|
|
44
|
+
for (const f of m.farms) putIn(f.rewardsToken, f.seed, `seed:farm:${f.key}`);
|
|
45
|
+
// SOL a lottery holds for its rounds' rent: lent to each round, back at its close.
|
|
46
|
+
for (const l of m.lotteries) if (steps.has(`rent:lottery:${l.key}`)) seedLamports += roundRentOf(l, m);
|
|
44
47
|
for (const sc of m.schedules) if (sc.sealed && (sc.fundFrom ?? 'wallet') === 'wallet') putIn(sc.currency, sc.fund, `fund:${sc.key}`);
|
|
45
48
|
const total = rentLamports + feeLamports + networkLamports + seedLamports;
|
|
46
49
|
const notes = [];
|
|
47
50
|
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
|
|
51
|
+
if (Object.entries(seeds).some(([symbol]) => [...m.tokens.values()].some((t) => t.wsol && t.symbol === symbol))) notes.push('The total includes the SOL you put in: pools’, lotteries’ and farms’ seeds, and sealed schedules’ funds.');
|
|
52
|
+
if (m.lotteries.some((l) => steps.has(`rent:lottery:${l.key}`))) notes.push('The total includes the SOL lotteries hold for their rounds’ rent and their tickets’ entries (`roundRent`, sized by default for the rounds and purchases expected at once): each round borrows it and gives it back when it closes; it comes back to you when the lottery closes.');
|
|
49
53
|
if (funded.length) notes.push('The total includes the SOL put in grants’ funds, which pays their claims; what is left goes back to you when a grant closes.');
|
|
50
54
|
notes.push('The network fee is 5,000 lamports a signature; a wallet may add a priority fee of its own.');
|
|
51
55
|
return {
|
|
@@ -64,7 +68,7 @@ export function setupCost(plan, economy) {
|
|
|
64
68
|
/**
|
|
65
69
|
* What each action costs whoever signs it while the economy runs: games.fun's fee, the network's, and for anything
|
|
66
70
|
* 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
|
|
71
|
+
* oracle's own fee (`oracleSol`, model.mjs ORACLE_FEE).
|
|
68
72
|
*/
|
|
69
73
|
export function actionCosts(economy, { now } = NO_OPTIONS) {
|
|
70
74
|
const m = model(economy);
|
|
@@ -77,42 +81,53 @@ export function actionCosts(economy, { now } = NO_OPTIONS) {
|
|
|
77
81
|
rows.push({ part, action, who, feeSol: sol(fee), ...(oracle ? { oracleSol: sol(oracle) } : {}), networkSol: sol(SIGNATURE_FEE), deposits, ...(extra ? { note: extra } : {}), ...(r ? { inAllSol: r.totalSol ?? null, onTopSol: r.extrasSol, holdSol: r.holdSol, inAll: inAll(r) } : {}) });
|
|
78
82
|
};
|
|
79
83
|
const back = (what, lamports, when) => ({ what, sol: sol(lamports), back: when });
|
|
80
|
-
// An order holds
|
|
81
|
-
|
|
84
|
+
// An order holds its rent and the fee; the open lends its throwaway settle key what the settle may open or mint (a
|
|
85
|
+
// collectible's rent for each item the richest outcome gives, each card of a pack), back unspent.
|
|
86
|
+
const itemsOf = (b) => drawsOf(b) * Math.max(0, ...(b.outcomes ?? []).map((o) => outcomeParts(o).filter((p) => holdsRent(p, m)).length));
|
|
82
87
|
const order = (b) => [
|
|
83
|
-
back('the order', rent(
|
|
84
|
-
...(
|
|
88
|
+
back('the order', rent(boxOrderBytes(b, m, 1, { opened: true })), 'at settle or refund'),
|
|
89
|
+
...(settleUnits(b, m) ? [back(`the SOL the open lends the throwaway key that settles it, for what its settle may open (${settleUnits(b, m)} × ${sol(SETTLE_RENT_UNIT)} SOL: each collectible, NFT of a stash, coin it may pay and NFT it rolls on)`, SETTLE_RENT_UNIT * BigInt(settleUnits(b, m)), `once the key has settled, less what it opened (a collectible’s rent is about ${sol(COLLECTIBLE_RENT)} SOL); all of it when the roll fails`)] : []),
|
|
85
90
|
];
|
|
86
91
|
// A box that needs no roll is bought in one step (`buy`): no order, and a collectible's rent is paid at once.
|
|
87
92
|
const bought = (b) => (itemsOf(b) ? [back(`${itemsOf(b)} new collectible${itemsOf(b) > 1 ? 's' : ''}`, COLLECTIBLE_RENT * BigInt(itemsOf(b)), `never: it is the collectible’s own rent (about ${sol(COLLECTIBLE_RENT)} SOL each)`)] : []);
|
|
88
93
|
for (const b of m.shop) row(`item:${b.shopOf}`, `buy “${b.name}”`, 'player', FEES.player, bought(b), 'One transaction: the price is split and the item given at once.');
|
|
89
94
|
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),
|
|
95
|
+
if (boxBuys(b, m)) row(`box:${b.key}`, `buy “${b.name}”`, 'player', FEES.player, bought(b), 'One transaction: the price is split and what it gives handed over at once.');
|
|
96
|
+
else row(`box:${b.key}`, `open “${b.name}”`, 'player', FEES.player, order(b), 'The oracle’s fee is paid by whoever asks for the roll (the player, at the open) and a refund doesn’t return it.', ORACLE_FEE);
|
|
92
97
|
}
|
|
93
98
|
for (const p of m.payments) row(`payment:${p.key}`, `pay “${p.name}”`, 'player', FEES.player);
|
|
94
99
|
for (const r of m.recipes) {
|
|
95
100
|
const made = (r.out ?? []).filter((l) => l.gives === 'item' && !l.compressed).length;
|
|
96
101
|
row(`recipe:${r.key}`, `craft “${r.name}”`, 'player', FEES.player, made ? [back(`${made} new collectible${made > 1 ? 's' : ''}`, COLLECTIBLE_RENT * BigInt(made), `never: it is the collectible’s own rent (about ${sol(COLLECTIBLE_RENT)} SOL each)`)] : []);
|
|
97
102
|
}
|
|
98
|
-
for (const l of m.lotteries)
|
|
103
|
+
for (const l of m.lotteries) {
|
|
104
|
+
// A purchase adds its entry's bytes to the round. In a round the lottery opened, the SOL it holds for its rounds
|
|
105
|
+
// (`roundRent`, on by default) pays them, back to it at the close: the buyer pays only the fee and the price. Without
|
|
106
|
+
// it, or once it runs short, the buyer pays them (to the treasury at the close), and a round's first buyer fronts its
|
|
107
|
+
// header. A share the buyer chooses makes each entry keep what arrived.
|
|
108
|
+
const chosen = lotterySplit(l).some((s) => String(s.to).startsWith('chosen'));
|
|
109
|
+
const entry = BigInt(entryWidth(chosen, Boolean(l.ticketNft))) * RENT_PER_BYTE;
|
|
110
|
+
const held = roundRentOf(l, m);
|
|
111
|
+
const funded = held > 0n;
|
|
112
|
+
row(`lottery:${l.key}`, `buy tickets for “${l.name}”`, 'player', FEES.player, funded ? [] : [back('the purchase’s entry in the round (its bytes)', entry, 'never: it goes to the treasury when the round closes')], funded ? `A round’s record, its token accounts and each purchase’s entry are paid from the SOL the lottery holds for them (${sol(held)} SOL), back to it when the round closes: a buyer pays no program rent. When that runs short, a buyer pays their entry’s bytes (about ${sol(entry)} SOL, to the treasury at the close) and a round’s first buyer fronts its header, back to them at the close.` : 'A round’s first buyer also fronts its record’s header and token accounts (about 0.003 SOL and up, more with many places), back to them when the round closes, once every part is pulled; `roundRent` (on by default) has the lottery pay them, and each entry too.');
|
|
113
|
+
}
|
|
99
114
|
for (const f of m.farms) {
|
|
100
|
-
|
|
115
|
+
// A seat that can stop earning writes its end in the farm's record: a new time costs whoever writes it, spare rent first.
|
|
116
|
+
const endRent = BigInt(END_BYTES) * RENT_PER_BYTE;
|
|
117
|
+
row(`farm:${f.key}`, `stake in “${f.name}”`, 'player', FEES.player, [back('the seat', rent(ACCOUNT_BYTES.Seat), 'when they take everything out'), ...(farmEnds(f) ? [back(`a new end time in the farm’s record of ends (${END_BYTES} bytes; none when another seat ends at that second, or spare rent in the record covers it)`, endRent, 'never: it stays with the record for the next time written')] : [])], farmEnds(f) ? 'Whatever writes a new end time (a stake, a booster put on, a refresh that reads new upkeep) pays its bytes, its sender’s.' : undefined);
|
|
101
118
|
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)}`}.`,
|
|
119
|
+
(f.tiers ?? []).some((t) => t.exitFee) && 'Leaving a tier before its lock leaves its exit fee of what is taken out, and keeps the rewards the seat was credited.',
|
|
105
120
|
].filter(Boolean).join(' ') || undefined);
|
|
106
121
|
}
|
|
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
|
|
122
|
+
for (const h of m.holds) row(`hold:${h.key}`, `hand in for “${h.name}”`, 'player', FEES.player, [back('the hold', rent(ACCOUNT_BYTES.Hold), 'when it is released')]);
|
|
108
123
|
// What giving a prize opens, paid by whoever sends the instruction that gives it (prize.rs).
|
|
109
124
|
const opens = (p) => prizeLine(p)?.compressed ? ', and the compressed NFT, which has no rent' : ({ item: ', plus the collectible’s rent', tokens: ', plus the winner’s token account for the prize when they have none', share: ', plus the winner’s token account for the prize when they have none', nft: ', and the NFT is handed over in the claim (a Token Metadata NFT opens the winner’s token account of it)', roll: ', plus the roll’s order (below)' })[prizeLine(p)?.gives] ?? '';
|
|
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
|
|
125
|
+
for (const g of m.grants) row(`grant:${g.key}`, `claim “${g.name}”`, 'the winner, who signs every claim (anyone may pay its fees)', FEES.player, [], `Only the winning wallet can claim: it signs, whatever proves it won (a voucher, a list, an open grant). Whoever pays the transaction pays the player fee from its own SOL; its records (${LIMIT_RECORD_BYTES} bytes a wallet, ${ROUND_RECORD_BYTES} a round it is paid)${opens(g.prize ?? g.gives)} come from the grant’s funds (the sender pays what they can’t), and go back to the owner when its shards close after the grant: xsolana_simulate gives what to put in its funds per claim.`);
|
|
111
126
|
// A prize of a collectible given when a place is paid or a winning entry claimed: its rent, from whoever sends it.
|
|
112
127
|
for (const p of prizes(m).filter((p) => p.prize.gives === 'item' && !p.prize.compressed && !['grant', 'recipe'].includes(p.route)))
|
|
113
128
|
row(p.by, `give ${prizeWords(p.prize, m)} (${p.route})`, p.route === 'mission' ? 'the player leaving the mission' : p.route === 'contest' ? 'whoever claims the winning entry' : 'whoever pays the place', 0n, [back('the new collectible', COLLECTIBLE_RENT, 'never: it is the collectible’s own rent')]);
|
|
114
129
|
for (const c of m.contests) {
|
|
115
|
-
row(`contest:${c.key}`, `back a side in “${c.name}”`, 'player', FEES.player, [back('the entry',
|
|
130
|
+
row(`contest:${c.key}`, `back a side in “${c.name}”`, 'player', FEES.player, [back('the entry’s bytes in the contest’s list', BigInt(c.stakeNft ? CONTEST_ENTRY_BYTES.nfts : CONTEST_ENTRY_BYTES.tokens) * RENT_PER_BYTE, 'never: they go to the treasury when the contest closes')], 'A loser of a contest of tokens sends nothing more; every other entry is paid by a claim anyone may send.');
|
|
116
131
|
row(`contest:${c.key}`, `open “${c.name}”`, 'authority', FEES.project, [back('the contest and its pot', rent(contestBytes(c, m)) + rent(165), 'when it closes')]);
|
|
117
132
|
row(`contest:${c.key}`, `decide “${c.name}”`, 'authority', FEES.project);
|
|
118
133
|
}
|
|
@@ -126,17 +141,17 @@ export function actionCosts(economy, { now } = NO_OPTIONS) {
|
|
|
126
141
|
for (const r of prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route))) {
|
|
127
142
|
const name = m.box.get(r.box)?.name ?? r.box;
|
|
128
143
|
const who = r.route === 'grant' ? 'the grant’s funds (the sender, what they can’t)' : r.route === 'auction' ? 'whoever ends it' : r.route === 'contest' ? 'whoever claims the winning entry' : 'whoever pays the place';
|
|
129
|
-
//
|
|
130
|
-
const mints = (m.box.get(r.box)?.outcomes ?? []).flatMap(outcomeParts).some((o) => o
|
|
131
|
-
row(r.by, `make a winner’s roll of “${name}” (${r.route})`, who, 0n, [back(
|
|
132
|
-
row(r.by, `ask for the roll of “${name}”`, '
|
|
144
|
+
// Whoever settles it pays a collectible's rent when its box gives one (an item outcome).
|
|
145
|
+
const mints = (m.box.get(r.box)?.outcomes ?? []).flatMap(outcomeParts).some((o) => holdsRent(o, m));
|
|
146
|
+
row(r.by, `make a winner’s roll of “${name}” (${r.route})`, who, 0n, [back('the roll’s order', rent(boxOrderBytes(m.box.get(r.box), m)), 'to the winner when it is settled')], `Then the winner or the project’s keys ask the oracle for the roll (its fee, below) and settle it${mints ? `, paying the collectible’s rent when it gives one (about ${sol(COLLECTIBLE_RENT)} SOL)` : ''}.`);
|
|
147
|
+
row(r.by, `ask for the roll of “${name}”`, 'the winner or the project’s keys', 0n, [], 'It asks MagicBlock’s oracle, which takes its fee from whoever asks; a refund doesn’t return it.', ORACLE_FEE);
|
|
133
148
|
}
|
|
134
149
|
for (const d of m.decisions) row(`decision:${d.key}`, d.when, 'authority', FEES.project, [], d.does === 'roll' ? 'The roll asks MagicBlock’s oracle, which takes its own fee from the authority.' : undefined, d.does === 'roll' ? ORACLE_FEE : 0n);
|
|
135
|
-
if (m.allBoxes.some((b) => !boxBuys(b))) row('orders', 'settle an order', '
|
|
136
|
-
if (m.schedules.length) row('schedules', 'run a schedule when it is due', '
|
|
150
|
+
if (m.allBoxes.some((b) => !boxBuys(b, m))) row('orders', 'settle an order', 'the player, a key they named, or the project’s keys', 0n, [], 'The sender pays what the settle opens or mints (a collectible’s rent, a token account the player lacks); the order’s rent goes back to the player. js/play’s throwaway key sends it with what the open lent it.');
|
|
151
|
+
if (m.schedules.length) row('schedules', 'run a schedule when it is due', 'the project’s keys, or who it pays (a sealed one’s recipient, from their page)', 0n, [], `A run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse.${m.schedules.some((sc) => sc.sealed) ? ' A sealed schedule pays from its own vault, and its fixed amount pays every run due, in one transaction.' : ''}`);
|
|
137
152
|
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', '
|
|
153
|
+
row('lotteries', 'ask for a draw', 'anyone (a keeper, or a player)', 0n, [], 'The draw asks MagicBlock’s oracle for the winning numbers, and fixes the round’s money first (the next round sells at once); the oracle takes its fee from whoever sends the draw. An oracle that never answers within about a week: the next ask cancels the round, and every ticket is refunded.', ORACLE_FEE);
|
|
154
|
+
row('lotteries', 'pull a part: a refund, a place’s part of the pot or its prize, a share owed in common', 'anyone (the winner’s page, a keeper)', 0n, [], 'Each part is pulled by itself, in any order, and the pull that leaves nothing owed closes the round; whoever pulls opens the payee’s token account when it is missing.');
|
|
140
155
|
if (m.lotteries.some((l) => l.minSold)) row('lotteries', 'a short round', 'anyone', 0n, [], 'A round that sold fewer tickets than its least is cancelled at its draw, with no oracle fee, and every ticket refunded.');
|
|
141
156
|
}
|
|
142
157
|
return rows;
|
package/js/economy/examples.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export const ideas = () => IDEAS;
|
|
|
31
31
|
const SYNONYMS = {
|
|
32
32
|
gacha: ['box', 'boxes', 'pull'], loot: ['box', 'boxes'], mystery: ['box', 'boxes'], pack: ['box', 'boxes'], packs: ['box', 'boxes'], roll: ['box', 'boxes'], chest: ['box', 'boxes'],
|
|
33
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: ['
|
|
34
|
+
raffle: ['lottery', 'lotteries'], lottery: ['lottery', 'lotteries'], draw: ['lottery', 'lotteries'], jackpot: ['lottery', 'lotteries', 'pot'],
|
|
35
35
|
craft: ['recipe', 'recipes'], crafting: ['recipe', 'recipes'], merge: ['recipe', 'recipes'], upgrade: ['recipe', 'recipes'], breed: ['recipe', 'recipes', 'life'], breeding: ['recipe', 'recipes', 'life'],
|
|
36
36
|
season: ['schedule', 'schedules'], seasons: ['schedule', 'schedules'], halving: ['schedule', 'schedules'], emission: ['schedule', 'schedules'], emissions: ['schedule', 'schedules'], vesting: ['schedule', 'schedules'],
|
|
37
37
|
bet: ['contest', 'contests'], betting: ['contest', 'contests'], duel: ['contest', 'contests'], wager: ['contest', 'contests'], prediction: ['contest', 'contests'], fantasy: ['contest', 'contests'],
|
|
@@ -52,7 +52,7 @@ const STOP = new Set(['a', 'an', 'the', 'and', 'or', 'of', 'to', 'in', 'for', 'w
|
|
|
52
52
|
export function partsUsed(spec) {
|
|
53
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
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/], ['
|
|
55
|
+
for (const [f, re] of [['rise', /"rise"/], ['fall', /"fall"/], ['life', /"life"/], ['holder', /holder:/], ['burn', /"burn"/], ['gate', /"gate"/], ['odds', /"odds"/], ['token-2022', /token-2022/], ['staked', /"staked":\{/], ['no-loss', /"noLoss":true/], ['chosen', /"to":"chosen/], ['minpot', /"minPot":/], ['shared-clock', /"clock":"shared"/]]) if (re.test(all)) out.push(f);
|
|
56
56
|
return out;
|
|
57
57
|
}
|
|
58
58
|
|
package/js/economy/explain.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// going live costs, and what the owner must know. Deterministic. Pure.
|
|
4
4
|
import { check, describe } from './check.mjs';
|
|
5
5
|
import { cost } from './cost.mjs';
|
|
6
|
-
import { boxBuys, farmEndWords, listOf, lotterySplit, model,
|
|
7
|
-
import { playSteps } from './plan.mjs';
|
|
6
|
+
import { boostReadsGrowth, boxBuys, farmEndWords, lateRunWords, listOf, lotterySplit, model, outcomeParts, placeOf, prizeLine, prizeRolls, prizes, prizeWords, rollsOf, sharesOf, stashNfts, toSeconds, whenOutWords } from './model.mjs';
|
|
7
|
+
import { farmEnds, playSteps } from './plan.mjs';
|
|
8
8
|
import { inAll, poolFate, round, simulate, unnamedExceptions, unnamedWords } from './simulate.mjs';
|
|
9
9
|
|
|
10
10
|
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
@@ -26,7 +26,7 @@ function splitWords(split, treasury = 'your treasury', rest = `to ${treasury}`,
|
|
|
26
26
|
const ruleWords = (r) => {
|
|
27
27
|
if (!r) return '';
|
|
28
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)}
|
|
29
|
+
const what = r.item ? `a “${r.item}”` : r.collection ? `an NFT of the collection ${r.collection.slice(0, 4)}… (not a Token Metadata print)` : 'one of the game’s collectibles';
|
|
30
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
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
32
|
};
|
|
@@ -85,8 +85,8 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
85
85
|
const src = m.box.get(b.key);
|
|
86
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
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 === '
|
|
89
|
-
const outs = b.outcomes.map((o) => `${num(o.chance)}% ${o.gives === 'bundle' ?
|
|
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 === '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(' + ') : part(o)}`).join(', ');
|
|
90
90
|
const odds = (src.odds ?? []).map((o) => `outcome ${o.outcome} ${effectWords(o.add)} for a player who shows ${ruleWords(o.when)}`);
|
|
91
91
|
// A roll on an NFT: what each outcome does to it.
|
|
92
92
|
const fates = (src.outcomes ?? []).flatMap((o, i) => {
|
|
@@ -107,12 +107,12 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
107
107
|
return kind === 'lottery' ? `${r.route} of the lottery “${part?.name ?? key}”` : `${named[r.route] ?? kind} “${part?.name ?? key}”`;
|
|
108
108
|
}))];
|
|
109
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}
|
|
110
|
+
actions.push(`“${b.name}” is never sold: ${by.join(' and ') || 'nothing'} rolls it. Odds: ${outs}${odds.length ? `; ${odds.join('; ')}` : ''}.${rolls} The winner or the project’s keys ask the oracle for a roll, and settle it (the oracle's fee is the asker's). A roll that holds no payment is never refunded and never lost: an edit of the box leaves it to roll on the box as it is then; one holding a payment or an NFT that the oracle leaves unanswered for 7 days can be refunded. ${whenOutWords(src, { sold: false })}.`);
|
|
111
111
|
continue;
|
|
112
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: ${
|
|
113
|
+
const verb = boxBuys(src, m) ? 'Buy' : 'Open';
|
|
114
|
+
const oneStep = boxBuys(src, m) ? ' 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, m) ? 'It gives' : 'Odds'}: ${outs}${odds.length ? `; ${odds.join('; ')}` : ''}${src.gate ? `. ${gateWords(src.gate, 'open it', m)}` : ''}.${rolls} The money: ${splitOf(src.split)}.${limitWords(src)}${src.outcomes.flatMap(outcomeParts).some((o) => o.gives === 'pay') ? (boxBuys(src, m) ? ' It sells only while its pools hold every fixed prize.' : ' A prize won is set aside for its winner at once.') : ''}${b.limitedStock ? ` ${whenOutWords(src)}.` : ''}${!boxBuys(src, m) ? ' An edit of the box’s roll fails a purchase still waiting, for its refund.' : ''}${oneStep}`);
|
|
116
116
|
}
|
|
117
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
118
|
for (const r of sim.recipes) actions.push(`Craft “${r.name}”: ${r.takes.join(' + ') || 'nothing'} → ${r.gives.join(' + ') || 'nothing'} (+ ${fee} SOL player fee).`);
|
|
@@ -125,7 +125,7 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
125
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
126
|
continue;
|
|
127
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
|
|
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 pull it), and the winners take only the pot, what is paid into it, so a ticket wins ${num(l.expectedPerTicket)} on average on top of its price coming back (tickets are not weighted by time: someone may buy most of them right before the draw and risk only the fees)` : `a ticket is worth ${num(l.expectedPerTicket)} on average${l.unpricedPrizes ? ', plus its chance at the places’ prizes' : ''}${l.returnPercent !== null ? ` (${num(l.returnPercent)}%)` : ''}`}${l.cutPercent && !(src.split ?? []).length ? `; ${num(l.cutPercent)}% of ticket money goes to ${treasuryWords}, the rest into the pot` : ''}${others}${src.perWallet ? `; at most ${src.perWallet} tickets per wallet a round` : ''}.${src.gate ? ` ${gateWords(src.gate, 'buy tickets', m)}.` : ''}`);
|
|
129
129
|
}
|
|
130
130
|
for (const f of sim.farms) {
|
|
131
131
|
const src = m.farm.get(f.key);
|
|
@@ -138,22 +138,26 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
138
138
|
const earns = f.rate && !(Number(f.rate.perDay) > 0)
|
|
139
139
|
? 'no tokens'
|
|
140
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' :
|
|
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` : ''})` : ''}`;
|
|
141
|
+
? `${num(f.rate.perDay)} ${sym(f.rewardsToken)} a day ${f.rate.shared ? 'for the whole farm, shared by weight while anything is staked' : `for each ${f.unit ?? (src.stakes === 'collectibles' ? 'collectible' : sym(src.stakes))} at x1, whoever else is in`}${f.rate.halveEvery ? `, halving every ${f.rate.halveEvery}` : ''} (${f.rate.minted ? 'minted when a seat is paid while the mint has room for it, never owed later past that' : 'from its rewards vault while it lasts'}); what a seat gives back (a forfeit, a watched NFT sold) is ${f.rate.minted ? 'never minted' : 'left free in its vault'}, never to the other seats`
|
|
142
|
+
: `${num(f.rewardsPerDay)} ${sym(f.rewardsToken)} a day, what lands in its rewards vault, shared by weight${f.perUnitPerDay !== undefined ? ` (${num(f.perUnitPerDay)} a day for each ${f.unit} at x1${f.yearlyPercent !== undefined ? `, ${num(f.yearlyPercent)}% a year` : ''})` : ''}`;
|
|
143
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
144
|
const gates = listOf(src.gate);
|
|
145
145
|
const takes = gates.length ? `; to stake here an NFT must be ${gates.map(ruleWords).join(' and ')}` : '';
|
|
146
146
|
const end = farmEndWords(src);
|
|
147
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
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
|
|
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 from that time until it is topped up and its seat refreshed` : '';
|
|
150
|
+
// A farm that keeps its record of ends: what a seat earned by its end is exact, and each new end time has a rent.
|
|
151
|
+
const ends = farmEnds(src) ? `; what a seat earned by its end (${(src.tiers ?? []).some((t) => t.earnsFor) ? 'its tier’s term' : 'its upkeep'}) is exact however late it is next touched, and nobody has to act at that time: the farm keeps a record of those times, each new one costing whoever writes it 44 bytes of rent (about 0.00022 SOL; spare rent in the record pays first), not refunded on leaving. A farm idle past many ends has them walked in parts first (\`app.walkFarm\`, free and anyone’s)` : '';
|
|
152
|
+
// A boost that reads a level its own growth writes counts each level from when it is written.
|
|
153
|
+
const refreshes = boostReadsGrowth(src) ? '; a boost that reads what its own growth writes counts each level from when it is written, by a seat’s refresh, so a seat refreshed often earns more than one refreshed rarely: have the page or a keeper refresh seats (anyone may, for free)' : '';
|
|
150
154
|
const missions = f.tiers.filter((t) => t.prize).map((t) => `${t.prize} for staying ${t.lock}`);
|
|
151
155
|
const results = (src.tiers ?? []).filter((t) => t.roll).map((t) => {
|
|
152
156
|
const box = m.box.get(t.roll);
|
|
153
157
|
const fates = box ? [...new Set((box.outcomes ?? []).map((o) => fateWords(o, box, m)))] : [];
|
|
154
158
|
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
159
|
});
|
|
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}`);
|
|
160
|
+
actions.push(`Stake ${src.stakes === 'collectibles' ? `collectibles${src.item ? ` (${src.item})` : ''}` : sym(src.stakes)} in “${f.name}”: ${earns}${grows}${takes}${runs}${lock}${upkeep}${ends}${refreshes}${missions.length ? `; a mission that stays its lock (topped up, not forfeited) brings back ${missions.join(', ')}, given as it leaves (a roll is settled after)` : ''}${results.length ? `; ${results.join('; ')}` : ''}; lock tiers ${tiers}${boosts.length ? `; boosts ${boosts.join(', ')}, at most +${src.maxBoostPercent ?? 0}%` : ''}. After the lock, pause and suspend cannot stop an unstake; external token or NFT controls can still block custody release.${custody}`);
|
|
157
161
|
}
|
|
158
162
|
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
163
|
for (const ct of sim.contests) {
|
|
@@ -172,7 +176,7 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
172
176
|
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
177
|
continue;
|
|
174
178
|
}
|
|
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)}.` : ''}`);
|
|
179
|
+
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)}.${src.lot?.asset ? ' A seller whose account can’t take the money then doesn’t hold up the winner: the lot goes, the auction stays open owing it, and the next end pays and closes it.' : ''} With no bid the lot goes home; an auction nobody ends waits, and sells to the highest bidder whenever anyone ends it.${src.gate ? ` ${gateWords(src.gate, 'bid', m)}.` : ''}`);
|
|
176
180
|
}
|
|
177
181
|
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
182
|
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.');
|
|
@@ -191,14 +195,14 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
191
195
|
const nfts = stashNfts(src);
|
|
192
196
|
const boxes = m.boxes.filter((b) => (b.outcomes ?? []).flatMap(outcomeParts).some((o) => o.gives === 'stash' && o.stash === st.key)).map((b) => `“${b.name}”`);
|
|
193
197
|
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
|
|
198
|
+
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 the roll is then settled to hand it over; a prize that names an NFT hands it over in the instruction that wins it. Empty, a roll that lands on it makes no draw, and gives back that draw’s worth. Your wallet (or the authority) can put more in, or take one out, any time.${st.lastsDays !== undefined ? ` At the scenario’s rate it lasts about ${num(st.lastsDays)} days.` : ''}`);
|
|
195
199
|
}
|
|
196
|
-
// Prizes that are a roll of a box: made for the winner, then asked of the oracle and settled by
|
|
200
|
+
// Prizes that are a roll of a box: made for the winner, then asked of the oracle and settled by the winner or the project’s keys.
|
|
197
201
|
const prized = prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route));
|
|
198
202
|
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);
|
|
203
|
+
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); the winner or the project’s keys then ask the oracle for the roll (its fee is the asker’s) and settle it. A roll that holds no payment is never refunded and never lost: it rolls on the box as it is when it is asked.`);
|
|
200
204
|
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);
|
|
205
|
+
const rolls = (l.places ?? []).map(placeOf).map((p, i) => (p.prize ? `${p.times > 1 ? `${p.times} places from place ${i + 1} each win` : `place ${i + 1} wins`} ${prizeWords(p.prize, m)}${p.percent ? ` and ${p.percent}% of the pot` : ''}` : null)).filter(Boolean);
|
|
202
206
|
if (rolls.length) actions.push(`In “${l.name}”, ${rolls.join('; ')}.`);
|
|
203
207
|
}
|
|
204
208
|
section('What players do, what it costs and what it is worth', actions);
|
|
@@ -207,7 +211,7 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
207
211
|
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
212
|
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
213
|
}));
|
|
210
|
-
section('What runs
|
|
214
|
+
section('What runs on a timer', [...sim.schedules.map((s) => { const sc = m.schedules.find((x) => x.key === s.key); return `“${sc?.name ?? s.key}” ${s.says}; ${sc?.sealed ? 'its recipient runs it when it is due' : 'you, or a key of the project’s on your server, must send each run when it is due: nothing runs it otherwise'}; ${lateRunWords(sc ?? {})}.`; }), ...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), move the drawn round on and pull what each entry is owed.`)]);
|
|
211
215
|
const authority = play.filter((s) => s.signer.startsWith('authority'));
|
|
212
216
|
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
217
|
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.');
|