@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
@@ -21,7 +21,12 @@ import {
21
21
  } from '../chain/pda.js';
22
22
  import { createTableIx, extendTableIx } from '../chain/lookup.js';
23
23
  import { MEMO_PROGRAM } from '../chain/token2022.js';
24
- import { createAssociatedTokenAccountIdempotent, syncNative, transferLamports } from '../chain/programs.js';
24
+ import {
25
+ computeUnitLimit,
26
+ createAssociatedTokenAccountIdempotent,
27
+ syncNative,
28
+ transferLamports,
29
+ } from '../chain/programs.js';
25
30
  import {
26
31
  createAccountIx,
27
32
  initializeMint2Ix,
@@ -34,7 +39,7 @@ import { compileMessage, transactionSize } from '../chain/tx.js';
34
39
  import { sha256 } from '../chain/sha256.js';
35
40
  import { utf8 } from '../chain/bytes.js';
36
41
  import * as loot from '../client/index.js';
37
- import { check } from './check.mjs';
42
+ import { check, MAX_COMPUTE_UNITS } from './check.mjs';
38
43
  import {
39
44
  ACCOUNT_OVERHEAD,
40
45
  boxBuys,
@@ -187,7 +192,7 @@ export function boxArgs(b, m) {
187
192
  rise: rise(b.rise, t),
188
193
  fall: fall(b.fall, t, m.now),
189
194
  stock_odds: !!b.stockOdds,
190
- when_out: b.whenOut === 'drawLeft' ? 'DrawLeft' : 'StopSales',
195
+ when_out: WHEN_OUT[b.whenOut ?? 'stopSales'],
191
196
  active: b.active !== false,
192
197
  shares: shares(b.split, m),
193
198
  // What one open draws: each slot so many draws among the outcomes it names (all, when none), no outcome twice
@@ -371,9 +376,13 @@ export function lotteryArgs(l, m) {
371
376
  shares: shares(lotterySplit(l), m),
372
377
  // A place takes a share of the pot, a prize for its winner, or both.
373
378
  // A run of `times` places alike: "places 2 to 201 each take 1%" is one.
374
- places: (l.places ?? [])
375
- .map(placeOf)
376
- .map(p => ({ bps: Math.round(p.percent * 100), prize: prizeOrNull(p.prize, m), times: p.times })),
379
+ places: (l.places ?? []).map(placeOf).map(p => ({
380
+ bps: Math.round(p.percent * 100),
381
+ prize: prizeOrNull(p.prize, m),
382
+ // What a place of tokens from a pool gets when the pool holds less than the places with a winner win.
383
+ when_short: WHEN_SHORT[p.whenShort ?? 'inOrder'],
384
+ times: p.times,
385
+ })),
377
386
  ticket_mint: l.ticketCurrency ? `@mint:${l.ticketCurrency}` : '11111111111111111111111111111111',
378
387
  draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
379
388
  // A ticket paid with one NFT that passes the rule, in place of tokens.
@@ -382,6 +391,10 @@ export function lotteryArgs(l, m) {
382
391
  returns: Boolean(l.noLoss),
383
392
  // A least sale: a round that sold fewer tickets is cancelled at its draw and refunded.
384
393
  min_sold: BigInt(l.minSold ?? 0),
394
+ // Sales close this long before the draw (0: at the draw).
395
+ closes_before: BigInt(toSeconds(l.closesBefore ?? 0)),
396
+ // A no-loss lottery's purchase counts for its tickets times the seconds from it to the close of sales.
397
+ time_weighted: Boolean(l.timeWeighted),
385
398
  },
386
399
  every: BigInt(every),
387
400
  gate: gateOf(l.gate, m),
@@ -389,6 +402,14 @@ export function lotteryArgs(l, m) {
389
402
  };
390
403
  }
391
404
 
405
+ /** A box's `whenOut` in the spec, and the program's `WhenOut`: stop selling while an outcome is out (a paid miss is
406
+ * refunded), draw among what is left, or give nothing for a draw on an outcome that is out. */
407
+ export const WHEN_OUT = { stopSales: 'StopSales', drawLeft: 'DrawLeft', giveNothing: 'GiveNothing' };
408
+ /** A place's `whenShort` in the spec, and the program's `WhenShort`: what places of tokens from one pool get when the
409
+ * pool holds less than they win. Prizes of 1,000, 500 and 500 from a pool holding 1,700: 1,000, 500 and 0 in order;
410
+ * 850, 425 and 425 in proportion; or nothing. */
411
+ export const WHEN_SHORT = { inOrder: 'InOrder', inProportion: 'InProportion', nothing: 'Nothing' };
412
+
392
413
  /**
393
414
  * A lottery round's header as its first purchase sizes it (`LotteryRound::new`: its `head`), and the token accounts a
394
415
  * round may open (`accounts`: the payment mint's, the pot's, one for each run whose prize comes from a pool). The
@@ -405,15 +426,17 @@ export function roundSize(l, m) {
405
426
  .reduce((n, p) => n + Number(p.times ?? 1), 0);
406
427
  const termsBytes = encodeBorsh(IDL, { defined: { name: 'LotteryTerms' } }, terms).length;
407
428
  // The tag; project, lottery, id, incarnation, round, bump, opener, lent, head, status, revision; the terms; fee, sold,
408
- // to_pot; common; owns, owed, the draw's slots and times, answer and first ticket, the pot's four numbers; paid; prizes (59
429
+ // to_pot; the weight sold (1 byte in a round that doesn't weigh, 17 in one that does); common; owns, owed, the draw's
430
+ // slots and times, answer and first place's point (16), the pot's four numbers; paid; prizes (59
409
431
  // bytes each); accounts (64 each); nfts; assigned.
410
432
  const head =
411
433
  1 +
412
434
  130 +
413
435
  termsBytes +
414
436
  17 +
437
+ (terms.time_weighted ? 17 : 1) +
415
438
  (4 + 8 * terms.shares.length) +
416
- 112 +
439
+ 120 +
417
440
  (4 + Math.ceil((2 * places) / 8)) +
418
441
  (4 + 59 * tokens) +
419
442
  (4 + 64 * (2 + pools)) +
@@ -424,6 +447,15 @@ export function roundSize(l, m) {
424
447
  return { head, accounts };
425
448
  }
426
449
 
450
+ /** What decides the bytes of a purchase's entry in a lottery's round (`loot.entryWidth`'s arguments): whether it keeps
451
+ * what arrived (a share the buyer chooses, or a weighted round of paid tickets), whether the ticket is an NFT, and
452
+ * whether the round weighs its tickets by time. */
453
+ export const entryShape = l => {
454
+ const weighted = Boolean(l.timeWeighted);
455
+ const chosen = lotterySplit(l).some(s => String(s.to).startsWith('chosen'));
456
+ return [chosen || (weighted && !l.ticketNft), Boolean(l.ticketNft), weighted];
457
+ };
458
+
427
459
  /** How many rounds of a lottery are open or still paying out at once, and how many purchases a round takes, as the
428
460
  * spec expects them (`expect`): by default two rounds of a lottery that draws again (one selling, one paying out) and
429
461
  * one of a lottery that draws once; a round's purchases from the scenario's purchases a day over a round, else 100. */
@@ -449,10 +481,7 @@ export function roundRentOf(l, m) {
449
481
  if (l.roundRent !== undefined) return toUnits(l.roundRent, 9);
450
482
  const { head, accounts } = roundSize(l, m);
451
483
  const { rounds, entries } = roundsExpected(l, m);
452
- const width = loot.entryWidth(
453
- lotterySplit(l).some(s => String(s.to).startsWith('chosen')),
454
- Boolean(l.ticketNft),
455
- );
484
+ const width = loot.entryWidth(...entryShape(l));
456
485
  // The header and its entries' list head (6 bytes), each token account, each entry's bytes.
457
486
  const one = rent(head + 6) + BigInt(accounts) * rent(165) + BigInt(entries * width) * RENT_PER_BYTE;
458
487
  const all = BigInt(rounds) * one;
@@ -496,7 +525,7 @@ export function contestArgs(c, m, at = m.now) {
496
525
  // Each entry stakes one NFT that passes this rule, and the NFTs are the pot: each winner shares the losers', at most
497
526
  // `losersEach` of them (none: no cap).
498
527
  nft: c.stakeNft ? ruleOf(c.stakeNft, m) : null,
499
- cap: c.stakeNft && c.losersEach ? c.losersEach : null,
528
+ cap: c.stakeNft && c.losersEach !== undefined ? c.losersEach : null,
500
529
  };
501
530
  }
502
531
 
@@ -920,6 +949,10 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
920
949
  id: `${part}:${kind}`,
921
950
  part,
922
951
  says,
952
+ // A box's write finds each of its pools' and stashes' addresses (1,500 units a try) and compares its entries
953
+ // pair by pair: a box of 16 stashes takes some 170,000 units, more where its addresses take more tries, past
954
+ // the 200,000 an instruction has by default. Its transaction asks for all a transaction can have.
955
+ compute: Boolean(def.Box),
923
956
  creates: [{ what, bytes }, ...pot],
924
957
  build: ctx => [
925
958
  loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: resolve(def, ctx), ...options(ctx) }),
@@ -1598,6 +1631,7 @@ export const recordBytesOf = (kind, body) => {
1598
1631
  revision: 0,
1599
1632
  ...placeholders(loot.withDefaults({ Lottery: { args } }).Lottery.args),
1600
1633
  round: 0,
1634
+ last: 0,
1601
1635
  pots: 0,
1602
1636
  owing: NOBODY,
1603
1637
  }),
@@ -1760,7 +1794,18 @@ export function setupContext(m, { owner: ownerKey, keys, live: given = {}, rent
1760
1794
  export const stepBytes = (instructions, ctx) =>
1761
1795
  transactionSize(compileMessage({ payer: ctx.owner, blockhash: placeholderKey('blockhash'), instructions }));
1762
1796
 
1763
- /** Packs steps into transactions under the size budget, one signer set at a time; each step whole. */
1797
+ /** `instructions`, led by a compute limit of all a transaction can have when `compute` (a box's write is in them). */
1798
+ const budgeted = (instructions, compute) =>
1799
+ compute ? [computeUnitLimit(MAX_COMPUTE_UNITS), ...instructions] : instructions;
1800
+
1801
+ /** The bytes of a transaction of `step` alone, as `pack` sends it (its compute limit included). */
1802
+ export const stepAloneBytes = (step, ctx) => stepBytes(budgeted(step.build(ctx), Boolean(step.compute)), ctx);
1803
+
1804
+ /**
1805
+ * Packs steps into transactions under the size budget, one signer set at a time; each step whole. A transaction that
1806
+ * carries a step needing more compute than an instruction has by default (`compute`: a box's write) asks for all a
1807
+ * transaction can have, first.
1808
+ */
1764
1809
  export function pack(steps, ctx) {
1765
1810
  const transactions = [];
1766
1811
  let current = null;
@@ -1770,25 +1815,28 @@ export function pack(steps, ctx) {
1770
1815
  // A step with nothing to send (the lookup table of a plan built with no recent slot) is left out.
1771
1816
  if (!instructions.length) continue;
1772
1817
  const signerKind = step.signers.includes('authority') ? 'authority' : 'owner';
1773
- // A write that names boxes to roll takes all the compute a transaction has: alone.
1774
1818
  if (current && current.signer === signerKind && !step.alone && !current.alone) {
1775
- const next = [...current.instructions, ...instructions];
1819
+ const compute = current.compute || Boolean(step.compute);
1820
+ const next = budgeted([...current.built.flat(), ...instructions], compute);
1776
1821
  if (sizeOf(next) <= BUDGET) {
1777
1822
  current.instructions = next;
1823
+ current.compute = compute;
1778
1824
  current.steps.push(step);
1779
1825
  current.built.push(instructions);
1780
1826
  current.signers = [...new Set([...current.signers, ...step.signers])];
1781
1827
  continue;
1782
1828
  }
1783
1829
  }
1784
- const size = sizeOf(instructions);
1830
+ const alone = budgeted(instructions, Boolean(step.compute));
1831
+ const size = sizeOf(alone);
1785
1832
  if (size > BUDGET) throw new Error(`The step “${step.says}” is ${size} bytes: too big for one transaction.`);
1786
1833
  // `built`: each step's own instructions, in the order of `steps`.
1787
1834
  current = {
1788
1835
  signer: signerKind,
1789
1836
  steps: [step],
1790
1837
  built: [instructions],
1791
- instructions,
1838
+ instructions: alone,
1839
+ compute: Boolean(step.compute),
1792
1840
  signers: [...step.signers],
1793
1841
  alone: Boolean(step.alone),
1794
1842
  };
@@ -1803,7 +1851,8 @@ export function pack(steps, ctx) {
1803
1851
  * real instructions; without it they are built on stand-in addresses (for sizing, cost and showing).
1804
1852
  */
1805
1853
  export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null, only = null, live = null } = {}) {
1806
- const checked = check(economy, { now });
1854
+ // With a real context the project is known: a box's write is checked with its own address searches counted.
1855
+ const checked = check(economy, { now, ...(ctx?.project ? { project: ctx.project } : {}) });
1807
1856
  if (!checked.ok) return { ok: false, refusals: checked.refusals, warnings: checked.warnings };
1808
1857
  const { steps, model: m } = setupSteps(economy, { now, only, live });
1809
1858
  if (ctx)
@@ -384,7 +384,7 @@ export function review(economy, { now = Math.floor(Date.now() / 1000) } = {}) {
384
384
  const free = price === 0;
385
385
  const ceiling = free
386
386
  ? `${b.perWallet ? `${b.perWallet} per wallet, and wallets are free: ` : ''}${it?.supply ? `all ${it.supply}` : 'as many as they like'}, for only the fees.`
387
- : `What they pay for, at the price: ${rolls ? 'nobody, the project included, can steer the roll' : 'nothing free'}.${b.whenOut === 'drawLeft' && rolls ? ' With "whenOut": "drawLeft", someone watching the stock can empty a common outcome, then open for the rare at better odds.' : ''}`;
387
+ : `What they pay for, at the price: ${rolls ? 'nobody, the project included, can steer the roll' : 'nothing free'}.${b.whenOut === 'drawLeft' && rolls ? ' With "whenOut": "drawLeft", someone watching the stock can empty a common outcome, then open for the rare at better odds.' : ''}${b.whenOut === 'giveNothing' && rolls ? ' With "whenOut": "giveNothing", a draw on an outcome that is out gives nothing and is not refunded.' : ''}`;
388
388
  routes.push({
389
389
  kind,
390
390
  key,
@@ -526,15 +526,21 @@ export function review(economy, { now = Math.floor(Date.now() / 1000) } = {}) {
526
526
  ].filter(Boolean),
527
527
  trusts: ['MagicBlock’s oracle (the draw)'],
528
528
  trust: null,
529
- ceiling: `A place by chance: each ticket is one chance, and "perWallet" limits a wallet, not a person. Tickets are not weighted by time, so one buyer may take most of a round’s tickets just before the draw.`,
529
+ ceiling: l.timeWeighted
530
+ ? `A place by chance, by weight: each purchase counts its tickets times the seconds left until sales close, so a ticket held longer has a better chance; "perWallet" limits a wallet, not a person.`
531
+ : `A place by chance: each ticket is one chance, and "perWallet" limits a wallet, not a person. Tickets are not weighted by time, so one buyer may take most of a round’s tickets just before ${l.closesBefore ? 'sales close' : 'the draw'}.`,
530
532
  feeds: [...new Set((l.places ?? []).flatMap(p => feedsOf(p.prize)))],
531
533
  });
532
- find(
533
- 'R-LOTTERY-LATE',
534
- 'info',
535
- `lottery:${l.key}`,
536
- `Anyone may buy most of ${label(l)}’s tickets right before the draw, risking only the price and fees; the draw itself is the oracle’s, which nobody steers.`,
537
- );
534
+ if (!l.timeWeighted)
535
+ find(
536
+ 'R-LOTTERY-LATE',
537
+ l.noLoss && !l.closesBefore ? 'low' : 'info',
538
+ `lottery:${l.key}`,
539
+ `Anyone may buy most of ${label(l)}’s tickets right before ${l.closesBefore ? `sales close (${l.closesBefore} before the draw)` : 'the draw'}, risking only ${l.noLoss ? 'the fees: every ticket’s price comes back' : 'the price and fees'}; the draw itself is the oracle’s, which nobody steers.`,
540
+ l.noLoss
541
+ ? 'Weigh tickets by the time they are held ("timeWeighted": true), or close sales a while before the draw ("closesBefore").'
542
+ : undefined,
543
+ );
538
544
  }
539
545
  for (const c of m.contests) {
540
546
  const deciders = c.deciders ?? [];
@@ -670,6 +676,42 @@ export function review(economy, { now = Math.floor(Date.now() / 1000) } = {}) {
670
676
  const key = /^economy\.tokens\.([^.]+)/.exec(w.where)?.[1];
671
677
  find('R-MINT-POWERS', 'low', key ? `token:${key}` : 'economy', w.message, w.fix);
672
678
  }
679
+ // What the program takes and a player, a partner or a typo can turn against the project (loot 0.26): each warning,
680
+ // as a finding on its route.
681
+ const routeOf = where => {
682
+ const [, list, key] = /^economy\.([a-z]+)\.([^.[]+)/.exec(where) ?? [];
683
+ const kinds = {
684
+ boxes: 'box',
685
+ lotteries: 'lottery',
686
+ auctions: 'auction',
687
+ contests: 'contest',
688
+ farms: 'farm',
689
+ schedules: 'schedule',
690
+ recipes: 'recipe',
691
+ grants: 'grant',
692
+ holds: 'hold',
693
+ stashes: 'stash',
694
+ tokens: 'token',
695
+ };
696
+ return kinds[list] ? `${kinds[list]}:${key}` : 'economy';
697
+ };
698
+ for (const [code, id, severity] of [
699
+ ['W-FARM-AUTHORITY', 'R-FARM-AUTHORITY', 'high'],
700
+ ['W-SCHEDULE-PAYEE', 'R-SCHEDULE-PAYEE', 'medium'],
701
+ ['W-FAR-TIME', 'R-FAR-TIME', 'medium'],
702
+ ['W-HOLD-UP', 'R-HOLD-UP', 'medium'],
703
+ ['W-AUCTION-OUTSIDE', 'R-AUCTION-OUTSIDE', 'medium'],
704
+ ['W-OUTSIDE-NFT', 'R-OUTSIDE-NFT', 'medium'],
705
+ ['W-COLLECTION-PARTNER', 'R-COLLECTION-PARTNER', 'medium'],
706
+ ['W-FARM-MISSION', 'R-FARM-MISSION', 'medium'],
707
+ ['W-CONTEST-PRIZE', 'R-CONTEST-PRIZE', 'medium'],
708
+ ['W-SHARED-SOURCE', 'R-SHARED-SOURCE', 'low'],
709
+ ['W-LOTTERY-POOL', 'R-LOTTERY-POOL', 'low'],
710
+ ['W-LOTTERY-WEIGHT', 'R-LOTTERY-WEIGHT', 'info'],
711
+ ['W-ZERO', 'R-ZERO', 'low'],
712
+ ['W-ANSWER', 'R-ANSWER', 'low'],
713
+ ])
714
+ for (const w of warned(code)) find(id, severity, routeOf(w.where), w.message, w.fix);
673
715
 
674
716
  findings.sort((a, b) => SEVERITIES.indexOf(a.severity) - SEVERITIES.indexOf(b.severity));
675
717
  return {
@@ -278,7 +278,7 @@
278
278
  },
279
279
  "destination": {
280
280
  "type": "string",
281
- "description": "Where a share goes: \"burn\", \"treasury\" (the game's), \"pool:<key>\", \"wallet:<address>\" (a partner, a creator, a charity: the program pays their own account for the currency, and opens it when it is not there), \"account:<token account address>\" (an existing token account of that currency), \"holder:<collectible address>\" (whoever holds that collectible at the time), \"lottery:<key>\" (that lottery's pot), \"farm:<key>\" (that farm's rewards vault, shared by its stakers), or, in a box's or a lottery's split, \"chosen\" (a wallet the buyer names when they buy: a referrer, a streamer) or \"chosen:<item key>\" (whoever holds the collectible of that item the buyer names: the plot they build on; the buyer must name one a player holds, or the purchase is refused); in a payment's split, \"chosen\" only (a wallet the payer names each time they pay, never their own). A share that can't be paid (its wallet has no account, nobody holds the collectible) goes to the treasury instead; a sale never fails over it.",
281
+ "description": "Where a share goes: \"burn\", \"treasury\" (the game's), \"pool:<key>\", \"wallet:<address>\" (a partner, a creator, a charity: the program pays their own account for the currency, and opens it when it is not there), \"account:<token account address>\" (an existing token account of that currency), \"holder:<collectible address>\" (whoever holds that collectible at the time), \"lottery:<key>\" (that lottery's pot), \"farm:<key>\" (that farm's rewards vault, shared by its stakers), or, in a box's or a lottery's split, \"chosen\" (a wallet the buyer names when they buy: a referrer, a streamer) or \"chosen:<item key>\" (whoever holds the collectible of that item the buyer names: the plot they build on; the buyer must name one a player holds, or the purchase is refused); in a payment's split, \"chosen\" only (a wallet the payer names each time they pay, their own too). A chosen share can come back to the buyer: it is a rebate they control, not a referral the program can prove. A share that can't be paid (its wallet has no account, nobody holds the collectible) goes to the treasury instead; a sale never fails over it.",
282
282
  "pattern": "^(burn|treasury|chosen|chosen:[a-z][a-z0-9-]{0,31}|pool:[a-z][a-z0-9-]{0,31}|lottery:[a-z][a-z0-9-]{0,31}|farm:[a-z][a-z0-9-]{0,31}|wallet:[1-9A-HJ-NP-Za-km-z]{32,44}|account:[1-9A-HJ-NP-Za-km-z]{32,44}|holder:[1-9A-HJ-NP-Za-km-z]{32,44})$"
283
283
  },
284
284
  "share": {
@@ -427,6 +427,14 @@
427
427
  "transferFeePercent": {
428
428
  "$ref": "#/$defs/percent"
429
429
  },
430
+ "feeChangeable": {
431
+ "type": "boolean",
432
+ "description": "Its issuer can still change its transfer fee (Token-2022's transfer fee config has an authority): raised to 100%, it takes every stake or payment as it leaves."
433
+ },
434
+ "closeAuthority": {
435
+ "type": "boolean",
436
+ "description": "Its mint has a close authority, who could close it and make it again with other powers."
437
+ },
430
438
  "pausable": {
431
439
  "type": "boolean"
432
440
  },
@@ -435,14 +443,14 @@
435
443
  },
436
444
  "defaultState": {
437
445
  "type": "boolean",
438
- "description": "The mint carries Token-2022's DefaultAccountState, whatever it is set to. With a freeze authority it can be switched to frozen: auctions, contests, lotteries and sealed schedules refuse it."
446
+ "description": "The mint carries Token-2022's DefaultAccountState, whatever it is set to. With a freeze authority it can be switched to frozen: the checker warns what that does to an auction, a loan, a lottery or a contest in it."
439
447
  },
440
448
  "nonTransferable": {
441
449
  "type": "boolean"
442
450
  },
443
451
  "confidentialTransfers": {
444
452
  "type": "boolean",
445
- "description": "Its holders can turn plain transfers away (Token-2022 confidential transfers): lotteries, contests and auctions refuse it."
453
+ "description": "Its holders can turn plain transfers away (Token-2022 confidential transfers): the checker warns what that does to an auction, a loan, a lottery or a contest in it."
446
454
  }
447
455
  }
448
456
  }
@@ -875,9 +883,9 @@
875
883
  "description": "Each item's chance shrinks with what is left of its supply."
876
884
  },
877
885
  "whenOut": {
878
- "enum": ["stopSales", "drawLeft"],
886
+ "enum": ["stopSales", "drawLeft", "giveNothing"],
879
887
  "default": "stopSales",
880
- "description": "What a box that rolls does once an outcome can't be given (an item sold out, a pool short of its prize, a stash empty). \"stopSales\" (the default): it isn't sold while any outcome couldn't cover this purchase's draws and those of purchases still waiting for their answer, so every roll is on the odds written, and the project restocks or edits the box to sell again. \"drawLeft\": it keeps selling, and a roll draws among the outcomes still there by their written weights, so the others' chances rise as one runs out: a player can empty a common outcome to make a rare one likelier. A box selling a rare prize keeps \"stopSales\"."
888
+ "description": "What a box that rolls does once an outcome can't be given (an item sold out, a pool short of its prize, a stash empty). \"stopSales\" (the default): it isn't sold while any outcome couldn't cover this purchase's draws and those of purchases still waiting for their answer, so every roll is on the odds written, a paid miss is refunded, and the project restocks or edits the box to sell again; a free roll (another box's, a recipe's, a grant's, a mission's, a lottery place's) is still given after it stops, and is asked again until it hits. \"drawLeft\": it keeps selling, and a roll draws among the outcomes still there by their written weights, so the others' chances rise as one runs out: a player can empty a common outcome to make a rare one likelier. \"giveNothing\": it keeps selling, a roll stays on the weights written, and a draw that lands on an outcome that is out gives nothing: a paid draw is not refunded, a free roll is over. A box selling a rare prize keeps \"stopSales\"."
881
889
  },
882
890
  "active": {
883
891
  "type": "boolean",
@@ -1389,7 +1397,7 @@
1389
1397
  "type": "integer",
1390
1398
  "minimum": 0,
1391
1399
  "maximum": 15,
1392
- "description": "Pays its places from a pot it keeps from round to round, this one of its 16 (0 unless it moves to a fresh one): each place takes its percent of what the pot holds at the draw with the round's ticket money, so the seed, gifts and shares sent to it (\"lottery:<key>\"), yield, and what earlier rounds' places left all count, and the next round always starts with what the round before left. Write exactly one of \"pot\" or \"rest\". A no-loss lottery needs a pot. Changing the number between rounds starts a fresh pot; every pot it opened is emptied to the owner when it closes."
1400
+ "description": "Pays its places from a pot it keeps from round to round, this one of its 16 (0 unless it moves to a fresh one): each place takes its percent of what the pot holds at the draw with the round's ticket money, so the seed, gifts and shares sent to it (\"lottery:<key>\"), yield, and what earlier rounds' places left all count, and the next round always starts with what the round before left. Write exactly one of \"pot\" or \"rest\". Changing the number between rounds starts a fresh pot; every pot it opened is emptied to the owner when it closes."
1393
1401
  },
1394
1402
  "rest": {
1395
1403
  "$ref": "#/$defs/destination",
@@ -1422,7 +1430,12 @@
1422
1430
  "type": "integer",
1423
1431
  "minimum": 1,
1424
1432
  "maximum": 40960,
1425
- "description": "How many places in a row are alike (a run): \"places 2 to 201 each take 1%\" is one { \"percent\": 1, \"times\": 200 }. Not for a named NFT or a roll, which one place gives. Default 1."
1433
+ "description": "How many places in a row are alike (a run): \"places 2 to 201 each take 1%\" is one { \"percent\": 1, \"times\": 200 }. Not for a named NFT, which one place gives; a run of rolls gives each place its own roll. Default 1."
1434
+ },
1435
+ "whenShort": {
1436
+ "enum": ["inOrder", "inProportion", "nothing"],
1437
+ "default": "inOrder",
1438
+ "description": "For a prize of tokens from a pool: what each place gets when the pool holds less than the places with a winner win, read once when the draw is asked. \"inOrder\" (the default): whole prizes, first place first, while the pool covers one; \"inProportion\": every place the same share of its prize; \"nothing\": no place gets anything, and the pool keeps it. Prizes of 1,000, 500 and 500 from a pool holding 1,700 give 1,000, 500 and 0 in order, 850, 425 and 425 in proportion, or nothing. Every place paid from one pool says the same."
1426
1439
  }
1427
1440
  }
1428
1441
  }
@@ -1437,6 +1450,14 @@
1437
1450
  "firstDraw": {
1438
1451
  "$ref": "#/$defs/time"
1439
1452
  },
1453
+ "closesBefore": {
1454
+ "$ref": "#/$defs/duration",
1455
+ "description": "Sales close this long before each draw (default 0: at the draw); for a lottery that draws again, less than \"every\". A no-loss lottery wants one, so tickets bought just before the draw don't share the pot with tickets held all period."
1456
+ },
1457
+ "timeWeighted": {
1458
+ "type": "boolean",
1459
+ "description": "No-loss lotteries only: each purchase weighs its tickets times the seconds from it to the close of sales, and a place is won by weight, so a ticket held longer has a better chance. Set a sales close you mean (\"closesBefore\"); \"perWallet\" and \"minSold\" still count tickets. Off by default: every ticket counts one."
1460
+ },
1440
1461
  "gate": {
1441
1462
  "$ref": "#/$defs/gate"
1442
1463
  },
@@ -1485,7 +1506,7 @@
1485
1506
  },
1486
1507
  "noLoss": {
1487
1508
  "type": "boolean",
1488
- "description": "A no-loss lottery: every ticket goes back to its buyer once the round is drawn, and the winners take only the pot, what is paid into it (`seed`, a sponsor, a share of other sales: \"lottery:<key>\"). It needs \"pot\". No split, no cut and no `ticketCurrency`; NFT tickets come back too."
1509
+ "description": "A no-loss lottery: every ticket goes back to its buyer once the round is drawn, and the winners take only the pot, what is paid into it (`seed`, a sponsor, a share of other sales: \"lottery:<key>\"), or with \"rest\" only its places' prizes. No split, no cut and no `ticketCurrency`; NFT tickets come back too. Set \"closesBefore\", or \"timeWeighted\", so a late buyer doesn't share the pot with tickets held all period."
1489
1510
  },
1490
1511
  "minSold": {
1491
1512
  "type": "integer",
@@ -1619,8 +1640,8 @@
1619
1640
  },
1620
1641
  "losersEach": {
1621
1642
  "type": "integer",
1622
- "minimum": 1,
1623
- "description": "In a contest of NFTs (`stakeNft`), the most losers' NFTs one winner takes; what the winners leave goes to whoever opened it. Default: no cap, the winners share every loser's NFT (the first to enter on the winning side take one more when they don't divide evenly), each picking which."
1643
+ "minimum": 0,
1644
+ "description": "In a contest of NFTs (`stakeNft`), the most losers' NFTs one winner takes; what the winners leave goes to whoever opened it (0: every loser's NFT goes to the opener). Default: no cap, the winners share every loser's NFT (the first to enter on the winning side take one more when they don't divide evenly), each picking which."
1624
1645
  }
1625
1646
  }
