@xgamesfun/solana 0.25.4 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/js/approve/details.mjs +52 -2
- package/js/approve/live-check.mjs +0 -15
- package/js/approve/server.mjs +5 -4
- package/js/chain/token2022.js +3 -2
- package/js/client/accounts.js +67 -25
- package/js/client/grant.js +7 -2
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +716 -183
- package/js/client/ix.js +617 -113
- package/js/client/sizes.js +210 -64
- package/js/economy/check.mjs +691 -261
- package/js/economy/cost.mjs +2 -4
- package/js/economy/explain.mjs +16 -2
- package/js/economy/guide.mjs +12 -12
- package/js/economy/model.mjs +133 -14
- package/js/economy/plan.mjs +68 -19
- package/js/economy/review.mjs +50 -8
- package/js/economy/schema.json +73 -29
- package/js/economy/simulate.mjs +116 -13
- package/js/mcp/tools.mjs +2 -2
- package/js/play/index.js +855 -296
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +12 -4
- package/skills/xsolana-economy/references/after-launch.md +1 -1
- package/skills/xsolana-economy/references/apps.md +11 -2
- package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +22 -4
- package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
- package/types/chain/token2022.d.ts +3 -2
- package/types/client/accounts.d.ts +34 -11
- package/types/client/idl/loot.d.ts +25 -4
- package/types/client/ix.d.ts +217 -45
- package/types/client/sizes.d.ts +42 -8
- package/types/economy/check.d.mts +11 -2
- package/types/economy/cost.d.mts +1 -0
- package/types/economy/live.d.mts +2 -0
- package/types/economy/model.d.mts +75 -12
- package/types/economy/plan.d.mts +31 -2
- package/types/economy/simulate.d.mts +12 -1
- package/types/mcp/server.d.mts +12 -1
- package/types/mcp/tools.d.mts +12 -1
- package/types/play/index.d.ts +210 -144
package/js/economy/simulate.mjs
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
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
11
|
// the rest from then (`stockWalk`). It is a guide, not a promise: the full simulation (js/sim) runs the real program.
|
|
12
12
|
import { entryWidth, LIMIT_RECORD_BYTES, ROUND_RECORD_BYTES } from '../client/index.js';
|
|
13
|
-
import { boxOrderBytes, roundRentOf } from './plan.mjs';
|
|
13
|
+
import { boxOrderBytes, entryShape, roundRentOf } from './plan.mjs';
|
|
14
14
|
import {
|
|
15
|
+
boostedWeights,
|
|
15
16
|
boxBuys,
|
|
16
17
|
COLLECTIBLE_RENT as COLLECTIBLE_RENT_LAMPORTS,
|
|
17
18
|
drawsOf,
|
|
@@ -41,6 +42,11 @@ import {
|
|
|
41
42
|
toBps,
|
|
42
43
|
toSeconds,
|
|
43
44
|
toTime,
|
|
45
|
+
purchaseWeight,
|
|
46
|
+
shortPays,
|
|
47
|
+
stashNfts,
|
|
48
|
+
stopsSales,
|
|
49
|
+
toUnits,
|
|
44
50
|
weights,
|
|
45
51
|
whenOutWords,
|
|
46
52
|
} from './model.mjs';
|
|
@@ -59,15 +65,7 @@ const NETWORK_FEE = sol(SIGNATURE_FEE);
|
|
|
59
65
|
const ORACLE = sol(ORACLE_FEE);
|
|
60
66
|
/** A lottery purchase's entry in its round: the bytes the buyer adds (not refunded: they go to the treasury when the
|
|
61
67
|
* round closes), with a wallet kept for each share the buyer's own entry names (lottery.rs `entry_width`). */
|
|
62
|
-
const ticketLine = l =>
|
|
63
|
-
sol(
|
|
64
|
-
BigInt(
|
|
65
|
-
entryWidth(
|
|
66
|
-
lotterySplit(l).some(x => String(x.to).startsWith('chosen')),
|
|
67
|
-
Boolean(l.ticketNft),
|
|
68
|
-
),
|
|
69
|
-
) * RENT_PER_BYTE,
|
|
70
|
-
);
|
|
68
|
+
const ticketLine = l => sol(BigInt(entryWidth(...entryShape(l))) * RENT_PER_BYTE);
|
|
71
69
|
/** A collectible's Core account, paid from the SOL the order holds: its rent and Core's creation fee (model.mjs). */
|
|
72
70
|
const COLLECTIBLE_RENT = sol(COLLECTIBLE_RENT_LAMPORTS);
|
|
73
71
|
/** What a collectible a recipe burns gives back: its rent less one byte's (Core leaves a 1-byte account that keeps
|
|
@@ -369,9 +367,13 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
369
367
|
const stockOf = x => {
|
|
370
368
|
const supply = Number(m.item.get(x.item)?.supply ?? 0);
|
|
371
369
|
if (x.gives === 'item' && supply > 0 && !b.stockOdds) return [`item:${x.item}`, supply];
|
|
370
|
+
// A stash with nothing that weighs is out of stock: an NFT of weight 0 is never drawn (stash.rs `drawable`).
|
|
372
371
|
if (x.gives === 'stash' && m.stash.get(x.stash))
|
|
373
|
-
return [`stash:${x.stash}`, (m.stash.get(x.stash).
|
|
372
|
+
return [`stash:${x.stash}`, stashNfts(m.stash.get(x.stash)).filter(n => n.weight > 0).length];
|
|
374
373
|
if (x.gives === 'nft') return [`nft:${x.nft}`, 1];
|
|
374
|
+
// A minted prize is out of stock when the mint can't take it: a token the project can't mint (a fixed supply, or
|
|
375
|
+
// another's mint) mints nothing.
|
|
376
|
+
if (x.gives === 'mint' && !m.tokens.get(x.currency ?? game)?.gameMints) return [`mint:${x.currency ?? game}`, 0];
|
|
375
377
|
return null;
|
|
376
378
|
};
|
|
377
379
|
const needs = (b.outcomes ?? []).map(o => {
|
|
@@ -383,7 +385,7 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
383
385
|
return need;
|
|
384
386
|
});
|
|
385
387
|
if (!left.size || !(opens > 0)) return null;
|
|
386
|
-
const stops = b
|
|
388
|
+
const stops = stopsSales(b);
|
|
387
389
|
const out = i => ({
|
|
388
390
|
outcome: i,
|
|
389
391
|
gives: (b.outcomes[i].gives === 'bundle'
|
|
@@ -416,6 +418,32 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
416
418
|
: null;
|
|
417
419
|
}
|
|
418
420
|
const there = i => [...needs[i]].every(([k, n]) => left.get(k) >= n);
|
|
421
|
+
if (b.whenOut === 'giveNothing') {
|
|
422
|
+
// A draw stays on the weights as written: one on an outcome that is out is made and gives nothing, never refunded
|
|
423
|
+
// (order.rs `WhenOut::GiveNothing`). The box keeps selling; what misses grows as outcomes run out.
|
|
424
|
+
const all = w.reduce((n, x) => n + x, 0);
|
|
425
|
+
if (!all) return null;
|
|
426
|
+
let alive = w.map((_, i) => i).filter(there);
|
|
427
|
+
let day = 0;
|
|
428
|
+
const runsOut = [];
|
|
429
|
+
const miss = () => round((w.reduce((n, x, i) => n + (alive.includes(i) ? 0 : x), 0) / all) * 100, 6);
|
|
430
|
+
const gone = w.map((_, i) => i).filter(i => !alive.includes(i));
|
|
431
|
+
if (gone.length) runsOut.push({ inDays: 0, outcomes: gone.map(out), missPercent: miss() });
|
|
432
|
+
while (alive.length) {
|
|
433
|
+
const rate = new Map();
|
|
434
|
+
for (const i of alive)
|
|
435
|
+
for (const [k, n] of needs[i]) rate.set(k, (rate.get(k) ?? 0) + (opens * draws * w[i] * n) / all);
|
|
436
|
+
const next = [...rate].map(([k, r]) => ({ k, t: left.get(k) / r })).sort((a, z) => a.t - z.t)[0];
|
|
437
|
+
if (!next || day + next.t > days) break;
|
|
438
|
+
day += next.t;
|
|
439
|
+
for (const [k, r] of rate) left.set(k, Math.max(0, left.get(k) - r * next.t));
|
|
440
|
+
left.set(next.k, 0);
|
|
441
|
+
const ended = alive.filter(i => !there(i));
|
|
442
|
+
alive = alive.filter(there);
|
|
443
|
+
runsOut.push({ inDays: round(day, 4), outcomes: ended.map(out), missPercent: miss() });
|
|
444
|
+
}
|
|
445
|
+
return runsOut.length ? { whenOut: 'giveNothing', runsOut } : null;
|
|
446
|
+
}
|
|
419
447
|
const chances = alive => {
|
|
420
448
|
const sum = alive.reduce((n, i) => n + w[i], 0);
|
|
421
449
|
return w.map((x, i) => (sum && alive.includes(i) ? round((x / sum) * 100, 6) : 0));
|
|
@@ -449,6 +477,15 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
449
477
|
/** The walk of a box's stock in words (`stockWalk`). */
|
|
450
478
|
const stockNote = (b, walk) => {
|
|
451
479
|
const named = list => list.map(x => `outcome ${x.outcome} (${x.gives})`).join(', ');
|
|
480
|
+
if (walk.whenOut === 'giveNothing')
|
|
481
|
+
return `“${b.name}” keeps selling as its outcomes run out ("whenOut": "giveNothing"; its own opens alone): ${walk.runsOut
|
|
482
|
+
.map(
|
|
483
|
+
r =>
|
|
484
|
+
`${named(r.outcomes)} ${r.inDays ? `in about ${r.inDays} days` : 'from the start'}, then ${r.missPercent}% of draws give nothing`,
|
|
485
|
+
)
|
|
486
|
+
.join(
|
|
487
|
+
'; ',
|
|
488
|
+
)}. A draw on an outcome that is out is made and gives nothing: a paid one is not refunded, a free roll is over.`;
|
|
452
489
|
if (walk.whenOut === 'stopSales')
|
|
453
490
|
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 an item, a stash or a named NFT couldn’t cover, at the most any outcome takes of it in one draw, the draws of a purchase and of 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.`;
|
|
454
491
|
const steps = walk.runsOut.map(
|
|
@@ -567,6 +604,16 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
567
604
|
// What it gives that runs out, as the box says (`whenOut`): it stops selling, or draws among what is left.
|
|
568
605
|
const walk = !bought && b.active !== false ? stockWalk(b, opens) : null;
|
|
569
606
|
if (walk) notes.push(stockNote(b, walk));
|
|
607
|
+
// Each outcome's chance for a player who passes every odds rule at its most (order.rs, rules.rs `boosted`): counted
|
|
608
|
+
// in BigInt, each weight stopping at the largest. The rules count on every route, read when a roll is first asked.
|
|
609
|
+
const raised = (b.odds ?? []).length ? boostedWeights(b) : null;
|
|
610
|
+
const raisedSum = raised ? raised.reduce((n, x) => n + x, 0n) : 0n;
|
|
611
|
+
const oddsBoosted =
|
|
612
|
+
raised && raisedSum > 0n ? raised.map(x => round(Number((x * 100_000_000n) / raisedSum) / 1_000_000, 6)) : null;
|
|
613
|
+
if (oddsBoosted)
|
|
614
|
+
notes.push(
|
|
615
|
+
`“${b.name}”’s odds rules raise its chances to ${oddsBoosted.map((c, i) => `outcome ${i} ${c}%`).join(', ')} for a player who passes them all at their most. They count on every route: on a purchase, and on a roll another route gives, read when the roll is first asked, not when it was won.`,
|
|
616
|
+
);
|
|
570
617
|
return {
|
|
571
618
|
key: b.key,
|
|
572
619
|
name: b.name,
|
|
@@ -596,7 +643,8 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
596
643
|
)
|
|
597
644
|
? { limitedStock: true }
|
|
598
645
|
: {}),
|
|
599
|
-
...(!bought ? { whenOut: b.whenOut
|
|
646
|
+
...(!bought ? { whenOut: b.whenOut ?? 'stopSales' } : {}),
|
|
647
|
+
...(oddsBoosted ? { oddsBoosted } : {}),
|
|
600
648
|
...(walk ? { stock: walk } : {}),
|
|
601
649
|
...(isSol && known && price ? { returnOfRealCostPercent: round((value / (price + extrasSol)) * 100, 4) } : {}),
|
|
602
650
|
};
|
|
@@ -877,6 +925,58 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
877
925
|
return null;
|
|
878
926
|
};
|
|
879
927
|
const prized = placed.filter(p => prizeLine(p.prize)).map(p => worthOf(prizeLine(p.prize)));
|
|
928
|
+
// A weighted round (`timeWeighted`): each purchase weighs its tickets times the seconds from it to the close of
|
|
929
|
+
// sales, and a place is won by weight. With purchases spread evenly over the selling time, a ticket bought at the
|
|
930
|
+
// open counts twice the average, one bought halfway the average, one in the last hour next to nothing.
|
|
931
|
+
const closesBefore = toSeconds(l.closesBefore ?? 0);
|
|
932
|
+
const selling = Math.max(1, every - closesBefore);
|
|
933
|
+
const weighted = l.timeWeighted
|
|
934
|
+
? {
|
|
935
|
+
sellsForDays: round(selling / DAY, 4),
|
|
936
|
+
atOpen: 2,
|
|
937
|
+
halfway: 1,
|
|
938
|
+
lastHour: round(Number(purchaseWeight(1, selling - Math.min(3_600, selling), selling)) / (selling / 2), 6),
|
|
939
|
+
}
|
|
940
|
+
: null;
|
|
941
|
+
if (weighted)
|
|
942
|
+
notes.push(
|
|
943
|
+
`“${l.name}” weighs each purchase by its tickets times the seconds left until sales close: a ticket bought when a round opens counts about twice the average, one bought halfway the average, one in the last hour about ${weighted.lastHour} of it. A buyer sees what their purchase weighs before they buy.`,
|
|
944
|
+
);
|
|
945
|
+
// Places of tokens from a pool read it once, when the draw is asked: one that starts short pays as "whenShort" says.
|
|
946
|
+
const shortPools = [];
|
|
947
|
+
const fromPools = new Map();
|
|
948
|
+
for (const p of placed) {
|
|
949
|
+
const line = prizeLine(p.prize);
|
|
950
|
+
const key = line?.gives === 'tokens' ? prizePool(line) : null;
|
|
951
|
+
if (key) fromPools.set(key, [...(fromPools.get(key) ?? []), p]);
|
|
952
|
+
}
|
|
953
|
+
for (const [key, runs] of fromPools) {
|
|
954
|
+
const decimals = m.tokens.get(m.pool.get(key)?.currency)?.decimals ?? 0;
|
|
955
|
+
const units = v => {
|
|
956
|
+
try {
|
|
957
|
+
return toUnits(v ?? 0, decimals);
|
|
958
|
+
} catch {
|
|
959
|
+
return 0n;
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
const holds = units(m.pool.get(key)?.seed);
|
|
963
|
+
const rows = runs.map(p => ({ each: units(prizeLine(p.prize).amount), places: p.times ?? 1 }));
|
|
964
|
+
const wins = rows.reduce((n, r) => n + r.each * BigInt(r.places), 0n);
|
|
965
|
+
if (holds >= wins) continue;
|
|
966
|
+
const whenShort = runs[0].whenShort ?? 'inOrder';
|
|
967
|
+
const pays = shortPays(rows, holds, whenShort);
|
|
968
|
+
const scale = 10 ** decimals;
|
|
969
|
+
shortPools.push({
|
|
970
|
+
pool: key,
|
|
971
|
+
holds: Number(holds) / scale,
|
|
972
|
+
wins: Number(wins) / scale,
|
|
973
|
+
whenShort,
|
|
974
|
+
each: pays.map(x => Number(x.each) / scale),
|
|
975
|
+
});
|
|
976
|
+
notes.push(
|
|
977
|
+
`“${l.name}”’s places paid from “${key}” win ${Number(wins) / scale} and the pool starts with ${Number(holds) / scale}: if it holds no more when a draw is asked, they get ${pays.map(x => Number(x.each) / scale).join(', ')} each ("whenShort": "${whenShort}").`,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
880
980
|
// One ticket bought on its own: the fee and the record are per purchase, so a bundle of tickets shares them. The
|
|
881
981
|
// record's rent comes back once the round is done with it, and a no-loss ticket's price after the draw: both held.
|
|
882
982
|
const realCost = realCostOf({
|
|
@@ -918,6 +1018,9 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
918
1018
|
roundRentSol: round(Number(roundRentOf(l, m)) / 1e9),
|
|
919
1019
|
...(l.ticketNft ? { ticketNft: true } : {}),
|
|
920
1020
|
...(l.noLoss ? { noLoss: true } : {}),
|
|
1021
|
+
...(closesBefore ? { closesBeforeDays: round(closesBefore / DAY, 4) } : {}),
|
|
1022
|
+
...(weighted ? { weighted } : {}),
|
|
1023
|
+
...(shortPools.length ? { shortPools } : {}),
|
|
921
1024
|
realCost,
|
|
922
1025
|
};
|
|
923
1026
|
});
|
package/js/mcp/tools.mjs
CHANGED
|
@@ -354,7 +354,7 @@ export const tools = [
|
|
|
354
354
|
name: 'xsolana_check',
|
|
355
355
|
annotations: READ,
|
|
356
356
|
description:
|
|
357
|
-
'Check an economy spec: refusals (each with a code, what it means and its fix) stop it from going live; warnings are what the person must hear (chance that pays money, revenue paid to stakers, an unfunded prize, a grant without limits, a mint’s powers). Run it after every change. Give the spec inline (`spec`), a file, or an example.',
|
|
357
|
+
'Check an economy spec: refusals (each with a code, what it means and its fix) stop it from going live; warnings are what the person must hear (chance that pays money, revenue paid to stakers, an unfunded prize, a grant without limits, a mint’s powers, a time far off with its date in words, a token a player can hold a payment up in, a box longer than one answer of the oracle’s holds). The program refuses only what it must: much of what a project sets up is taken and warned of, never refused, so tell the person each warning in plain words. Run it after every change. Give the spec inline (`spec`), a file, or an example.',
|
|
358
358
|
inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
|
|
359
359
|
async handler(input, ctx) {
|
|
360
360
|
const { economy, from } = await load(input, ctx);
|
|
@@ -376,7 +376,7 @@ export const tools = [
|
|
|
376
376
|
name: 'xsolana_simulate',
|
|
377
377
|
annotations: READ,
|
|
378
378
|
description:
|
|
379
|
-
'The quick simulation (expected values from the spec, instant): what each box, ticket and craft gives back against its price, the house edge, each pool’s money in and out per day and whether it runs dry, settles or grows, farm rewards per unit staked, what players, the treasury and the platform get each day. `scenario` overrides the spec’s own ({"perDay": {"box:flip": 1000}, "days": 30, "staked": {"<farm>": 1000}}). Quote its numbers; never your own arithmetic.',
|
|
379
|
+
'The quick simulation (expected values from the spec, instant): what each box, ticket and craft gives back against its price, the house edge, each pool’s money in and out per day and whether it runs dry, settles or grows, farm rewards per unit staked, what players, the treasury and the platform get each day; a box’s chances with its odds rules at their most, what a box that gives nothing for a miss misses as its outcomes run out, what a ticket weighs in a lottery weighed by time, and what places get from a pool that starts short. `scenario` overrides the spec’s own ({"perDay": {"box:flip": 1000}, "days": 30, "staked": {"<farm>": 1000}}). Quote its numbers; never your own arithmetic.',
|
|
380
380
|
inputSchema: {
|
|
381
381
|
type: 'object',
|
|
382
382
|
properties: {
|