@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.
Files changed (44) hide show
  1. package/README.md +8 -8
  2. package/js/approve/details.mjs +52 -2
  3. package/js/approve/live-check.mjs +0 -15
  4. package/js/approve/server.mjs +5 -4
  5. package/js/chain/token2022.js +3 -2
  6. package/js/client/accounts.js +67 -25
  7. package/js/client/grant.js +7 -2
  8. package/js/client/idl/loot.js +1 -1
  9. package/js/client/idl/loot.json +716 -183
  10. package/js/client/ix.js +617 -113
  11. package/js/client/sizes.js +210 -64
  12. package/js/economy/check.mjs +691 -261
  13. package/js/economy/cost.mjs +2 -4
  14. package/js/economy/explain.mjs +16 -2
  15. package/js/economy/guide.mjs +12 -12
  16. package/js/economy/model.mjs +133 -14
  17. package/js/economy/plan.mjs +68 -19
  18. package/js/economy/review.mjs +50 -8
  19. package/js/economy/schema.json +73 -29
  20. package/js/economy/simulate.mjs +116 -13
  21. package/js/mcp/tools.mjs +2 -2
  22. package/js/play/index.js +855 -296
  23. package/package.json +1 -1
  24. package/release.json +5 -5
  25. package/skills/xsolana-economy/SKILL.md +12 -4
  26. package/skills/xsolana-economy/references/after-launch.md +1 -1
  27. package/skills/xsolana-economy/references/apps.md +11 -2
  28. package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
  29. package/skills/xsolana-economy/references/telling-the-person.md +22 -4
  30. package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
  31. package/types/chain/token2022.d.ts +3 -2
  32. package/types/client/accounts.d.ts +34 -11
  33. package/types/client/idl/loot.d.ts +25 -4
  34. package/types/client/ix.d.ts +217 -45
  35. package/types/client/sizes.d.ts +42 -8
  36. package/types/economy/check.d.mts +11 -2
  37. package/types/economy/cost.d.mts +1 -0
  38. package/types/economy/live.d.mts +2 -0
  39. package/types/economy/model.d.mts +75 -12
  40. package/types/economy/plan.d.mts +31 -2
  41. package/types/economy/simulate.d.mts +12 -1
  42. package/types/mcp/server.d.mts +12 -1
  43. package/types/mcp/tools.d.mts +12 -1
  44. package/types/play/index.d.ts +210 -144
@@ -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).nfts ?? []).length];
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.whenOut !== 'drawLeft';
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 === 'drawLeft' ? 'drawLeft' : 'stopSales' } : {}),
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: {