@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/simulate.mjs
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
//
|
|
5
5
|
// It assumes the scenario's counts (`scenario.perDay`, default 100 of each player action a day) arrive evenly,
|
|
6
6
|
// odds as written (stock odds start where the table starts), and one ticket per lottery purchase. Shares of a
|
|
7
|
-
// pool are worked out continuously, so a pool that pays a share of itself settles at a level
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
// pool (a schedule's, a decision's) are worked out continuously, so a pool that pays a share of itself settles at a level
|
|
8
|
+
// instead of emptying. A prize is a fixed amount, all of it or none. Money is counted per day while a box sells; when
|
|
9
|
+
// its items, stashes or named NFTs run out (its own opens alone), a box that stops selling (`whenOut` "stopSales") says
|
|
10
|
+
// the day it stops, and one that draws among what is left ("drawLeft") the day each outcome runs out and the odds of
|
|
11
|
+
// the rest from then (`stockWalk`). It is a guide, not a promise: the full simulation (js/sim) runs the real program.
|
|
12
|
+
import { entryWidth, LIMIT_RECORD_BYTES, ROUND_RECORD_BYTES } from '../client/index.js';
|
|
13
|
+
import { boxOrderBytes, roundRentOf } from './plan.mjs';
|
|
14
|
+
import { boxBuys, COLLECTIBLE_RENT as COLLECTIBLE_RENT_LAMPORTS, drawsOf, settleUnits, holdsRent, outcomeParts, SETTLE_RENT_UNIT, FEES, LAMPORTS_PER_SOL, ORACLE_FEE, RENT_PER_BYTE, SIGNATURE_FEE, destination, listOf, lotterySplit, model, placeOf, prizeLine, prizePool, prizeToken, farmEndWords, lateRunWords, prizeWords, releaseWords, sharesOf, toBps, toSeconds, toTime, weights, whenOutWords } from './model.mjs';
|
|
11
15
|
|
|
12
16
|
const DAY = 86_400;
|
|
13
17
|
const DEFAULT_PER_DAY = 100;
|
|
@@ -19,19 +23,21 @@ const PROJECT_FEE = sol(FEES.project);
|
|
|
19
23
|
const NETWORK_FEE = sol(SIGNATURE_FEE);
|
|
20
24
|
/** MagicBlock's oracle, per random number: a box's open (the player), a lottery's draw, a `roll` (model.mjs ORACLE_FEE). */
|
|
21
25
|
const ORACLE = sol(ORACLE_FEE);
|
|
22
|
-
/** A lottery purchase's
|
|
23
|
-
|
|
26
|
+
/** A lottery purchase's entry in its round: the bytes the buyer adds (not refunded: they go to the treasury when the
|
|
27
|
+
* round closes), with a wallet kept for each share the buyer's own entry names (lottery.rs `entry_width`). */
|
|
28
|
+
const ticketLine = (l) => sol(BigInt(entryWidth(lotterySplit(l).some((x) => String(x.to).startsWith('chosen')), Boolean(l.ticketNft))) * RENT_PER_BYTE);
|
|
24
29
|
/** A collectible's Core account, paid from the SOL the order holds: its rent and Core's creation fee (model.mjs). */
|
|
25
30
|
const COLLECTIBLE_RENT = sol(COLLECTIBLE_RENT_LAMPORTS);
|
|
26
31
|
/** What a collectible a recipe burns gives back: its rent less one byte's (Core leaves a 1-byte account that keeps
|
|
27
32
|
* the creation fee). 189 bytes × 5,080 lamports, measured by the full simulation. */
|
|
28
33
|
const BURN_REFUND = 0.00096;
|
|
29
|
-
/**
|
|
30
|
-
|
|
34
|
+
/** What a grant's claim adds to its lists: the wallet's limit record (its first claim) and a record of the round paid
|
|
35
|
+
* (none for an open grant's claim). Back to the owner when the shards close after the grant. */
|
|
36
|
+
const claimRecordsRent = (g) => sol(BigInt(LIMIT_RECORD_BYTES + ((g.vouchers ?? []).length || (g.keys ?? []).length || (g.list ?? []).length ? ROUND_RECORD_BYTES : 0)) * RENT_PER_BYTE);
|
|
31
37
|
/** A wallet's token account, which a grant that pays tokens opens from its funds when the winner has none. */
|
|
32
38
|
const TOKEN_ACCOUNT_RENT = sol((165n + 128n) * RENT_PER_BYTE);
|
|
33
|
-
/**
|
|
34
|
-
const
|
|
39
|
+
/** An order of box `b`'s rent (`orderBytes`), returned at settle or refund: an open's (`opened`, sized as `open_box` sizes it), or a roll's made inside another call. */
|
|
40
|
+
const orderRent = (b, m, opened = false) => sol((BigInt(boxOrderBytes(b, m, 1, { opened })) + 128n) * RENT_PER_BYTE);
|
|
35
41
|
const num = (v) => (v === undefined || v === null ? 0 : Number(v));
|
|
36
42
|
export const round = (x, digits = 9) => (Number.isFinite(x) ? Number(x.toFixed(digits)) : x);
|
|
37
43
|
|
|
@@ -146,6 +152,8 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
146
152
|
const pools = new Map(m.pools.map((p) => [p.key, { key: p.key, currency: p.currency, seed: num(p.seed), in: [], out: [], rate: [], variance: 0 }]));
|
|
147
153
|
// Each lottery's pot is an account of its own, counted here as a pool named "lottery:<key>".
|
|
148
154
|
for (const l of m.lotteries) pools.set(`lottery:${l.key}`, { key: `lottery:${l.key}`, pot: l.name, currency: l.currency ?? game, seed: num(l.seed), in: [], out: [], rate: [], variance: 0 });
|
|
155
|
+
// Each farm's rewards vault is an account of its own too, counted as a pool named "farm:<key>".
|
|
156
|
+
for (const f of m.farms) pools.set(`farm:${f.key}`, { key: `farm:${f.key}`, farm: f.name, currency: f.rewardsToken, seed: num(f.seed), in: [], out: [], rate: [], variance: 0 });
|
|
149
157
|
const flowIn = (pool, by, amount) => amount && pools.get(pool)?.in.push({ by, perDay: amount });
|
|
150
158
|
const flowOut = (pool, by, amount) => amount && pools.get(pool)?.out.push({ by, perDay: amount });
|
|
151
159
|
const rateOut = (pool, by, perDayShare, to) => perDayShare && pools.get(pool)?.rate.push({ by, perDay: perDayShare, ...(to ? { to } : {}) });
|
|
@@ -156,15 +164,16 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
156
164
|
const accounts = {};
|
|
157
165
|
const platform = { player: 0, project: 0 };
|
|
158
166
|
const notes = [];
|
|
159
|
-
/**
|
|
167
|
+
/** What a schedule or a decision pays out as a share of a pool: counted for players once each pool's level is known. */
|
|
160
168
|
const sharePrizes = [];
|
|
161
169
|
/** Shares of a pool that are burned (a schedule, a decision): counted once each pool's level is known. */
|
|
162
170
|
const shareBurns = [];
|
|
163
171
|
/** NFTs taken out of a stash a day, by each box that gives one; and the rolls of a box prizes give a day. */
|
|
164
172
|
const stashTakes = [];
|
|
165
173
|
const prizeTakes = [];
|
|
166
|
-
/** The SOL a prize's order holds while it waits (its rent
|
|
167
|
-
|
|
174
|
+
/** The SOL a prize's order holds while it waits (its rent), back to the winner at settle. Whoever settles it pays a
|
|
175
|
+
* collectible's rent. */
|
|
176
|
+
const orderHeld = (box) => orderRent(m.box.get(box), m);
|
|
168
177
|
/**
|
|
169
178
|
* A route gives prize line `line` `times` a day (prize.rs), `payer` ({ rent, oracle }) paying what it opens: a
|
|
170
179
|
* collectible's rent, the winner's token account (the bound, not the average), a roll's order (back to the winner at
|
|
@@ -183,10 +192,6 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
183
192
|
const token = prizePool(line) ? m.pool.get(prizePool(line))?.currency : line.currency ?? game;
|
|
184
193
|
if (prizePool(line)) flowOut(prizePool(line), by, times * num(line.amount));
|
|
185
194
|
add(players.get, token, times * num(line.amount));
|
|
186
|
-
} else if (line.gives === 'share') {
|
|
187
|
-
each = TOKEN_ACCOUNT_RENT;
|
|
188
|
-
rateOut(line.pool, by, times * ((toBps(line.percent) ?? 0) / 10_000));
|
|
189
|
-
sharePrizes.push({ pool: line.pool, rate: times * ((toBps(line.percent) ?? 0) / 10_000) });
|
|
190
195
|
} else if (line.gives === 'nft') {
|
|
191
196
|
stashTakes.push({ stash: line.stash, perDay: times, by });
|
|
192
197
|
} else if (line.gives === 'roll') {
|
|
@@ -212,10 +217,11 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
212
217
|
notes.push(sold.last >= 10 * num(part.price) ? `${said} It ends ${sold.last >= PRICE_CAP ? 'beyond any price' : `${round(sold.last / num(part.price), 1)} times`} where it started: players stop buying long before that. A smaller growth keeps it in reach.` : said);
|
|
213
218
|
};
|
|
214
219
|
|
|
215
|
-
/** Sends `amount` of `token` a day to one destination (model `destination`): a pool, a lottery's pot, a burn, the treasury or an account. */
|
|
220
|
+
/** Sends `amount` of `token` a day to one destination (model `destination`): a pool, a lottery's pot, a farm's rewards vault, a burn, the treasury or an account. */
|
|
216
221
|
const sendTo = (d, token, amount, by) => {
|
|
217
222
|
if (d.kind === 'pool') flowIn(d.key, by, amount);
|
|
218
223
|
else if (d.kind === 'lottery') flowIn(`lottery:${d.key}`, by, amount);
|
|
224
|
+
else if (d.kind === 'farm') flowIn(`farm:${d.key}`, by, amount);
|
|
219
225
|
else if (d.kind === 'burn') add(burned, token, amount);
|
|
220
226
|
else if (d.kind === 'treasury') add(creator.treasury, token, amount);
|
|
221
227
|
else if (d.kind === 'chosen') add(accounts, d.key ? `the holder of the ${m.item.get(d.key)?.name ?? d.key} the buyer names` : `the wallet the ${String(by).startsWith('payment:') ? 'payer' : 'buyer'} names`, amount);
|
|
@@ -238,6 +244,76 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
238
244
|
players.network += count * signatures * NETWORK_FEE;
|
|
239
245
|
};
|
|
240
246
|
|
|
247
|
+
/**
|
|
248
|
+
* What box `b`'s items with a supply, stashes and named NFTs come to over the scenario, with `opens` opens a day and
|
|
249
|
+
* its own opens alone taking from them (pools are walked with their own rows): when an outcome runs out, a box that
|
|
250
|
+
* stops selling stops (`stopsSellingInDays`), and one that draws among what is left goes on with the rest's chances
|
|
251
|
+
* rising (`runsOut`: each outcome's day, and every chance from then), until nothing in it is left. An item whose odds
|
|
252
|
+
* follow its stock (`stockOdds`) never runs out here: its chance shrinks instead. Null when nothing it gives runs out.
|
|
253
|
+
*/
|
|
254
|
+
const stockWalk = (b, opens) => {
|
|
255
|
+
const w = weights(b);
|
|
256
|
+
const draws = drawsOf(b);
|
|
257
|
+
// What each stock holds at the start, and what one draw of each outcome takes from it.
|
|
258
|
+
const left = new Map();
|
|
259
|
+
const stockOf = (x) => {
|
|
260
|
+
const supply = Number(m.item.get(x.item)?.supply ?? 0);
|
|
261
|
+
if (x.gives === 'item' && supply > 0 && !b.stockOdds) return [`item:${x.item}`, supply];
|
|
262
|
+
if (x.gives === 'stash' && m.stash.get(x.stash)) return [`stash:${x.stash}`, (m.stash.get(x.stash).nfts ?? []).length];
|
|
263
|
+
if (x.gives === 'nft') return [`nft:${x.nft}`, 1];
|
|
264
|
+
return null;
|
|
265
|
+
};
|
|
266
|
+
const needs = (b.outcomes ?? []).map((o) => {
|
|
267
|
+
const need = new Map();
|
|
268
|
+
for (const [key, holds] of outcomeParts(o).map(stockOf).filter(Boolean)) {
|
|
269
|
+
left.set(key, holds);
|
|
270
|
+
need.set(key, (need.get(key) ?? 0) + 1);
|
|
271
|
+
}
|
|
272
|
+
return need;
|
|
273
|
+
});
|
|
274
|
+
if (!left.size || !(opens > 0)) return null;
|
|
275
|
+
const stops = b.whenOut !== 'drawLeft';
|
|
276
|
+
const there = (i) => [...needs[i]].every(([k, n]) => left.get(k) >= n);
|
|
277
|
+
const chances = (alive) => {
|
|
278
|
+
const sum = alive.reduce((n, i) => n + w[i], 0);
|
|
279
|
+
return w.map((x, i) => (sum && alive.includes(i) ? round((x / sum) * 100, 6) : 0));
|
|
280
|
+
};
|
|
281
|
+
let alive = w.map((_, i) => i).filter(there);
|
|
282
|
+
let day = 0;
|
|
283
|
+
const runsOut = [];
|
|
284
|
+
const out = (i) => ({ outcome: i, gives: (b.outcomes[i].gives === 'bundle' ? (b.outcomes[i].prizes ?? []).map((x) => x.item ?? x.stash ?? x.gives) : [b.outcomes[i].item ?? b.outcomes[i].stash ?? b.outcomes[i].gives]).join(' + ') });
|
|
285
|
+
const gone = w.map((_, i) => i).filter((i) => !alive.includes(i));
|
|
286
|
+
if (gone.length) {
|
|
287
|
+
if (stops) return { whenOut: 'stopSales', stopsSellingInDays: 0, by: gone.map(out) };
|
|
288
|
+
runsOut.push({ inDays: 0, outcomes: gone.map(out), chancesAfter: chances(alive) });
|
|
289
|
+
}
|
|
290
|
+
while (alive.length) {
|
|
291
|
+
const sum = alive.reduce((n, i) => n + w[i], 0);
|
|
292
|
+
// Each stock's take a day, by the outcomes still there.
|
|
293
|
+
const rate = new Map();
|
|
294
|
+
for (const i of alive) for (const [k, n] of needs[i]) rate.set(k, (rate.get(k) ?? 0) + (opens * draws * w[i] * n) / sum);
|
|
295
|
+
const next = [...rate].map(([k, r]) => ({ k, t: left.get(k) / r })).sort((a, z) => a.t - z.t)[0];
|
|
296
|
+
if (!next || day + next.t > days) break;
|
|
297
|
+
day += next.t;
|
|
298
|
+
for (const [k, r] of rate) left.set(k, Math.max(0, left.get(k) - r * next.t));
|
|
299
|
+
left.set(next.k, 0);
|
|
300
|
+
const ended = alive.filter((i) => !there(i));
|
|
301
|
+
alive = alive.filter(there);
|
|
302
|
+
if (stops) return { whenOut: 'stopSales', stopsSellingInDays: round(day, 4), by: ended.map(out) };
|
|
303
|
+
runsOut.push({ inDays: round(day, 4), outcomes: ended.map(out), chancesAfter: chances(alive) });
|
|
304
|
+
}
|
|
305
|
+
if (stops) return null;
|
|
306
|
+
return runsOut.length ? { whenOut: 'drawLeft', runsOut, ...(alive.length ? {} : { stopsSellingInDays: runsOut.at(-1).inDays }) } : null;
|
|
307
|
+
};
|
|
308
|
+
/** The walk of a box's stock in words (`stockWalk`). */
|
|
309
|
+
const stockNote = (b, walk) => {
|
|
310
|
+
const named = (list) => list.map((x) => `outcome ${x.outcome} (${x.gives})`).join(', ');
|
|
311
|
+
if (walk.whenOut === 'stopSales')
|
|
312
|
+
return `“${b.name}” stops selling ${walk.stopsSellingInDays ? `in about ${walk.stopsSellingInDays} days` : 'from the start'}, when ${named(walk.by)} runs out (its own opens alone): it isn’t sold while any outcome couldn’t cover a purchase and those still waiting for the oracle ("whenOut": "stopSales"), so every roll is on the odds as written. Restock it, or edit the box, to sell again; "drawLeft" would keep selling, the rest’s chances rising.`;
|
|
313
|
+
const steps = walk.runsOut.map((r) => `${named(r.outcomes)} ${r.inDays ? `in about ${r.inDays} days` : 'from the start'}${r.chancesAfter.some((c) => c > 0) ? `, then ${r.chancesAfter.map((c, i) => (c > 0 ? `outcome ${i} ${round(c, 2)}%` : null)).filter(Boolean).join(', ')}` : ''}`);
|
|
314
|
+
return `“${b.name}” keeps selling as its outcomes run out ("whenOut": "drawLeft"; its own opens alone): ${steps.join('; ')}${walk.stopsSellingInDays !== undefined ? `; with nothing left, it stops selling` : ''}. Each roll draws among what is left by the written weights, so a player can empty a common outcome to make a rare one likelier; "stopSales" keeps the odds as written.`;
|
|
315
|
+
};
|
|
316
|
+
|
|
241
317
|
// ---- boxes
|
|
242
318
|
const boxes = m.boxes.map((b) => {
|
|
243
319
|
const token = b.currency ?? game;
|
|
@@ -248,7 +324,6 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
248
324
|
const w = weights(b);
|
|
249
325
|
const total = w.reduce((a, x) => a + x, 0) || 1;
|
|
250
326
|
let value = 0;
|
|
251
|
-
let burnChance = 0;
|
|
252
327
|
const unpriced = [];
|
|
253
328
|
const other = {};
|
|
254
329
|
// One part of what an outcome gives (model `outcomeParts`), landing with chance `p`: its row, and its flows.
|
|
@@ -266,13 +341,6 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
266
341
|
Object.assign(row, { amount: num(o.amount), token: prizeToken, minted: true });
|
|
267
342
|
if (prizeToken === token) value += p * num(o.amount);
|
|
268
343
|
else add(other, prizeToken, p * num(o.amount));
|
|
269
|
-
} else if (o.gives === 'share') {
|
|
270
|
-
const bps = toBps(o.percent) ?? 0;
|
|
271
|
-
const prizeToken = m.pool.get(o.pool)?.currency;
|
|
272
|
-
Object.assign(row, { pool: o.pool, percent: o.percent, token: prizeToken });
|
|
273
|
-
rateOut(o.pool, `box:${b.key}`, opens * p * (bps / 10_000));
|
|
274
|
-
sharePrizes.push({ pool: o.pool, rate: opens * p * (bps / 10_000), box: b.key, perOpen: p * (bps / 10_000) });
|
|
275
|
-
row.shareOfPool = true;
|
|
276
344
|
} else if (o.gives === 'item') {
|
|
277
345
|
const it = m.item.get(o.item);
|
|
278
346
|
const worth = it?.value !== undefined ? num(it.value) : num(it?.price) > 0 ? num(it.price) : null;
|
|
@@ -297,13 +365,11 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
297
365
|
const outcomes = (b.outcomes ?? []).map((o, i) => {
|
|
298
366
|
const p = w[i] / total;
|
|
299
367
|
const parts = outcomeParts(o).map((part) => partRow(part, p));
|
|
300
|
-
if (
|
|
301
|
-
if (o.gives === 'bundle') return { gives: 'bundle', chance: round(p * 100, 6), prizes: parts, ...(o.burns ? { burns: true } : {}) };
|
|
368
|
+
if (o.gives === 'bundle') return { gives: 'bundle', chance: round(p * 100, 6), prizes: parts };
|
|
302
369
|
return parts[0] ?? { gives: o.gives, chance: round(p * 100, 6) };
|
|
303
370
|
});
|
|
304
371
|
// Every part of every outcome, each with its outcome's chance.
|
|
305
372
|
const partsOf = outcomes.flatMap((o) => (o.gives === 'bundle' ? o.prizes : [o]));
|
|
306
|
-
const shares = partsOf.filter((o) => o.shareOfPool);
|
|
307
373
|
// How much the prizes swing from day to day, per pool: opens × (E[prize²] − E[prize]²).
|
|
308
374
|
for (const key of new Set(partsOf.filter((o) => o.gives === 'pay').map((o) => o.pool))) {
|
|
309
375
|
const rows = partsOf.filter((o) => o.gives === 'pay' && o.pool === key);
|
|
@@ -311,31 +377,30 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
311
377
|
const square = rows.reduce((n, o) => n + (o.chance / 100) * o.amount ** 2, 0);
|
|
312
378
|
if (pools.has(key)) pools.get(key).variance += opens * (square - mean ** 2);
|
|
313
379
|
}
|
|
314
|
-
// Money: the payment is split
|
|
315
|
-
carryOut(b.split, token, opens * price
|
|
316
|
-
add(burned, token, opens * price * burnChance);
|
|
380
|
+
// Money: the payment is split (a share may burn it).
|
|
381
|
+
carryOut(b.split, token, opens * price, `box:${b.key}`);
|
|
317
382
|
add(players.pay, token, opens * price);
|
|
318
383
|
add(players.get, token, opens * value);
|
|
319
384
|
for (const [t, v] of Object.entries(other)) add(players.get, t, opens * v);
|
|
320
|
-
// Two signatures: the open, then the settle that carries the outcome out
|
|
321
|
-
// that needs no roll is bought in one (`buy`), with no order;
|
|
322
|
-
|
|
323
|
-
const rolled = boxRolls(b);
|
|
324
|
-
const bought = boxBuys(b);
|
|
385
|
+
// Two signatures: the open, then the settle that carries the outcome out (a throwaway key the open funds sends it).
|
|
386
|
+
// A box that needs no roll is bought in one (`buy`), with no order; every open asks the oracle.
|
|
387
|
+
const bought = boxBuys(b, m);
|
|
325
388
|
const signatures = bought ? 1 : 2;
|
|
326
389
|
playerAction(opens, signatures);
|
|
327
|
-
if (
|
|
328
|
-
const collectibles = partsOf.filter((o) => o
|
|
390
|
+
if (!bought) players.oracle += opens * ORACLE;
|
|
391
|
+
const collectibles = drawsOf(b) * partsOf.filter((o) => holdsRent(o, m) && m.item.get(o.item)?.kind !== 'normal').reduce((n, o) => n + o.chance / 100, 0);
|
|
329
392
|
players.rent += opens * collectibles * COLLECTIBLE_RENT;
|
|
330
|
-
const known = !unpriced.length && !
|
|
393
|
+
const known = !unpriced.length && !Object.keys(other).length;
|
|
331
394
|
// What one open really takes from the player's wallet: the price, the platform's fee, the oracle's and the network's.
|
|
332
395
|
const isSol = Boolean(m.tokens.get(token)?.wsol);
|
|
333
|
-
const oracle =
|
|
334
|
-
// A collectible's rent comes out of the SOL the
|
|
335
|
-
const
|
|
336
|
-
const realCost = realCostOf({ price, token, isSol, oracle, signatures, rent: collectibles * COLLECTIBLE_RENT, rentIs: collectibles < 1 ? 'the collectible’s rent, when it gives one' : 'the collectible’s rent', held: bought ? 0 : sol(ESCROW_LAMPORTS) * items + ORDER_RENT });
|
|
396
|
+
const oracle = bought ? 0 : ORACLE;
|
|
397
|
+
// A collectible's rent comes out of the SOL the open lends its settle key: on average, the chance of one.
|
|
398
|
+
const realCost = realCostOf({ price, token, isSol, oracle, signatures, rent: collectibles * COLLECTIBLE_RENT, rentIs: collectibles < 1 ? 'the collectible’s rent, when it gives one' : 'the collectible’s rent', held: bought ? 0 : sol(SETTLE_RENT_UNIT) * settleUnits(b, m) + orderRent(b, m, true) });
|
|
337
399
|
const extrasSol = realCost.extrasSol;
|
|
338
400
|
priceNote(b, sold);
|
|
401
|
+
// What it gives that runs out, as the box says (`whenOut`): it stops selling, or draws among what is left.
|
|
402
|
+
const walk = !bought && b.active !== false ? stockWalk(b, opens) : null;
|
|
403
|
+
if (walk) notes.push(stockNote(b, walk));
|
|
339
404
|
return {
|
|
340
405
|
key: b.key,
|
|
341
406
|
name: b.name,
|
|
@@ -352,6 +417,10 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
352
417
|
playerFee: PLAYER_FEE,
|
|
353
418
|
oracleFee: oracle,
|
|
354
419
|
realCost,
|
|
420
|
+
// What it gives can run out, and what the box does then (`whenOut`, `whenOutWords`).
|
|
421
|
+
...(!bought && outcomes.flatMap((o) => (o.gives === 'bundle' ? o.prizes : [o])).some((o) => o.gives === 'pay' || o.gives === 'stash' || o.gives === 'nft' || (o.gives === 'item' && Number(m.item.get(o.item)?.supply ?? 0) > 0)) ? { limitedStock: true } : {}),
|
|
422
|
+
...(!bought ? { whenOut: b.whenOut === 'drawLeft' ? 'drawLeft' : 'stopSales' } : {}),
|
|
423
|
+
...(walk ? { stock: walk } : {}),
|
|
355
424
|
...(isSol && known && price ? { returnOfRealCostPercent: round((value / (price + extrasSol)) * 100, 4) } : {}),
|
|
356
425
|
};
|
|
357
426
|
});
|
|
@@ -428,10 +497,6 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
428
497
|
} else if (line.gives === 'nft') {
|
|
429
498
|
gives.push(prizeWords(line, m));
|
|
430
499
|
stashTakes.push({ stash: line.stash, perDay: crafts, by: `recipe:${r.key}` });
|
|
431
|
-
} else if (line.gives === 'share') {
|
|
432
|
-
gives.push(`${line.percent}% of ${line.pool}`);
|
|
433
|
-
rateOut(line.pool, `recipe:${r.key}`, crafts * ((toBps(line.percent) ?? 0) / 10_000));
|
|
434
|
-
sharePrizes.push({ pool: line.pool, rate: crafts * ((toBps(line.percent) ?? 0) / 10_000) });
|
|
435
500
|
}
|
|
436
501
|
}
|
|
437
502
|
// The player's is the only signature: what a craft makes is at its item's and serial's address.
|
|
@@ -448,7 +513,7 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
448
513
|
creator.network += runs * NETWORK_FEE; // run_schedule is free of the program's fee; whoever sends it pays the network
|
|
449
514
|
if (s.does === 'issue') {
|
|
450
515
|
const amount = num(s.amount);
|
|
451
|
-
flowIn(s.pool, `schedule:${s.key}`, runs * amount);
|
|
516
|
+
flowIn(destination(s.to ?? '').kind === 'farm' ? s.to : s.pool, `schedule:${s.key}`, runs * amount);
|
|
452
517
|
const halving = s.halveEvery ? `, halving every ${s.halveEvery} runs (every ${round((s.halveEvery * every) / DAY, 2)} days)` : '';
|
|
453
518
|
// Halving every h runs: h runs at the amount, h at half, … (2 × h × amount in all, with no end); with
|
|
454
519
|
// maxRuns, the full halvings it reaches and the runs after the last.
|
|
@@ -496,7 +561,7 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
496
561
|
return { key: s.key, does: s.does, percentPerDay: round(runs * s.percent, 6), says: `${where} ${s.percent}% of ${from} every ${round(every / 3600, 3)} h` };
|
|
497
562
|
});
|
|
498
563
|
|
|
499
|
-
// ---- stakes: everything that arrives in
|
|
564
|
+
// ---- stakes: everything that arrives in a farm’s rewards vault goes to its stakers
|
|
500
565
|
// ---- lotteries: in steady state each round pays out what came in (what the places leave rolls over). One that draws
|
|
501
566
|
// once (no `every`) has one round, from going live to its first draw (the scenario's end at the latest): its tickets,
|
|
502
567
|
// its draw and its pot happen once, spread over the scenario's days like an auction's sale.
|
|
@@ -525,7 +590,9 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
525
590
|
carryOut(split, potToken, tickets * price, `lottery:${l.key}`, `lottery:${l.key}`);
|
|
526
591
|
}
|
|
527
592
|
playerAction(tickets);
|
|
528
|
-
|
|
593
|
+
// A lottery that holds SOL for its rounds (`roundRent`, on by default) pays each entry's bytes: the buyer pays none.
|
|
594
|
+
const entryRent = roundRentOf(l, m) > 0n ? 0 : ticketLine(l);
|
|
595
|
+
players.rent += tickets * entryRent;
|
|
529
596
|
const placed = (l.places ?? [100]).map(placeOf);
|
|
530
597
|
const places = placed.map((p) => Number(p.percent));
|
|
531
598
|
creator.network += (1 + places.length) * NETWORK_FEE * drawsPerDay; // the draw and each place's payment
|
|
@@ -542,8 +609,8 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
542
609
|
const prized = placed.filter((p) => prizeLine(p.prize)).map((p) => worthOf(prizeLine(p.prize)));
|
|
543
610
|
// One ticket bought on its own: the fee and the record are per purchase, so a bundle of tickets shares them. The
|
|
544
611
|
// record's rent comes back once the round is done with it, and a no-loss ticket's price after the draw: both held.
|
|
545
|
-
const realCost = realCostOf({ price, token: ticketToken, isSol: Boolean(m.tokens.get(ticketToken)?.wsol),
|
|
546
|
-
return { key: l.key, name: l.name, price, token: ticketToken, potToken, ...(l.minSold ? { minSold: l.minSold } : {}), ticketsPerDay: round(tickets, 6), ticketsPerRound: round(perRound, 4), roundDays: round(every / DAY, 4), places, placePrizes: placed.map((p) => (p.prize ? prizeWords(p.prize, m) : null)), prizeWorthPerRound: round(prized.reduce((n, w) => n + (w ?? 0), 0)), ...(prized.includes(null) ? { unpricedPrizes: true } : {}), cutPercent: num(l.cutPercent), splitPercent: round(cut * 100, 4), drawsOnce: once, ...(l.ticketNft ? { ticketNft: true } : {}), ...(l.noLoss ? { noLoss: true } : {}), realCost };
|
|
612
|
+
const realCost = realCostOf({ price, token: ticketToken, isSol: Boolean(m.tokens.get(ticketToken)?.wsol), rent: entryRent, rentIs: 'the purchase’s entry in the round', ...(l.noLoss ? { heldIs: 'the ticket’s price' } : {}), priceBack: Boolean(l.noLoss) });
|
|
613
|
+
return { key: l.key, name: l.name, price, token: ticketToken, potToken, ...(l.minSold ? { minSold: l.minSold } : {}), ticketsPerDay: round(tickets, 6), ticketsPerRound: round(perRound, 4), roundDays: round(every / DAY, 4), places, placePrizes: placed.map((p) => (p.prize ? prizeWords(p.prize, m) : null)), prizeWorthPerRound: round(prized.reduce((n, w) => n + (w ?? 0), 0)), ...(prized.includes(null) ? { unpricedPrizes: true } : {}), cutPercent: num(l.cutPercent), splitPercent: round(cut * 100, 4), drawsOnce: once, roundRentSol: round(Number(roundRentOf(l, m)) / 1e9), ...(l.ticketNft ? { ticketNft: true } : {}), ...(l.noLoss ? { noLoss: true } : {}), realCost };
|
|
547
614
|
});
|
|
548
615
|
|
|
549
616
|
// ---- decisions with a known rate
|
|
@@ -619,15 +686,15 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
619
686
|
};
|
|
620
687
|
});
|
|
621
688
|
for (const f of m.farms) playerAction(perDay('farm', f.key));
|
|
622
|
-
// Grants: each claim's sender pays the player fee; its
|
|
689
|
+
// Grants: each claim's sender pays the player fee; its records' rent comes from the grant's funds (the owner's SOL, back when its shards close).
|
|
623
690
|
const grants = m.grants.map((g) => {
|
|
624
691
|
const claims = num(m.scenario.perDay?.[`grant:${g.key}`]);
|
|
625
692
|
playerAction(claims);
|
|
626
|
-
creator.rent = (creator.rent ?? 0) + claims *
|
|
693
|
+
creator.rent = (creator.rent ?? 0) + claims * claimRecordsRent(g);
|
|
627
694
|
// Its prize, paid for by the grant's funds; a roll's order goes back to the winner at settle.
|
|
628
695
|
const line = prizeLine(g.prize ?? g.gives);
|
|
629
696
|
const each = givePrize(line, claims, `grant:${g.key}`, creator);
|
|
630
|
-
return { key: g.key, name: g.name, claimsPerDay: claims, gives: prizeWords(line, m), fundsPerClaimSol: round(
|
|
697
|
+
return { key: g.key, name: g.name, claimsPerDay: claims, gives: prizeWords(line, m), fundsPerClaimSol: round(claimRecordsRent(g) + each), ...(line?.gives === 'roll' ? { orderHeldSol: round(orderHeld(line.box)) } : {}) };
|
|
631
698
|
});
|
|
632
699
|
for (const h of m.holds) playerAction(perDay('hold', h.key));
|
|
633
700
|
for (const c of m.contests) playerAction(perDay('contest', c.key));
|
|
@@ -653,48 +720,48 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
653
720
|
p.inflow = inflow;
|
|
654
721
|
}
|
|
655
722
|
|
|
656
|
-
// Farms: everything that arrives in
|
|
657
|
-
// longest lock, a day at least), by weight (stake × tier × boosts) as it is released; or a fixed rate for each seat
|
|
658
|
-
// (minted, or from the
|
|
659
|
-
|
|
723
|
+
// Farms: everything that arrives in a farm's rewards vault is released to the seats over the farm's release span (its
|
|
724
|
+
// longest lock, a day at least), by weight (stake × tier × boosts) as it is released; or a fixed rate for each seat,
|
|
725
|
+
// or with `shared` for the whole farm (minted, or from the vault while it lasts). In a steady state the release pays
|
|
726
|
+
// what arrives, a day at a time.
|
|
727
|
+
/** A farm's ways out, in words (farm.rs): an early-exit fee, compounding; or null. */
|
|
660
728
|
const farmExtras = (f) => {
|
|
661
|
-
const exits = (f.tiers ?? []).map((t, i) => (t.exitFee ? `tier ${i + 1} may leave before its ${t.lock} lock by leaving ${t.exitFee}% of what it takes out (${f.exitFeeTo === 'burn' ? 'burned' : `to ${String(f.exitFeeTo).replace(/^pool:/, '')}`})` : null)).filter(Boolean);
|
|
729
|
+
const exits = (f.tiers ?? []).map((t, i) => (t.exitFee ? `tier ${i + 1} may leave before its ${t.lock} lock by leaving ${t.exitFee}% of what it takes out (${f.exitFeeTo === 'burn' ? 'burned' : f.exitFeeTo === 'rewards' ? 'into its own rewards vault' : `to ${String(f.exitFeeTo).replace(/^pool:/, '')}`}), keeping the rewards it was credited` : null)).filter(Boolean);
|
|
662
730
|
const parts = [
|
|
663
731
|
...exits,
|
|
664
|
-
f.
|
|
665
|
-
f.
|
|
666
|
-
f.stakes !== 'collectibles' && m.pool.get(f.rewards)?.currency === f.stakes && 'its rewards can be compounded into the stake without restarting the lock',
|
|
667
|
-
f.unpaid && 'it pays nothing: staking there only counts (a stake gate reads it)',
|
|
732
|
+
f.stakes !== 'collectibles' && f.rewardsToken === f.stakes && 'its rewards can be compounded into the stake without restarting the lock',
|
|
733
|
+
(f.tiers ?? []).some((t) => t.earnsFor) && `a seat earns only for its tier’s term (${(f.tiers ?? []).map((t) => t.earnsFor ?? 'no end').join(', ')}) after its lock started`,
|
|
668
734
|
].filter(Boolean);
|
|
669
735
|
return parts.length ? parts.join('; ') : null;
|
|
670
736
|
};
|
|
671
|
-
/** What a seat of `f` can give
|
|
672
|
-
const giveBacks = (f) => ['a forfeit',
|
|
737
|
+
/** What a seat of `f` can give up (farm.rs `Farm::give`): a forfeit, a watched NFT gone. */
|
|
738
|
+
const giveBacks = (f) => ['a forfeit', f.custody === 'watch' && 'a watched NFT sold'].filter(Boolean).join(', ');
|
|
673
739
|
const farmRows = m.farms.map((f) => {
|
|
674
|
-
const pool = pools.get(f.
|
|
740
|
+
const pool = pools.get(`farm:${f.key}`);
|
|
675
741
|
const staked = num(m.scenario.staked?.[f.key]);
|
|
676
742
|
const perUnit = f.rate ? num(f.rate.perDay) : null;
|
|
677
|
-
const wanted = f.rate ?
|
|
743
|
+
const wanted = !f.rate ? Infinity : f.rate.shared ? (staked ? perUnit : 0) : perUnit * staked;
|
|
678
744
|
const free = pool ? Math.max(0, pool.inflow - pool.out.reduce((n, x) => n + x.perDay, 0)) : 0;
|
|
679
745
|
const rewards = f.rate?.minted ? wanted : Math.min(wanted, f.rate ? free + (pool?.seed ?? 0) / Math.max(1, m.scenario.days ?? 30) : free);
|
|
680
746
|
if (pool && !f.rate?.minted) pool.out.push({ by: `farm:${f.key}`, perDay: rewards });
|
|
681
|
-
const sameToken =
|
|
682
|
-
const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map((t) => ({ lock: t.lock, multiplier: t.multiplier ?? 1, ...(prizeLine(t.prize) ? { prize: prizeWords(t.prize, m) } : {}) }));
|
|
747
|
+
const sameToken = f.rewardsToken === f.stakes;
|
|
748
|
+
const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map((t) => ({ lock: t.lock, multiplier: t.multiplier ?? 1, ...(t.earnsFor ? { earnsFor: t.earnsFor } : {}), ...(prizeLine(t.prize) ? { prize: prizeWords(t.prize, m) } : {}) }));
|
|
683
749
|
const unit = f.stakes === 'collectibles' ? 'collectible' : symbol(f.stakes);
|
|
684
750
|
// Growth: the days a collectible at x1 needs for each level.
|
|
685
751
|
const growth = listOf(f.growth).map((g) => {
|
|
686
752
|
const levels = (g.levels ?? []).map((points, i) => ({ level: i + 1, points, days: round(points / g.perDay, 2) }));
|
|
687
753
|
return { attribute: g.attribute ?? 'xp', perDay: g.perDay, ...(levels.length ? { level: g.level ?? 'level', levels } : {}), streak: Boolean(g.streak) };
|
|
688
754
|
});
|
|
755
|
+
const perSeat = !f.rate ? null : f.rate.shared ? (staked ? perUnit / staked : null) : perUnit;
|
|
689
756
|
return {
|
|
690
757
|
key: f.key,
|
|
691
758
|
name: f.name,
|
|
692
759
|
stakes: f.stakes,
|
|
693
|
-
rewardsToken:
|
|
760
|
+
rewardsToken: f.rewardsToken,
|
|
694
761
|
rewardsPerDay: round(rewards),
|
|
695
|
-
...(f.rate ? { rate: { perDay: perUnit, minted: Boolean(f.rate.minted) } } : {}),
|
|
762
|
+
...(f.rate ? { rate: { perDay: perUnit, minted: Boolean(f.rate.minted), ...(f.rate.shared ? { shared: true } : {}), ...(f.rate.halveEvery ? { halveEvery: f.rate.halveEvery } : {}) } } : {}),
|
|
696
763
|
tiers,
|
|
697
|
-
...(staked ? { staked, perUnitPerDay: round(f.rate ?
|
|
764
|
+
...(staked ? { staked, perUnitPerDay: round(f.rate ? perSeat : rewards / staked), unit } : f.rate && !f.rate.shared ? { perUnitPerDay: perUnit, unit } : {}),
|
|
698
765
|
...(staked && sameToken ? { yearlyPercent: round(((rewards * 365) / staked) * 100, 4) } : {}),
|
|
699
766
|
...(growth.length ? { growth } : {}),
|
|
700
767
|
...(f.runs ? { runs: f.runs } : {}),
|
|
@@ -703,8 +770,8 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
703
770
|
...(f.upkeepAttribute ? { upkeepAttribute: f.upkeepAttribute } : {}),
|
|
704
771
|
...(farmExtras(f) ? { extras: farmExtras(f) } : {}),
|
|
705
772
|
says: f.rate
|
|
706
|
-
? `each seat earns ${perUnit} a day for each ${unit} at x1, whoever else is in${f.rate.minted ? ' (minted, as far as the mint has room for: what it can’t mint is not owed later)' : ', while
|
|
707
|
-
: `what lands in
|
|
773
|
+
? `${f.rate.shared ? `the farm pays ${perUnit} a day in all, shared by weight while anything is staked` : `each seat earns ${perUnit} a day for each ${unit} at x1, whoever else is in`}${f.rate.halveEvery ? `, halving every ${f.rate.halveEvery}` : ''}${f.rate.minted ? ' (minted when a seat is paid, as far as the mint has room for: what it can’t mint is not owed later)' : ', while its vault lasts'}${perUnit > 0 ? `; what a seat gives back (${giveBacks(f)}) is ${f.rate.minted ? 'never minted' : 'left free in its vault'}, never to the other seats` : ''}`
|
|
774
|
+
: `what lands in its rewards vault is released over ${releaseWords(f)} to the seats there as it is released, so a stake earns only while it stays; ${tiers.length > 1 || (f.boosts ?? []).length ? 'shares are by weight: a higher tier or a boosted collectible takes more of the same rewards, so the figures are for a seat at x1 among seats at x1' : 'shares are by what each seat stakes'}`,
|
|
708
775
|
};
|
|
709
776
|
});
|
|
710
777
|
for (const row of farmRows) add(players.get, row.rewardsToken, row.rewardsPerDay);
|
|
@@ -739,6 +806,7 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
739
806
|
const row = {
|
|
740
807
|
key: p.key,
|
|
741
808
|
...(p.pot ? { pot: p.pot } : {}),
|
|
809
|
+
...(p.farm ? { farm: p.farm } : {}),
|
|
742
810
|
token: p.currency,
|
|
743
811
|
seed: p.seed,
|
|
744
812
|
inPerDay: round(inflow),
|
|
@@ -776,7 +844,7 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
776
844
|
row.swingPerDay = round(Math.sqrt(p.variance), 6);
|
|
777
845
|
const weekly = Math.sqrt(p.variance * 7);
|
|
778
846
|
if (row.lasts !== 'runs dry' && p.seed < 3 * weekly)
|
|
779
|
-
notes.push(`The pool “${p.key}” ${row.lasts === 'grows' ? 'takes in more than it pays on average' : row.lasts === 'settles' ? 'settles on average' : 'pays even on average'}, but luck moves it by about ±${round(row.swingPerDay, 4)} ${symbol(p.currency)} a day (±${round(weekly, 4)} a week), against a seed of ${p.seed}. While it can’t cover a fixed prize
|
|
847
|
+
notes.push(`The pool “${p.key}” ${row.lasts === 'grows' ? 'takes in more than it pays on average' : row.lasts === 'settles' ? 'settles on average' : 'pays even on average'}, but luck moves it by about ±${round(row.swingPerDay, 4)} ${symbol(p.currency)} a day (±${round(weekly, 4)} a week), against a seed of ${p.seed}. While it can’t cover a fixed prize, a box that stops selling when out (the default) sells nothing, and one that draws among what is left never lands on that prize, so a bad week stalls sales or takes the big wins away until it refills. A seed of about ${round(3 * weekly, 2)} (three times a week’s swing) rides out almost any week.`);
|
|
780
848
|
}
|
|
781
849
|
if (row.lasts === 'runs dry') notes.push(`The pool “${p.key}” pays out more than comes in: it lasts about ${row.lastsDays} days from its seed of ${p.seed} ${symbol(p.currency)}.`);
|
|
782
850
|
return row;
|
|
@@ -787,10 +855,8 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
787
855
|
const level = row ? (row.settlesAt ?? row.seed) : 0;
|
|
788
856
|
if (prize.account) sendTo(destination(prize.account), row?.token, prize.rate * level, prize.by);
|
|
789
857
|
else add(players.get, row?.token, prize.rate * level);
|
|
790
|
-
const box = prize.box && boxes.find((b) => b.key === prize.box);
|
|
791
|
-
if (box) add(box.alsoPays, row?.token, round(prize.perOpen * level));
|
|
792
858
|
}
|
|
793
|
-
// A share burned is burned at the pool's level, as a share
|
|
859
|
+
// A share burned is burned at the pool's level, as a share paid out is.
|
|
794
860
|
for (const b of shareBurns) {
|
|
795
861
|
const row = poolRows.find((r) => r.key === b.pool);
|
|
796
862
|
add(burned, row?.token, b.rate * (row ? (row.settlesAt ?? row.seed) : 0));
|
|
@@ -819,8 +885,8 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
819
885
|
const holds = (s.nfts ?? []).length;
|
|
820
886
|
const net = perDay - inPerDay;
|
|
821
887
|
const filled = inPerDay > 0 ? `, and takes in about ${round(inPerDay, 4)} a day (${[...new Set(fills.map((t) => t.by))].join(', ')})` : '';
|
|
822
|
-
if (perDay > 0 && net > 0) notes.push(`The stash “${s.name}” starts with ${holds} NFT${holds === 1 ? '' : 's'} and gives about ${round(perDay, 4)} a day (${[...new Set(takes.map((t) => t.by))].join(', ')})${filled}: ${holds ? `empty in about ${round(holds / net, 2)} days` : 'empty from the start'}, after which its outcome
|
|
823
|
-
else if (perDay > 0) notes.push(`The stash “${s.name}” starts with ${holds} NFT${holds === 1 ? '' : 's'}, gives about ${round(perDay, 4)} a day (${[...new Set(takes.map((t) => t.by))].join(', ')})${filled}: it keeps up${holds ? '' : ', once the first come in (until then its outcome
|
|
888
|
+
if (perDay > 0 && net > 0) notes.push(`The stash “${s.name}” starts with ${holds} NFT${holds === 1 ? '' : 's'} and gives about ${round(perDay, 4)} a day (${[...new Set(takes.map((t) => t.by))].join(', ')})${filled}: ${holds ? `empty in about ${round(holds / net, 2)} days` : 'empty from the start'}, after which a draw that lands on its outcome is not made, and gives back that draw’s worth, until more are put in.`);
|
|
889
|
+
else if (perDay > 0) notes.push(`The stash “${s.name}” starts with ${holds} NFT${holds === 1 ? '' : 's'}, gives about ${round(perDay, 4)} a day (${[...new Set(takes.map((t) => t.by))].join(', ')})${filled}: it keeps up${holds ? '' : ', once the first come in (until then a draw that lands on its outcome is not made)'}.`);
|
|
824
890
|
return { key: s.key, name: s.name, holds, givenPerDay: round(perDay, 6), ...(inPerDay > 0 ? { takenInPerDay: round(inPerDay, 6) } : {}), ...(net > 0 ? { lastsDays: round(holds / net, 4) } : {}) };
|
|
825
891
|
});
|
|
826
892
|
const contestRolls = m.contests.filter((c) => prizeLine(c.prize)?.gives === 'roll');
|
|
@@ -851,7 +917,7 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
851
917
|
burned: totals(burned),
|
|
852
918
|
accounts: totals(accounts),
|
|
853
919
|
},
|
|
854
|
-
fees: { playerSol: PLAYER_FEE, projectSol: PROJECT_FEE, oracleSol: ORACLE, networkPerSignatureSol: NETWORK_FEE,
|
|
920
|
+
fees: { playerSol: PLAYER_FEE, projectSol: PROJECT_FEE, oracleSol: ORACLE, networkPerSignatureSol: NETWORK_FEE, settleRentUnitSol: sol(SETTLE_RENT_UNIT), ticketLineRentSol: round(ticketLine({})), collectibleRentSol: COLLECTIBLE_RENT, burnRefundSol: BURN_REFUND },
|
|
855
921
|
notes,
|
|
856
922
|
};
|
|
857
923
|
}
|
|
@@ -905,9 +971,10 @@ export function explain(sim, economy) {
|
|
|
905
971
|
const back = b.returnPercent !== null ? `${n(b.expectedBack)} ${sym(b.token)} on average (${n(b.returnPercent)}%)${also.length ? ` and ${also.join(', ')}` : ''}` : `${also.join(', ') || 'nothing'} on average`;
|
|
906
972
|
const all = `; ${inAll(b.realCost)}`;
|
|
907
973
|
lines.push(`Box “${b.name}”: costs ${n(b.price)} ${sym(b.token)} (${all.slice(2)}), gives back ${back}${edge}.`);
|
|
908
|
-
const part = (o) => (o.gives === 'pay' ? `${n(o.amount)} ${sym(o.token)} from ${o.pool}` : o.gives === 'mint' ? `${n(o.amount)} ${sym(o.token)}, minted` : o.gives === '
|
|
909
|
-
for (const o of b.outcomes) lines.push(` ${n(o.chance)}%: ${o.gives === 'bundle' ?
|
|
974
|
+
const part = (o) => (o.gives === 'pay' ? `${n(o.amount)} ${sym(o.token)} from ${o.pool}` : o.gives === 'mint' ? `${n(o.amount)} ${sym(o.token)}, minted` : o.gives === 'stash' ? `an NFT of the stash “${o.stash}”, at random` : o.gives === 'nft' ? `the NFT ${String(o.nft).slice(0, 4)}… of “${o.stash}”` : o.gives === 'roll' ? `a roll of “${o.box}”` : o.gives === 'item' ? `${o.item}${o.worth !== null ? ` (worth ${n(o.worth)})` : ' (no value given)'}` : o.gives);
|
|
975
|
+
for (const o of b.outcomes) lines.push(` ${n(o.chance)}%: ${o.gives === 'bundle' ? o.prizes.map(part).join(' + ') : part(o)}`);
|
|
910
976
|
if (b.unpriced.length) lines.push(` Give ${b.unpriced.join(', ')} a "value" to count them in the return.`);
|
|
977
|
+
if (b.limitedStock) lines.push(` ${whenOutWords(m.box.get(b.key))}.`);
|
|
911
978
|
}
|
|
912
979
|
for (const it of sim.items) lines.push(`Shop “${it.name}”: ${n(it.price)} ${sym(it.token)}; ${inAll(it.realCost)}.`);
|
|
913
980
|
for (const p of sim.payments) lines.push(`Payment “${p.name}”: ${p.atLeast ? 'at least ' : ''}${n(p.amount)} ${sym(p.token)}; ${inAll(p.realCost)}.`);
|
|
@@ -928,10 +995,10 @@ export function explain(sim, economy) {
|
|
|
928
995
|
for (const g of sim.grants) lines.push(`Grant “${g.name}”: ${n(g.claimsPerDay)} claims a day, each giving ${g.gives}; each claim costs its funds about ${n(g.fundsPerClaimSol)} SOL${g.orderHeldSol ? ` (${n(g.orderHeldSol)} of it the roll’s order, which goes to the winner when it is settled; whoever asks for the roll pays the oracle)` : ''}.`);
|
|
929
996
|
for (const s of sim.stashes ?? []) lines.push(`Stash “${s.name}”: ${s.holds} NFT${s.holds === 1 ? '' : 's'} to start${s.takenInPerDay ? `, about ${n(s.takenInPerDay)} a day come in` : ''}${s.lastsDays !== undefined ? `, given at about ${n(s.givenPerDay)} a day: lasts about ${n(s.lastsDays)} days` : s.givenPerDay ? `, given at about ${n(s.givenPerDay)} a day` : ''}.`);
|
|
930
997
|
for (const r of sim.recipes) lines.push(`Recipe “${r.name}”: ${r.takes.join(' + ') || 'nothing'} → ${r.gives.join(' + ') || 'nothing'}, ${n(r.craftsPerDay)} a day.`);
|
|
931
|
-
for (const s of sim.schedules) lines.push(`Schedule “${s.key}”: ${s.says}.`);
|
|
998
|
+
for (const s of sim.schedules) lines.push(`Schedule “${s.key}”: ${s.says}; ${lateRunWords(m.schedules.find((x) => x.key === s.key) ?? {})}.`);
|
|
932
999
|
for (const c of sim.contests) lines.push(`Contest “${c.name}”: ${c.says}.`);
|
|
933
1000
|
for (const a of sim.auctions ?? []) lines.push(`Auction “${a.name}”: ${a.says}.`);
|
|
934
|
-
for (const p of sim.pools) lines.push(`${p.pot ? `The pot of “${p.pot}”` : `Pool “${p.key}”`}: ${n(p.inPerDay)} ${sym(p.token)} in and ${n(p.outPerDay)} out a day from a seed of ${n(p.seed)}; it ${poolFate(p, sim.scenario.days)}.`);
|
|
1001
|
+
for (const p of sim.pools) lines.push(`${p.pot ? `The pot of “${p.pot}”` : p.farm ? `The rewards vault of “${p.farm}”` : `Pool “${p.key}”`}: ${n(p.inPerDay)} ${sym(p.token)} in and ${n(p.outPerDay)} out a day from a seed of ${n(p.seed)}; it ${poolFate(p, sim.scenario.days)}.`);
|
|
935
1002
|
const d = sim.perDay;
|
|
936
1003
|
const list = (map) => Object.entries(map).map(([k, v]) => `${n(v)} ${sym(k)}`).join(', ') || 'nothing';
|
|
937
1004
|
lines.push(`Each day players pay ${list(d.players.pay)} and get back ${list(d.players.getBack)} on average (items at the value you gave them), plus ${n(d.players.playerFeesSol)} SOL in player fees, ${n(d.players.oracleFeesSol)} SOL to MagicBlock’s oracle and ${n(d.players.networkFeesSol)} SOL in network fees.`);
|
package/js/mcp/tools.mjs
CHANGED
|
@@ -169,7 +169,7 @@ export function carryIds(previous, next) {
|
|
|
169
169
|
carry('pools', ['index']);
|
|
170
170
|
for (const list of ['stashes', 'boxes', 'recipes', 'schedules', 'lotteries', 'grants']) carry(list, ['id']);
|
|
171
171
|
// A farm that pays nothing keeps the number of the empty pool made for it; an auction keeps the seller its address names.
|
|
172
|
-
carry('farms', ['id'
|
|
172
|
+
carry('farms', ['id']);
|
|
173
173
|
carry('auctions', ['id', 'seller']);
|
|
174
174
|
return out;
|
|
175
175
|
}
|
|
@@ -449,8 +449,10 @@ export const tools = [
|
|
|
449
449
|
' app.collectibles(), app.balances() what a player does. `listing`: the entry shop(), boxes(), lotteries() or recipes() showed the',
|
|
450
450
|
' player; its price (or version) is the most they pay: one that moved since is refused, never paid unseen',
|
|
451
451
|
' a prize (a grant, a lottery place, a contest win, a mission, a recipe) is given in the instruction that wins it,',
|
|
452
|
-
' its accounts from loot.prizeMetas({ project, collection, prize, winner, poolMints, items, nft }) (a roll
|
|
453
|
-
'
|
|
452
|
+
' its accounts from loot.prizeMetas({ project, collection, prize, winner, poolMints, items, nft }) (a roll: its box and',
|
|
453
|
+
' its order; ask for it in the same transaction, loot.askRollIx, which fails the whole transaction, SoldOut, when the box',
|
|
454
|
+
' has nothing in stock (only the order’s player asks then, a slot later, and that ends the roll with nothing); a write naming a',
|
|
455
|
+
' box to roll passes loot.boxesAccounts after it, and the box is written first); app.claims() finds',
|
|
454
456
|
' what a player is owed. A roll (a prize of a roll, an auction lot, a box) is an order: app.claim / app.collect, or',
|
|
455
457
|
' loot.askRollIx({ payer, orderAddress, order, box }) then loot.settleIx(...) hands over what it rolled (a stash NFT too)',
|
|
456
458
|
' loot.stashIx({ signer, project, id, asset, out }) the owner or authority puts an NFT in a stash, or takes one out',
|
|
@@ -458,20 +460,24 @@ export const tools = [
|
|
|
458
460
|
' app.buyListing(asset, { listing }), app.rentListing(asset, { periods, listing }) (`listing`: what listing() or listings()',
|
|
459
461
|
' showed the player), app.unlist(asset), app.rentedTo(asset) (one account read) listings, sale and rent',
|
|
460
462
|
' app.raffle(id, nft, { price, drawsAt, minSold }), app.unraffle(id, nft) a raffle from the lottery’s own stash',
|
|
461
|
-
' app.compound(key), app.
|
|
462
|
-
' app.
|
|
463
|
+
' app.compound(key), app.runSchedule(key) compound, send a schedule that is due (a run missed is missed: only a sealed fixed amount pays every run due)',
|
|
464
|
+
' app.farmWalk(key), app.walkFarm(key) a farm idle past many end times: how many it must walk, and walking them in parts (anyone, free; moves ask for the compute to walk a few themselves)',
|
|
465
|
+
' app.claims() also lists a lottery’s parts to pull (a refund, a place, a raffle’s NFT), vested runs, a rental over',
|
|
463
466
|
'Lower level, for what the player module does not cover:',
|
|
464
467
|
' import * as loot from "@xgamesfun/solana/client";',
|
|
465
468
|
' loot.pda.project(owner, number), loot.pda.box(project, id), … every address the program derives',
|
|
466
469
|
' loot.openBoxIx({ player, project, box, nonce }), loot.buyIx({ player, project, box, items }) … one builder per instruction; each returns { programId, keys, data }',
|
|
467
470
|
' loot.payNftIx({ player, project, asset, to }) pay with an NFT (burned, or given to `to`): the app admits by the receipt',
|
|
468
471
|
' loot.openListingIx / takeListingIx / closeListingIx, loot.listingTerms(listing), loot.pda.listing(project, asset), loot.renterOf(listing, now)',
|
|
469
|
-
'
|
|
470
|
-
' loot.
|
|
472
|
+
' a take or a bid signs the record’s terms: loot.listingTerms / offerTerms / auctionTerms(record), the hash of its bytes as read, but what a take changes',
|
|
473
|
+
' loot.decodeRound(data), loot.partsOwed(round, entry), loot.commonOwed(round), loot.pullIx(...) a round’s parts, each pulled in any order (a place’s roll: loot.pullRolls, asked in the same transaction)',
|
|
474
|
+
' loot.decodeEnds(data), loot.endsToWalk(farm, ends, now), loot.farmNeedsWalk(farm, ends, now), loot.walkFarmParts({ project, id, farm, ends, now }), loot.pda.ends(farm) a farm’s record of when seats stop earning',
|
|
475
|
+
' loot.orderRefundable(order, slot, now), loot.rollAskable(order, slot, now), loot.lotteryDrawStuck(round, now), loot.lotteryCancellable(round, now) deadlines, by the chain’s clock',
|
|
476
|
+
' loot.runsDue(schedule, now, vaultBalance), loot.scheduleCatchesUp(schedule), loot.pda.vest(schedule), loot.pda.prizeVault(contest), loot.pda.lotteryStash(lottery), loot.pda.rewards(farm)',
|
|
471
477
|
' loot.decode("LootBox", data), loot.receipts(tx) accounts, and the receipts of a getTransaction result (none when meta.err is set)',
|
|
472
478
|
'IDL: @xgamesfun/solana/idl.json (Anchor 1.x format).',
|
|
473
479
|
'A page builds the instruction, puts it in a transaction, and asks the player’s wallet (Wallet Standard: Phantom, Solflare, Backpack) to sign and send it. The app never holds a key.',
|
|
474
|
-
'Reads: the project’s records are plain accounts (loot.decode); every move leaves a receipt in the transaction’s logs (loot.receipts(tx)). A backend delivers only from a finalized transaction whose meta.err is null, once per order or reference: a failed transaction keeps its logs, and they can read as all-success.',
|
|
480
|
+
'Reads: the project’s records are plain accounts (loot.decode); every move leaves a receipt in the transaction’s logs (loot.receipts(tx)). A backend delivers only from a finalized transaction whose meta.err is null, once per order or reference, and only for a receipt whose data.project is its own (loot.receipts reads every project’s, and anyone can make a project): a failed transaction keeps its logs, and they can read as all-success.',
|
|
475
481
|
].join('\n');
|
|
476
482
|
return { text: text + footer, ...PROGRAM_INFO };
|
|
477
483
|
},
|