1626
1647
  },
@@ -2062,16 +2083,14 @@
2062
2083
  "oneOf": {
2063
2084
  "type": "array",
2064
2085
  "minItems": 1,
2065
- "maxItems": 16,
2066
2086
  "items": { "type": "string" },
2067
- "description": "The attribute is one of these texts (at most 16): a tier of two, one species of three."
2087
+ "description": "The attribute is one of these texts: a tier of two, one species of forty. As many as the record holds; a lottery's NFT ticket takes at most 170, and a box's gate about 340 (what one answer of the oracle's reads)."
2068
2088
  },
2069
2089
  "items": {
2070
2090
  "type": "array",
2071
2091
  "minItems": 1,
2072
- "maxItems": 16,
2073
2092
  "items": { "$ref": "#/$defs/key" },
2074
- "description": "Any of these items the game mints (at most 16), in its own collection: a farm for three tree species, a gate for holders of any of five plots. Read by the program as the collection's `item_id` being one of theirs; not with `item`, `collection` or `attribute`."
2093
+ "description": "Any of these items the game mints, in its own collection: a farm for three tree species, a gate for holders of any of five plots. Read by the program as the collection's `item_id` being one of theirs; not with `item`, `collection` or `attribute`."
2075
2094
  },
2076
2095
  "time": {
2077
2096
  "enum": ["ahead", "passed"],
@@ -2082,8 +2101,32 @@
2082
2101
  "boost": {
2083
2102
  "type": "number",
2084
2103
  "minimum": 0,
2085
- "maximum": 10000,
2086
- "description": "A percent added to a weight: 100 doubles it. At most 10,000 (101 times): enough for a stat that scales a rate, such as +100% a point of power up to 100."
2104
+ "maximum": 100000,
2105
+ "description": "A percent a farm's boost adds to a weight: 100 doubles it. At most 100,000 (1,001 times), alone and all of a farm's boosts together: enough for a stat that scales a rate, such as +100% a point of power up to 1,000."
2106
+ },
2107
+ "oddsBoost": {
2108
+ "type": "number",
2109
+ "minimum": 0,
2110
+ "maximum": 42949672.95,
2111
+ "description": "A percent a box's odds rule adds to an outcome's weight: 100 doubles it, 99,900 makes it a thousand times likelier. Any amount: a boosted weight stops at the largest weight (4,294,967,295), and two outcomes boosted past it are equally likely."
2112
+ },
2113
+ "oddsEffect": {
2114
+ "type": "object",
2115
+ "additionalProperties": false,
2116
+ "description": "How much a collectible that passes the rule adds to the outcome's weight: a flat percent, or a percent per point of its attribute (up to `upTo` points).",
2117
+ "properties": {
2118
+ "addPercent": {
2119
+ "$ref": "#/$defs/oddsBoost"
2120
+ },
2121
+ "perPointPercent": {
2122
+ "$ref": "#/$defs/oddsBoost"
2123
+ },
2124
+ "upTo": {
2125
+ "type": "integer",
2126
+ "minimum": 1,
2127
+ "maximum": 4294967295
2128
+ }
2129
+ }
2087
2130
  },
2088
2131
  "effect": {
2089
2132
  "type": "object",
@@ -2119,7 +2162,7 @@
2119
2162
  "$ref": "#/$defs/rule"
2120
2163
  },
2121
2164
  "add": {
2122
- "$ref": "#/$defs/effect"
2165
+ "$ref": "#/$defs/oddsEffect"
2123
2166
  }
2124
2167
  }
2125
2168
  },
@@ -2135,9 +2178,9 @@
2135
2178
  "multiplier": {
2136
2179
  "type": "number",
2137
2180
  "exclusiveMinimum": 0,
2138
- "maximum": 11,
2181
+ "maximum": 100,
2139
2182
  "default": 1,
2140
- "description": "Its weight: above 0 and up to 11 times what it stakes (below 1: a flexible tier that earns less)."
2183
+ "description": "Its weight: above 0 and up to 100 times what it stakes (below 1: a flexible tier that earns less)."
2141
2184
  },
2142
2185
  "prize": {
2143
2186
  "$ref": "#/$defs/prize",
@@ -2631,9 +2674,9 @@
2631
2674
  },
2632
2675
  "weight": {
2633
2676
  "type": "integer",
2634
- "minimum": 1,
2677
+ "minimum": 0,
2635
2678
  "maximum": 4294967295,
2636
- "description": "Its weight in the stash's draws (1 when left out): an NFT that weighs 10 is drawn ten times as often as one that weighs 1."
2679
+ "description": "Its weight in the stash's draws (1 when left out): an NFT that weighs 10 is drawn ten times as often as one that weighs 1. 0: never drawn, given only by name."
2637
2680
  }
2638
2681
  }
2639
2682
  },
@@ -2656,9 +2699,9 @@
2656
2699
  },
2657
2700
  "weight": {
2658
2701
  "type": "integer",
2659
- "minimum": 1,
2702
+ "minimum": 0,
2660
2703
  "maximum": 4294967295,
2661
- "description": "Its weight in the stash's draws (1 when left out): an NFT that weighs 10 is drawn ten times as often as one that weighs 1."
2704
+ "description": "Its weight in the stash's draws (1 when left out): an NFT that weighs 10 is drawn ten times as often as one that weighs 1. 0: never drawn, given only by name."
2662
2705
  }
2663
2706
  }
2664
2707
  },
@@ -2673,9 +2716,9 @@
2673
2716
  },
2674
2717
  "weight": {
2675
2718
  "type": "integer",
2676
- "minimum": 1,
2719
+ "minimum": 0,
2677
2720
  "maximum": 4294967295,
2678
- "description": "Its weight in the stash's draws (1 when left out): an NFT that weighs 10 is drawn ten times as often as one that weighs 1."
2721
+ "description": "Its weight in the stash's draws (1 when left out): an NFT that weighs 10 is drawn ten times as often as one that weighs 1. 0: never drawn, given only by name."
2679
2722
  }
2680
2723
  }
2681
2724
  }
@@ -2815,14 +2858,15 @@
2815
2858
  },
2816
2859
  "weight": {
2817
2860
  "type": "integer",
2818
- "minimum": 1,
2819
- "maximum": 4294967295
2861
+ "minimum": 0,
2862
+ "maximum": 4294967295,
2863
+ "description": "Its weight in the draws. 0: never drawn."
2820
2864
  },
2821
2865
  "stock": {
2822
2866
  "type": "integer",
2823
- "minimum": 1,
2867
+ "minimum": 0,
2824
2868
  "maximum": 4294967294,
2825
- "description": "How many units may draw it: left out, never runs out."
2869
+ "description": "How many units may draw it: left out, never runs out. 0: never drawn."
2826
2870
  }
2827
2871
  }
2828
2872
  }