@xgamesfun/solana 0.0.0-stage → 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.
Files changed (138) hide show
  1. package/DISCLAIMER.md +41 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +13 -0
  4. package/README.md +172 -2
  5. package/bin/xsolana.mjs +189 -0
  6. package/examples/artwork-auction.json +51 -0
  7. package/examples/blind-forge.json +128 -0
  8. package/examples/coin-flip-arena.json +103 -0
  9. package/examples/duel-arena.json +137 -0
  10. package/examples/elements-set.json +274 -0
  11. package/examples/event-badges.json +67 -0
  12. package/examples/expedition-guild.json +154 -0
  13. package/examples/fair-presale.json +48 -0
  14. package/examples/falling-drop.json +90 -0
  15. package/examples/guild-missions.json +137 -0
  16. package/examples/halving-seasons.json +107 -0
  17. package/examples/hero-trials.json +93 -0
  18. package/examples/ideas.json +789 -0
  19. package/examples/level-ladder.json +65 -0
  20. package/examples/merge-2048.json +128 -0
  21. package/examples/nft-raffle.json +33 -0
  22. package/examples/paper-hands-farm.json +44 -0
  23. package/examples/partner-vault.json +112 -0
  24. package/examples/sacrifice-raffle.json +118 -0
  25. package/examples/scorched-treasury.json +84 -0
  26. package/examples/shelter-fair.json +164 -0
  27. package/examples/spark-pity.json +153 -0
  28. package/examples/stakers-draw.json +118 -0
  29. package/examples/star-fleet.json +150 -0
  30. package/examples/starforce.json +116 -0
  31. package/examples/stud-farm.json +134 -0
  32. package/examples/team-vesting.json +70 -0
  33. package/examples/trade-in-shop.json +168 -0
  34. package/js/approve/details.mjs +263 -0
  35. package/js/approve/ed25519.mjs +36 -0
  36. package/js/approve/live-check.mjs +144 -0
  37. package/js/approve/page.html +86 -0
  38. package/js/approve/page.js +203 -0
  39. package/js/approve/server.mjs +726 -0
  40. package/js/chain/base58.js +73 -0
  41. package/js/chain/borsh.js +270 -0
  42. package/js/chain/bytes.js +141 -0
  43. package/js/chain/core.js +181 -0
  44. package/js/chain/curve.js +52 -0
  45. package/js/chain/lookup.js +279 -0
  46. package/js/chain/mint.js +96 -0
  47. package/js/chain/pda.js +66 -0
  48. package/js/chain/programs.js +102 -0
  49. package/js/chain/rpc.js +219 -0
  50. package/js/chain/sha256.js +68 -0
  51. package/js/chain/token.js +85 -0
  52. package/js/chain/token2022.js +247 -0
  53. package/js/chain/tx.js +224 -0
  54. package/js/client/accounts.js +679 -0
  55. package/js/client/compressed.js +46 -0
  56. package/js/client/grant.js +299 -0
  57. package/js/client/idl/loot.js +2 -0
  58. package/js/client/idl/loot.json +17206 -0
  59. package/js/client/index.js +10 -0
  60. package/js/client/ix.js +2887 -0
  61. package/js/client/pda.js +114 -0
  62. package/js/client/policy.js +69 -0
  63. package/js/client/random.js +70 -0
  64. package/js/client/sizes.js +297 -0
  65. package/js/client/traits.js +44 -0
  66. package/js/connect/connect.mjs +182 -0
  67. package/js/economy/check.mjs +2236 -0
  68. package/js/economy/cost.mjs +185 -0
  69. package/js/economy/examples.mjs +114 -0
  70. package/js/economy/explain.mjs +239 -0
  71. package/js/economy/guide.mjs +232 -0
  72. package/js/economy/legal.mjs +15 -0
  73. package/js/economy/live.mjs +130 -0
  74. package/js/economy/model.mjs +698 -0
  75. package/js/economy/plan.mjs +1155 -0
  76. package/js/economy/schema.json +3071 -0
  77. package/js/economy/secrets.mjs +60 -0
  78. package/js/economy/simulate.mjs +1019 -0
  79. package/js/economy/txsize.mjs +45 -0
  80. package/js/economy/validate.mjs +85 -0
  81. package/js/index.js +10 -0
  82. package/js/mcp/server.mjs +113 -0
  83. package/js/mcp/tools.mjs +485 -0
  84. package/js/play/index.js +2855 -0
  85. package/js/play/keys.js +167 -0
  86. package/js/results/index.js +184 -0
  87. package/js/results/wire.js +90 -0
  88. package/package.json +100 -4
  89. package/release.json +8 -0
  90. package/skills/xsolana-economy/SKILL.md +109 -0
  91. package/skills/xsolana-economy/references/after-launch.md +124 -0
  92. package/skills/xsolana-economy/references/apps.md +193 -0
  93. package/skills/xsolana-economy/references/sentence-to-spec.md +180 -0
  94. package/skills/xsolana-economy/references/telling-the-person.md +134 -0
  95. package/types/chain/base58.d.ts +10 -0
  96. package/types/chain/borsh.d.ts +23 -0
  97. package/types/chain/bytes.d.ts +45 -0
  98. package/types/chain/core.d.ts +52 -0
  99. package/types/chain/curve.d.ts +1 -0
  100. package/types/chain/lookup.d.ts +194 -0
  101. package/types/chain/mint.d.ts +25 -0
  102. package/types/chain/pda.d.ts +25 -0
  103. package/types/chain/programs.d.ts +65 -0
  104. package/types/chain/rpc.d.ts +68 -0
  105. package/types/chain/sha256.d.ts +1 -0
  106. package/types/chain/token.d.ts +141 -0
  107. package/types/chain/token2022.d.ts +53 -0
  108. package/types/chain/tx.d.ts +60 -0
  109. package/types/client/accounts.d.ts +318 -0
  110. package/types/client/compressed.d.ts +29 -0
  111. package/types/client/grant.d.ts +170 -0
  112. package/types/client/idl/loot.d.ts +4498 -0
  113. package/types/client/index.d.ts +9 -0
  114. package/types/client/ix.d.ts +5315 -0
  115. package/types/client/pda.d.ts +95 -0
  116. package/types/client/policy.d.ts +48 -0
  117. package/types/client/random.d.ts +26 -0
  118. package/types/client/sizes.d.ts +101 -0
  119. package/types/client/traits.d.ts +12 -0
  120. package/types/economy/check.d.mts +100 -0
  121. package/types/economy/cost.d.mts +69 -0
  122. package/types/economy/examples.d.mts +27 -0
  123. package/types/economy/explain.d.mts +19 -0
  124. package/types/economy/guide.d.mts +140 -0
  125. package/types/economy/legal.d.mts +12 -0
  126. package/types/economy/live.d.mts +124 -0
  127. package/types/economy/model.d.mts +431 -0
  128. package/types/economy/plan.d.mts +1014 -0
  129. package/types/economy/secrets.d.mts +4 -0
  130. package/types/economy/simulate.d.mts +164 -0
  131. package/types/economy/txsize.d.mts +17 -0
  132. package/types/economy/validate.d.mts +3 -0
  133. package/types/index.d.ts +9 -0
  134. package/types/mcp/tools.d.mts +635 -0
  135. package/types/play/index.d.ts +880 -0
  136. package/types/play/keys.d.ts +51 -0
  137. package/types/results/index.d.ts +155 -0
  138. package/types/results/wire.d.ts +122 -0
@@ -0,0 +1,232 @@
1
+ // What an economy can be made of, read from the schema, and what loot cannot do: the guide an agent reads first.
2
+ // Pure.
3
+ import { FEES, LAMPORTS_PER_SOL, ORACLE_FEE } from './model.mjs';
4
+ import { SCHEMA } from './validate.mjs';
5
+ import { IDL } from '../client/ix.js';
6
+
7
+ const sol = (lamports) => Number(lamports) / Number(LAMPORTS_PER_SOL);
8
+
9
+ /** Each part: what it is for, who acts at play time and its fee, and the nested shapes worth showing. */
10
+ export const PARTS = {
11
+ tokens: { def: 'token', says: 'The currencies: SOL, any existing mint by address (classic or Token-2022, coins from a token launchpad or an AMM included), or a token the game makes when it goes live: one the game mints as its rules say (points, tickets, shards, an emission), or one with a fixed `supply` minted once to the owner with minting turned off for good (a community token you keep). loot does not put a token on a market: for trading, launch it on a token launchpad or an AMM, then name the mint.', acts: 'nobody: a token is a fact' },
12
+ currency: { says: 'The key of the token that is the game’s own currency: what the shop is priced in and what the treasury holds. Fixed once live.' },
13
+ collection: { def: 'collection', says: 'The Metaplex Core collection the project’s collectibles are minted into, made at setup; `false` makes none (a project of tokens only). `collections` adds Core collections your wallet already controls (as many as the project’s setup transaction carries, about 25 in all): an NFT of any of them is the project’s own, and an item can be minted into one with its `collection` (once you add the project as an update delegate of it). `projectNumber` numbers the project among your wallet’s (its address is ["project", owner, number]), so one wallet can run several economies.' },
14
+ items: { def: 'item', nested: ['rule'], says: 'What the game sells, drops or makes: collectibles (minted to the player, with attributes), and normal items (kept by the project’s backend). A price puts an item in the shop, sold in one step (`buy`: no order, nothing to settle). `{serial}` in its `uri` is each unit’s serial, so each can have its own art.', acts: `the player buys (${sol(FEES.player)} SOL player fee), and gets it in the same transaction (a normal item: the project’s backend delivers it from the receipt)` },
15
+ pools: { def: 'pool', says: 'Vaults the game’s parts pay from (a prize pool, a buyback reserve), which no player can take from. The authority can also move what a pool holds (`move_tokens`, a payout decision), so players trust it with what is in them; a lottery’s pot and what a farm owes its stakers are vaults it can’t touch (a farm’s rewards land in a vault of the farm’s own). `seed` is what the owner puts in at the start.', acts: 'nobody directly: other parts fill and drain them' },
16
+ stashes: { def: 'stash', says: 'NFTs the game holds to give away as prizes, of any collection and any kind (Core with or without the game’s authority, Token Metadata legacy and pNFT, compressed): a partner’s NFTs, a treasury’s rare pieces, one artwork to raffle. Your wallet puts in each NFT it lists when the economy goes live (it must hold them then; a compressed one goes in after, with `loot.proveLeafIx` and `loot.stashIx`). A prize { "gives": "nft", "stash": key, "nft": address } gives that one NFT at once (once: the first winner takes it); a box outcome { "gives": "stash", "stash": key } gives one of them at random, taken out at the roll, so two winners never get the same one; a draw that lands on an empty stash isn’t made, and gives back that draw’s worth. Several stashes with weights are rarity tiers. Give each route its own stash when a shop and a raffle must not empty it for each other.', acts: `the owner or the authority puts an NFT in or takes one out (${sol(FEES.project)} SOL); anyone settles a roll that won one` },
17
+ boxes: { def: 'box', nested: ['outcome', 'odds', 'gate', 'rule', 'effect', 'rise', 'fall', 'split', 'perWalletEach'], says: 'A paid (or free) roll on chain with MagicBlock’s verifiable randomness, optionally on an NFT (`rollsOn`: an enchant, a mission’s result, a risky forge), whose outcomes then move its numbers (`shifts`) and send it back, burn it or give it away (`then`; a box that can burn or give it away holds it until the roll settles): each of up to 128 outcomes gives an item, a fixed prize from a pool, tokens minted (`tokens`), an NFT of a stash (picked at random, or a named one: `nft`), a roll of another box (`roll`), up to 14 of these together (`bundle`: { "gives": "bundle", "prizes": [...] }, a pack; it gives all it names or isn’t given, so it names a pool once, a stash’s random NFT beside nothing else of that stash, and two of an item need two left), or nothing. A box of one outcome with nothing random and no NFT it rolls on is bought in one step (`buy`): the price split and what it gives handed over at once, with no order and no oracle. A box takes an NFT as its price through `rollsOn` (which NFTs) and `then` (where the one paid goes: burned, given, or into a stash, a trade-in that becomes another player’s prize). A box is also the prize any route gives: a grant (`gives: { "roll": box }`), a lottery’s place (`{ "percent", "roll" }`), a contest’s winners (`roll`), an auction’s lot (`{ "roll": box }`), a mission’s tier and a recipe roll it for a winner, who gets whatever it gives; such a box is left `active: false`. A roll is on its written odds whatever is in stock: an outcome drawn that can’t be given then makes no draw, no other takes its place, and that draw’s worth of the price goes back at the settle; with no draw made a paid open fails and is refunded, and a free roll goes back to waiting. A sale in one step sells only while its pools hold every fixed prize. A prize won is set aside for its winner at once. An edit of a box’s roll fails only a purchase still waiting (for its refund); any other roll waiting rolls on the box as edited. A gate (a collectible shown, a few wallets, or a list of any size: `{ "list": [address, …] }`, an allowlist whose proof each player passes) and odds can read what a player shows; `opens`/`closes` time a drop; `perWallet`/`perWalletEach` limit each wallet (one ticket per wallet). A split share "chosen" lets the buyer name a referrer; "chosen:<item>" the holder of a collectible they name (plot rent).', acts: `the player opens (${sol(FEES.player)} SOL + ${sol(ORACLE_FEE)} SOL to the oracle); its player, a key they named or the project’s keys settle. Chance that pays money is flagged` },
18
+ payments: { def: 'payment', nested: ['split'], says: 'A player pays for something given off chain (an attempt, an entry, a tip), split up to four ways at once. With `any`, the player chooses the amount, at least `amount` (tips, donations). Its split may name one `"chosen"` share, a wallet the payer names each time they pay (a referrer: `app.pay(key, { chosenRecipient })`), never their own; not `"chosen:<item>"`.', acts: `the player pays (${sol(FEES.player)} SOL)` },
19
+ recipes: { def: 'recipe', nested: ['input', 'output'], says: 'Collectibles and tokens in, collectibles and tokens out, all or nothing: merges, upgrades, salvage, breeding, exchanges, repairs, a daily claim. `raises` buy a level or a stat on a collectible kept (+1 level for 100 DUST, up to 10), or with `fromNow` move a time (a day of fuel, a day off a lock); a `staked` line takes a collectible only while it is staked in the farm it names (`farm`; a set bound into a badge, its raises moving that farm’s `lockAttribute`), a `show` line takes any NFT of a collection, the project’s or not (its stats live in the project’s Sheet of it), so players can train an NFT the project didn’t make, and a `trade` line takes one and keeps it: burned, given on, or into a stash (`then`), a trade-in. Each `out` line is a prize (the same prize line every route takes: an item, tokens minted or from a pool, a share of a pool, an NFT of a stash, a roll of a box), given in the craft; `{ "gives": "roll", "box": key, "on": place }` rolls on a collectible it keeps (a risky upgrade).', acts: `the player crafts (${sol(FEES.player)} SOL)` },
20
+ schedules: { def: 'schedule', says: 'On a timer: mint a game token into a pool, burn from a pool, move between pools, or pay a wallet, an account, a collectible’s holder, a lottery’s pot ("lottery:<key>") or a farm’s rewards ("farm:<key>"). `start` sets the first run (a cliff), `maxRuns` the last (a vesting of 12 monthly runs), `halveEvery` halves it every N runs; halving with `maxRuns` caps the total for good (an emission with a fixed most). A run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse; only a sealed schedule’s fixed amount pays every run due. `sealed` (with `fund`): it pays a wallet, the treasury or a burn from a vault of its own that nobody, the owner included, can change, stop or take back; its last run pays what is left.', acts: 'the project’s keys, or who it pays, run it when it is due (a sealed one needs nobody else)' },
21
+ lotteries: { def: 'lottery', nested: ['gate', 'rule', 'split'], says: 'Tickets into a pot of its own (nothing but its winners’ payouts takes from it), a draw on a timer by the oracle, up to 64 runs of places (a place with `times` is a run of places alike: { "percent": 1, "times": 100 } is a hundred). `split` sends part of ticket money elsewhere first (a charity, a referrer the buyer names with "chosen", the treasury); `seed` fills the pot at go-live; anything can pay into it ("lottery:<key>"). Leave out `every` for a raffle that draws once and ends. `perWallet` caps each wallet’s tickets in a round. A place can win a prize instead of (or as well as) a share of the pot: `{ "percent": 0, "prize": { "gives": "nft", "stash": key, "nft": address } }` raffles one NFT, which its winner pulls after the draw (if that NFT is gone from its stash, a stranger’s pull is refused: the winner or a project key passes it over); a lottery that draws once with `nfts` (put in its own stash at go-live) and places { "prize": { "gives": "raffled" }, "times": n } raffles many, each winner drawing one at random; `{ "gives": "roll", "box": key }` makes it a random prize. `ticketNft` ({ "rule", "then" }) makes a ticket cost one NFT instead of tokens: held until the draw, then burned, given on or stashed (back to its buyer if the draw is cancelled; no price, no split). The draw’s ask fixes the round’s money and opens the next round at once; then anyone pulls each part (a refund, a place, a share) in any order, and the last pull closes the round. The owner puts SOL in the lottery at go-live for its rounds’ records and every ticket’s entry (`roundRent`, on by default and sized from `expect`: rounds at once and purchases a round; back at each close), so buyers pay no program rent; 0 turns it off, and then a round’s first buyer fronts its record and each buyer pays their entry’s bytes. `noLoss: true` is a no-loss lottery: every ticket (tokens or an NFT) 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 to "lottery:<key>"); no split, no cut, no `ticketCurrency`. Tickets are not weighted by time: someone may buy most of a round’s tickets right before the draw and risk only the fees.', acts: `players buy tickets (${sol(FEES.player)} SOL); anyone draws and pays (free). Flagged as chance` },
22
+ farms: { def: 'farm', nested: ['tier', 'rule', 'effect', 'growth'], says: 'Staking: players stake a token or NFTs of any kind (the game’s, or any collection its gate names: Core, Token Metadata legacy and pNFT, compressed) and share what lands in the farm’s own rewards vault by weight (`rewards` names its token; fund it with `seed`, a share "farm:<key>" or a schedule that pays it; what lands is released to the seats over the farm’s longest lock, a day at least, so a stake earns only while it stays and nobody takes what was funded before them at once), or earn a fixed `rate` a day each (minted, or from the vault while it lasts, as most NFT staking pays; `shared` makes it the whole farm’s a day, shared by weight, and `halveEvery` halves it), with lock tiers (x1 to x11) and up to 32 boosts from what a collectible says. `growth` writes points (and a level, from `levels`) on a staked collectible for each day in, at its tier and boosts, with nobody’s signature: on the NFT when the game has authority over it, in its Sheet otherwise; a boost per level, a gate, box odds, a recipe’s need or a grant then read that level. A boost counts a level from when it is written, by a seat’s refresh, so a farm whose boost reads what its own growth writes should have its page or a keeper refresh seats (anyone may, for free). A tier’s `prize` (a prize line, as everywhere; it needs a `lock` above 0) makes it a mission: a collectible that stays its lock (topped up, not forfeited) leaves with it, given as it leaves: tokens (minted or from a pool, grown by its boosts), an item, an NFT of a stash, or a chest (`{ "gives": "roll", "box": key }`, settled after the oracle). A tier’s `roll` of a box with `rollsOn` is a roll on the NFT leaving it (success, a stat lost, the NFT lost). A collectible can also boost a token seat. `growth` takes up to 32 (+1 "level" and +2 "affection" a day; without `levels` the points are the level; up to 128 levels each), `gate` up to 32 rules an NFT must all pass, and `runs` ends a farm that long after it goes live (a mission open for a week): it takes no stake at its end or after it, nor one whose lock would run past it, and in a farm that shares what lands, what is not released by the end stays in its vault, as does what a seat forfeits after it, so fund it at least its release span before the end. A tier’s `earnsFor` is a term: a seat earns and grows that long after its lock started, and what it earned by then is exact however late it is next touched: nobody has to act at its end. A farm with a term or an upkeep that pays keeps a record of those times: each new one costs whoever writes it 44 bytes of rent (about 0.00022 SOL), not refunded on leaving, and a farm idle past very many of them is walked in parts first (`app.walkFarm`, anyone’s, free). A `rate` of 0 pays nothing: a mission that only grows or gives prizes. Escrow is the default (the game holds the NFT); Freeze locks it in the wallet, with release dependent on Core handling later issuer metadata changes; Watch leaves it free, earning only while its staker holds it, so one NFT can sit in a farm and its missions at once. Soulbound collectibles require Freeze. A farm of collectibles can keep two times on the NFT, as numbers recipes and rolls move: `lockAttribute` (its lock, written as the time it ends: a recipe that raises it by "-1d" `fromNow` for a price is a way out early, a roll that adds "1d" is jail) and `upkeepAttribute` (fuel: it earns and grows only until that time; a recipe sells it "+1d" `fromNow`, up to `upTo` ahead, and it runs out by itself). A recipe reaches a collectible staked in escrow on its seat. Never edited once open: a new mission is a new farm, added to the spec and published.', acts: `the player stakes, unstakes and harvests (${sol(FEES.player)} SOL each); pause and suspend do not stop an unstake; external custody controls still apply` },
23
+ contests: { def: 'contest', nested: ['split', 'gate', 'rule'], says: 'Players back a side; the side that wins shares the pot by stake, after the rake. The authority names it, or a `deciders` key (a results server) signs it, so the authority’s key never sits on a server. One side and a `minPot` is an all-or-nothing crowdfund (short of it, everyone takes their money back); a wallet gate and a `stake` is a 1v1 wager. A contest whose result is overdue is off and pays everyone back. `prize` gives every winning entry a prize too, when it is claimed (a collectible, tokens, an NFT, a roll of a box); one scarce prize goes to the entry claimed first. `stakeNft` (a rule) makes each entry stake one NFT instead of tokens: the NFTs are the pot, each winner takes its own back and picks its share of the losers’ (`losersEach` caps it: 1 for “winner takes one”), and the opener gets what the winners leave.', acts: `the authority opens and decides (${sol(FEES.project)} SOL each); players enter (${sol(FEES.player)} SOL). Flagged as betting` },
24
+ auctions: { def: 'auction', nested: ['split', 'rule'], says: 'An English auction, opened when the economy goes live: the highest bid when the time runs out wins one new unit of an item, any Core NFT the owner holds (give its `collection` when it isn’t the game’s), or a roll of one of the game’s boxes (`{ "roll": box }`, a mystery lot; its price goes to the treasury). Bids in any token, SOL included; each reaches the reserve and tops the last by the least raise; the bid it tops goes back to its bidder at once. A late bid moves the end. With no bid the lot goes home; an auction nobody ends waits, and still sells to its high bidder whenever someone ends it. A seller of an NFT whose account can’t take the money doesn’t hold up the winner: the lot goes, and the auction stays open owing it until a later end pays and closes it. `bidNfts` (a rule) makes a bid a bundle of up to three Core or Token Metadata NFTs instead of tokens (never compressed ones: a bid hands the one it tops back at once, and a compressed NFT’s tree can be changed under it): the most NFTs wins, and the reserve counts NFTs.', acts: `players bid (${sol(FEES.player)} SOL each); anyone ends it once it is over (free)` },
25
+ holds: { def: 'hold', says: 'A player hands a collectible to the project for a time they choose (`for`), or until a set time (`until`: a raid at 18:00); until it is up the authority says where it goes, after it anyone sends it home. A hold’s address names its end, so a late award never lands on a later hold of the NFT.', acts: `the player hands it in (${sol(FEES.player)} SOL); the authority releases it (${sol(FEES.project)} SOL)` },
26
+ grants: { def: 'grant', nested: ['gate', 'rule', 'perWalletEach'], says: 'Rewards handed out to whoever earned them: a `prize` given in the claim itself (a collectible, tokens minted or from a pool, one NFT of a stash), or a roll of a box (`{ "gives": "roll", "box": key }`: a quest chest, a daily spin, the grant’s funds paying its order). A claim of a prize it can’t give is refused, and keeps its round. Only the winning wallet claims: it signs every claim, whatever proves it won (a voucher, a list, an open grant); anyone may pay its fees, nobody claims for it. A voucher key (a results server: the results server, or one you run) signs who earned it, or one of `keys` (one per promo code: the page signs with the code’s key, and `perKey: 1` pays each code once), or the wallet is on a list. With none of them it is open: any wallet claims within its limits (with a gate, each collectible shown claims once too). The program keeps every claim within its limits per wallet (in all and per window), per person, per key, per day and in total. A grant of a fixed amount of tokens paid by a voucher or a list pays each claim the amount the voucher (or the list’s leaf) names, up to the grant’s own: one grant for rewards by rank or share (1st 500, 2nd 300, …), with the grant’s amount as the most any claim takes; 0 or none named pays the grant’s amount.', acts: `anyone sends a claim and pays the ${sol(FEES.player)} SOL player fee; the grant’s funds pay its rent, and the sender what they can’t` },
27
+ decisions: { def: 'decision', says: 'The authority’s rules in words: what only the game knows (who cleared the level, who won) and what the program then does (a payout, a burn, a mint, a collectible made, rewritten or burned, a held collectible released, a roll).', acts: `the authority signs each one (${sol(FEES.project)} SOL)` },
28
+ keys: { says: '`authority`: "owner" (the owner’s wallet is both keys) or another wallet’s public address (a server’s). Never a private key.' },
29
+ scenario: { def: 'scenario', says: 'What the quick simulation assumes: how many of each thing a day ("box:flip": 1000), days, how much is staked in each farm.' },
30
+ };
31
+
32
+ export const CONVENTIONS = [
33
+ 'The file is xsolana.json: { "version": 2, "network": "devnet", … }.',
34
+ 'Amounts are whole units of their currency: 0.1 is 0.1 SOL. Write "0.1" as text when it must be exact.',
35
+ 'Shares are percents with at most two decimals: 2.5 is 2.5%.',
36
+ 'Times are durations ("45s", "30m", "6h", "7d", "2w", or seconds); a start is "publish", "publish+7d" or a date like "2026-11-01T18:00:00Z".',
37
+ 'Every part has a `key` (lower case, digits, dashes) that other parts name: a box pays from "pool": "house"; a split sends to "pool:house".',
38
+ `A split is any number of shares of a payment, each { "to": "burn" | "treasury" | "pool:<key>" | "lottery:<key>" | "wallet:<address>" | "account:<token account>" | "holder:<collectible address>" | "chosen" | "chosen:<item>", "percent": n }; what they leave goes to the treasury, and so does a share that can’t be paid (a wallet with no account, a collectible nobody holds): a sale never fails over it.`,
39
+ 'The treasury, where what a split leaves goes (and a lottery’s cut), is the owner’s own wallet: the program pays its account of whatever currency was paid, and opens it when it is not there. `"treasury": "wallet:<address>"` makes it another wallet instead (a charity, a team’s multisig vault).',
40
+ 'A box’s outcomes each have a `chance` (percents adding up to 100) or each a `weight` (whole numbers). An outcome gives one thing, or a bundle of any number ({ "gives": "bundle", "prizes": [{ "gives": "item", … }, { "gives": "pay", … }] }).',
41
+ 'An NFT can be the price: a box’s `rollsOn` and `then`, a recipe’s `trade` line, a lottery’s `ticketNft`, a contest’s `stakeNft`, an auction’s `bidNfts`. Where an NFT paid goes (`then`): "burn", "treasury", { "giveTo": address } or { "stash": key }. A payment with an NFT is a runtime call (`loot.payNftIx`), not a part of the spec.',
42
+ 'A prize is one line, the same on every route that gives one (a grant’s `prize`, a lottery place’s `prize`, a contest’s `prize`, a mission tier’s `prize`, a recipe’s `out` lines): { "gives": "item", "item": key } (with `"compressed": true`, a compressed NFT of it: no rent each, minted into a tree the setup opens, `compressedTree`), { "gives": "tokens", "amount": n, "from": "mint" (with "currency") or "pool:<key>": a fixed amount, all of it or none }, { "gives": "nft", "stash": key, "nft": address } or { "gives": "roll", "box": key }. All but a roll are given in the transaction that wins them; use a roll only for chance.',
43
+ 'A rule (a gate, odds, a boost) is { "item": key (or left out for any), "attribute": "level", "atLeast": 3 } or { …, "equals": "gold" }, or { …, "time": "ahead" } (a time still to come: fuel left) or "passed" (out of jail), or just an attribute for anything. A gate may instead list wallets: { "wallets": [address, …] } (at most 16), or { "list": [address, …] } (any size: the program keeps its root, and each player passes their proof), or a stake: { "staked": { "farm": key, "atLeast": n } } (the wallet has at least n of a farm of tokens’ token staked in it and still locked, staked before the second it is used: launchpad tiers, stake to enter. A stake counts only while its tier’s lock runs, so one stake can’t be unstaked, moved to another wallet and count again; a tier with no lock never passes. The app passes the wallet’s seat and the farm).',
44
+ 'A box, a shop item and a recipe take `opens` and `closes` (times), and `perWallet` (in all) and `perWalletEach` ({ "count": 1, "every": "1d" }) to limit each wallet.',
45
+ 'Leave out `id`, `index`, `shopBox` and `live`: they are written when the economy goes live.',
46
+ 'Public data only. A spec never holds a private key or a seed phrase (the checker refuses one, E-SECRET).',
47
+ 'Devnet only: "network": "devnet".',
48
+ ];
49
+
50
+ /**
51
+ * What no part of loot does, and the combinations that come closest: { what, words (to match a person's own words),
52
+ * why, nearest: [the version, and what it gives up] }. A limit is never the end of the answer: say what is missing,
53
+ * then offer the nearest versions with their numbers, best first, and build the one the person picks. A feature that
54
+ * ships later moves its entry to HOW.
55
+ */
56
+ export const CANNOT = [
57
+ {
58
+ what: 'a prize that is a share of what a pool holds (a jackpot paying a tenth of itself)',
59
+ words: 'jackpot grows growing share percent of the pool tenth prize progressive pot winner takes share of pool',
60
+ why: 'a prize is what its record says, all of it or none, on every route: never what an account holds at the time',
61
+ nearest: [
62
+ 'a lottery: its pot is what its tickets fill, it grows round by round, and its places take percents of it; gives up a win at any time for a draw on a timer',
63
+ 'a fixed prize from a pool a share of each sale fills (`{ "gives": "pay", "pool": key, "amount": n }`); gives up a prize that grows, and a draw that lands on it while the pool is short gives back its worth',
64
+ 'a voucher grant that names each claim\'s own amount, up to the grant\'s, worked out by the project\'s server from what the pool holds; gives up the rule living on chain',
65
+ ],
66
+ },
67
+ {
68
+ what: 'put a token on a market (a bonding curve with selling back, trading fees for a creator)',
69
+ words: 'bonding curve market trading fees launch pump.fun meteora swap liquidity sell back creator coin',
70
+ why: 'loot runs no market: it moves and sells what the game owns, it doesn’t trade between strangers',
71
+ nearest: [
72
+ 'launch the coin on a token launchpad or an AMM (a launchpad’s creator fees pay on trades), then name its mint as an existing token: loot’s shops, boxes, farms and gates use it; gives up nothing on loot’s side',
73
+ 'a shop listing whose price rises after each sale (`rise`), and a recipe that buys the token back at a fixed price from a reserve pool a share of each sale fills (`in`: a `pay` line burned; `out`: tokens from the pool): a one-way curve with a floor; gives up a market price, and the reserve is the authority’s to spend',
74
+ 'a game token with a fixed `supply` minted once to the owner, minting off for good: a community token the owner keeps and hands out; gives up a public market',
75
+ ],
76
+ },
77
+ {
78
+ what: 'an order book, an AMM, or a swap two players agree on live',
79
+ words: 'order book amm swap trade trading marketplace market escrow p2p player trades sell to each other offer bid',
80
+ why: 'loot has no order book or matching: a player’s money meets another player’s NFT only through an offer or a listing one of them made first',
81
+ nearest: [
82
+ 'a bid any wallet makes for any NFT that passes a rule (“2 SOL for any NFT of another collection”, “50 gold for any level-10 sword”), escrowed in the project’s vault the authority can’t pay from: `openOfferIx` with `deal: { Buy: { price } }` and `left` (how many), `shares` (a royalty, a cut); a holder sells into it with `sellToOfferIx`, the NFT straight to the bidder, the price to the seller; the bidder takes back what is left with `closeOfferIx`. Made from the game’s page at run time, not in xsolana.json',
83
+ 'a listing: a holder puts an NFT of any kind up at a fixed price (`openListingIx` with `deal: { Sell: { price } }`, `app.listForSale`); a buyer pays and takes it in one instruction (`takeListingIx`, `app.buyListing`); the maker takes it home with `closeListingIx`. Made from the game’s page at run time',
84
+ 'players trade the game’s collectibles on NFT marketplaces that list Metaplex Core assets, with the collection’s royalty to the owner',
85
+ 'an auction a player opens for a collectible of the game’s own collection they hold, signing alone (`openAuctionIx` with an asset lot): escrowed bids, the outbid refunded at once, the price (less any shares) to the seller; `bidNfts` makes the bids NFTs (an NFT-for-NFT swap by auction)',
86
+ 'a swap against the game: a recipe whose `trade` line takes the player’s NFT into a stash and whose `out` gives a named NFT of a stash (`{ "gives": "nft" }`), or a fixed bundle for another (3 iron → 1 steel); the owner can take stash NFTs out',
87
+ ],
88
+ },
89
+ {
90
+ what: 'a sealed-bid auction (nobody sees the bids until the end), a second-price (Vickrey) auction, or one decided by a random draw per block',
91
+ words: 'sealed blind vickrey second-price hidden',
92
+ why: 'every bid is a public transaction',
93
+ nearest: ['an open English auction (`auctions`: every bid public, the highest wins, the outbid refunded at once)', 'a shop listing whose price falls with time to a floor (a Dutch-style sale)'],
94
+ },
95
+ {
96
+ what: 'lending against tokens, liquidation by price, leverage, perpetuals, options or yield vaults',
97
+ words: 'lend lending borrow loan leverage perp perpetual options vault yield strategy collateral liquidation',
98
+ why: 'there are no price oracles or liquidations in loot: its only loan is a fixed-term loan against an NFT',
99
+ nearest: [
100
+ 'a fixed-term loan against an NFT between players, with no trust in the game: a lender opens an offer with `deal: { Lend: { amount, repay, lasts } }` for any NFT that passes a rule; a holder borrows (`borrowIx`: the NFT held by the project, `amount` to them); anyone repays `repay` before it is due (`repayIx`, which moves only the money), then anyone sends the NFT home (`releaseIx`, whenever: send both in one transaction); unpaid once due, anyone sends it to the lender (`releaseIx` with `to` the lender). No grace period, no partial repayment, no cut of the interest; the lender trusts the NFT’s collection not to block or pull back the NFT. The borrower trusts the loan’s token: whoever holds its freeze authority, a pause, a transfer hook or a permanent delegate can make repaying fail, and a lender who holds one can take the NFT when it is due (the program takes such tokens; `app.loanOffer` names each power and its holder, and `app.borrow` needs `acceptRisks`): lend in SOL or a plain token. Made from the game’s page at run time, not in xsolana.json',
101
+ 'a farm whose rewards the game sends into its vault (staking rewards, not interest)',
102
+ 'otherwise nothing in loot: say so plainly (leverage, perps and options need a protocol built for them)',
103
+ ],
104
+ },
105
+ {
106
+ what: 'liquid staking receipts or transferable share tokens of a vault',
107
+ words: 'liquid lst xtoken transferable-stake',
108
+ why: 'a token farm’s seat belongs to a wallet and isn’t transferable while staked',
109
+ nearest: ['for NFTs, a farm with `custody: "watch"`: the NFT stays free in the wallet, earns while held, can be sold, and its next holder takes the seat over', 'a token farm with lock tiers and boosts (the stake stays with its wallet)'],
110
+ },
111
+ {
112
+ what: 'paying every holder of a collection without any step by the holder',
113
+ words: 'every-holder dividend dividends snapshot all-holders airdrop-holders',
114
+ why: 'the program pays accounts it is given; it can’t list a collection’s holders by itself',
115
+ nearest: [
116
+ 'a farm of the collection with `custody: "watch"` at a fixed `rate`: the NFT never leaves the wallet, earns only while held; holders opt in once',
117
+ 'a voucher grant a script signs from a snapshot, each claim its own amount (in proportion to what each holds); a new round each payout',
118
+ 'a split share to the holder of one named collectible (`holder:<address>`)',
119
+ ],
120
+ },
121
+ {
122
+ what: 'recurring pulls from a wallet (subscriptions that charge by themselves)',
123
+ words: 'subscription recurring monthly auto-renew auto renew charge every month automatically',
124
+ why: 'nothing takes from a wallet without its signature',
125
+ nearest: [
126
+ 'a pass that carries a time (`paid_until`), a renew recipe that raises it 30 days `fromNow` for the price, and gates that read `{ "attribute": "paid_until", "time": "ahead" }`: the member renews in one step, the page reminds them; gives up the automatic charge only',
127
+ 'a pass sold again each season as a new item',
128
+ ],
129
+ },
130
+ {
131
+ what: 'a treasury that needs several people to sign (a multisig)',
132
+ words: 'multisig multi-sig 3 of 5 several signers officers council squads dao treasury approval',
133
+ why: 'loot isn’t a multisig, but any wallet can receive and any key can be the spending key',
134
+ nearest: ['payments and sales go to a multisig vault: `"treasury": "wallet:<vault>"` or a `wallet:<vault>` share; spending from it needs the vault’s approvals in the multisig’s own app', 'the multisig vault as `keys.authority`, so moving the game’s pools needs its approvals (sent from the multisig’s own app, not the approve page)'],
135
+ },
136
+ {
137
+ what: 'a ranking the chain works out by itself (the week’s top tipper, a leaderboard)',
138
+ words: 'top-tipper tipper leaderboard top-spender ranking highest-total most-tips biggest-supporter',
139
+ why: 'the program keeps each payment’s receipt and per-NFT numbers, not a ranking of players',
140
+ nearest: [
141
+ 'one voucher grant that a script signs from the public receipts, each rank its own amount (1st 500, 2nd 300…)',
142
+ 'a running total on chain: a soulbound supporter badge that each tip (a recipe with a `pay` line) `raises`, which gates and odds read with `atLeast` (tiers, not a ranking)',
143
+ 'a lottery among the week’s buyers instead of a ranking',
144
+ ],
145
+ },
146
+ {
147
+ what: 'taking anything from a wallet without its owner signing',
148
+ words: 'auto pull charge automatically debit take dues automatically',
149
+ why: 'every move from a player is signed by that player',
150
+ nearest: ['a page where each member pays (a payment per week), and a list of who paid from its receipts'],
151
+ },
152
+ { what: 'mainnet', words: 'mainnet real money production', why: 'the program isn’t audited yet', nearest: ['devnet now; mainnet after the audit'] },
153
+ ];
154
+
155
+ /**
156
+ * Asks that look impossible but are one field or a short combination: { what, words, how }. Shown by xsolana_examples when a
157
+ * person's words match.
158
+ */
159
+ export const HOW = [
160
+ { what: 'an NFT raffle (a seller raffles one NFT; the winner gets it, the seller the ticket money)', words: 'raffle nft raffle sell tickets winner gets nft one-of-one', how: 'a lottery with `"raffle": "<NFT address>"` (or a first place `{ "prize": { "gives": "nft", "nft": "<address>", "held": true } }`): the NFT waits in the lottery’s own stash, which nothing but the draw can empty; the winner pulls it after the draw; `minSold` cancels and refunds a round that sold fewer. A seller runs it as the owner of a project of their own (`app.raffle`)' },
161
+ { what: 'rent an NFT out (a cabin, a club, a hero for a scholar)', words: 'rent rental renter lease borrow use scholarship cabin club hero lend for a week', how: 'a listing for rent: `openListingIx` with `deal: { Rent: { price, lasts, max_periods } }` (`app.listForRent`); a renter pays by the period (`app.rentListing`) and is its user until the end; nobody ends it early; the game reads the renter with one account read (`app.rentedTo(asset)`). On-chain gates don’t read rentals: the game checks them' },
162
+ { what: 'a presale or fair launch: a token split by what each put in', words: 'presale fair launch pro rata proportional raise contribute allocation by amount crowdsale ido', how: 'a contest with one side, a `minPot`, a split of 100% to the treasury, and a token `prize` with `"split": true`: the whole prize is set aside when it is decided, and each backer claims their part in proportion to what they put in; short of `minPot`, everyone is refunded' },
163
+ { what: 'vesting nobody can take back (a team’s, an investor’s unlock, a sponsor’s prize stream)', words: 'vesting vest unlock cliff team allocation investor locked tokens sealed escrow stream streamflow', how: 'a schedule with `"sealed": true`, `"does": "pay"`, `fund`, `start` (the cliff), `every` and `maxRuns`: it pays from a vault of its own that nobody, the owner included, can change, stop or empty; the recipient runs it whenever they like and catches up; its last run pays what is left' },
164
+ { what: 'leave a stake early for a fee, compounding', words: 'early exit unstake penalty exit fee paper hands compound auto-compound', how: 'a farm of tokens: a tier’s `exitFee` (percent left to `exitFeeTo`: a burn, a pool, or "rewards", its own vault, shared by the stakers who stay; leaving early keeps the rewards it was credited), and `app.compound(farm)` when rewards are the staked token (the lock keeps running). A farm of collectibles leaves early by a recipe that lowers its `lockAttribute`. loot has no unbonding wait and no harvest tax' },
165
+ { what: 'different amounts per wallet from a list (points to tokens, monthly vesting by list)', words: 'different amounts points-to-tokens conversion allocation weighted-airdrop pro-rata proportional list amounts monthly', how: 'a grant’s `list` entries `{ "wallet", "amount", "round" }`: each claims its own amount (up to the grant’s); the same wallet under another `round` claims again' },
166
+ { what: 'stake only to qualify (launchpad tiers, stake to enter)', words: 'stake to qualify launchpad tier tiers stake gate no rewards stake-to-enter staked qualify allocation', how: 'a farm of tokens that nothing funds (no `seed`, no share or schedule sends it anything) and a locked tier, and a gate `{ "staked": { "farm": key, "atLeast": n } }` on a box, item, lottery, contest, auction or grant (counted while its lock runs)' },
167
+ { what: 'a referral link: a different referrer paid on each sale', words: 'referral referrer refer affiliate invite link commission streamer creator-pick', how: 'a box’s, shop item’s or lottery’s split share `{ "to": "chosen", "percent": 10 }`: the buyer’s page passes the referrer from the link (`app.buy(key, { listing, chosenRecipient: referrer })`), paid in the same transaction; no server (with none named the share goes to the treasury, so the page makes the choice required). A payment’s split takes a `chosen` share too (`app.pay(key, { chosenRecipient: referrer })`), a wallet only' },
168
+ { what: 'a share to whoever holds the collectible the buyer picks (rent to the plot built on, a stud fee to the stallion’s owner)', words: 'plot land rent landlord stud stallion owner whichever holder per sale', how: 'a split share `{ "to": "chosen:<item key>", "percent": 10 }`: the buyer names a collectible of that item and whoever holds it then is paid, in the same transaction, after any resale (the sender opens their account if it is missing). The buyer must name one a player holds: a purchase naming none, or one burned or held by the project (staked in escrow, held, up for auction), is refused (BadChoice), so the page offers only collectibles players hold. Its part goes to the treasury only when the holder can’t take it by the time an order settles (staked in escrow since, or their account frozen)' },
169
+ { what: 'an all-or-nothing crowdfund with refunds', words: 'crowdfund crowdfunding goal kickstarter all-or-nothing pledge backers refund', how: 'a contest with one side, a `minPot` (the goal) and a split of 100% to whoever it funds: short of the goal when entries close, every backer claims their pledge back; a contest `prize` gives each backer a badge' },
170
+ { what: 'money held until someone approves, refunded if nobody does by a deadline (a bounty paid when the work is accepted, a paid DM held until it is answered, a deposit)', words: 'escrow held until approve approved accepted refund deadline bounty deposit answered released', how: 'a contest with one side, opened for each case, whose split sends the pot to the receiver (`wallet:<address>`, less any fee), decided by the approver (the authority, or a `deciders` key); `resultDue` is the deadline: after it the contest is off by itself and every entry takes its money back (anyone claims it). The receiver is fixed when it opens' },
171
+ { what: 'limits per wallet: one each, or N a day each', words: 'one-per-wallet per-wallet once-a-day daily-limit max-per-wallet one each', how: '`perWallet` (in all) and `perWalletEach` (`{ "count": 1, "every": "1d" }`) on items, boxes, recipes and grants (a grant’s list pays each listed entry once: list a wallet again under another `round` to pay it again)' },
172
+ { what: 'a falling price on its own clock, however many sell (a Dutch drop)', words: 'dutch falling price drop every minute goes down fixed clock', how: 'a listing’s `fall` (`step`, `every`, `floor`) with `"clock": "shared"`' },
173
+ { what: 'promo codes and QR claims without a server', words: 'promo code coupon redeem qr secret code flyer', how: 'a grant with `keys` (one keypair per code, made offline; only public keys go in the spec) and `limits.perKey: 1`: the page derives the key from the typed code and signs for the player’s own wallet. A code used at every visit (one QR at the register) needs a rotating code or a results server' },
174
+ { what: 'renewing or extending a pass', words: 'renew renewal extend membership monthly pass valid until expires', how: 'a time attribute on the pass raised by a recipe (`raises`: `{ "key": "paid_until", "by": "30d", "fromNow": true }` with a `pay` line), read by gates with `"time": "ahead"`. An item’s `life.lasts` is read only by recipes, never by gates' },
175
+ { what: 'a limited number of uses for something a box rolls on (breed a mare 3 times)', words: 'breed limit times uses breeding-count three times', how: 'a recipe that `use`s the NFT (`life.uses`, `cooldown`: the kit writes them into each recipe that takes the item, as raises of a count of uses and of a time of next use written on the NFT) and gives a permit item; the box `rollsOn` the permit (`then: "burn"`). A box never counts `uses` itself' },
176
+ { what: 'rewards by rank or by points from one grant', words: 'rank ranks top-10 first-second-third leaderboard-payout points-to-tokens', how: 'a grant of a fixed amount of tokens paid by vouchers: each voucher names its claim’s amount, up to the grant’s' },
177
+ { what: 'NFT badges or an airdrop for thousands of wallets without rent for each one (POAPs, attendance, cNFTs)', words: 'compressed cnft cnfts bubblegum poap attendance badge badges airdrop thousands cheap mint rent-free', how: 'a prize `{ "gives": "item", "item": key, "compressed": true }` (on a grant, a lottery place, a contest, a mission or a recipe): a compressed NFT of the collectible, minted into the project’s tree (`compressedTree.holds`, 16,384 by default; its rent, about 0.23 SOL, paid once at the setup). The setup’s collection takes them when the spec has one before it goes live; not soulbound, not in a box' },
178
+ { what: 'a no-loss prize draw', words: 'no-loss lossless prize-savings never-lose', how: 'a lottery with `"noLoss": true`: tickets come back after the draw; the pot is the seed plus what is paid into it (`lottery:<key>` in a split, or a schedule that pays it)' },
179
+ { what: 'a pack of cards from one purchase (a booster of five, a guaranteed rare, a hand of different cards)', words: 'pack packs booster five cards ten cards guaranteed rare hand deck several items one open gacha pull multi-pull', how: 'a box with `"slots"`: each slot so many draws among the outcomes it names (all, when it names none), `"distinct": true` for no outcome twice: `[{ "count": 4, "share": 60 }, { "count": 1, "from": [8, 9, 10], "share": 40 }]` is four cards and a rare. Slots that name outcomes each say what they are worth (`share`, 100 together): a draw that can’t be made gives that much back. All its cards come from one purchase and one oracle answer, and the player may open several packs at once: one wallet prompt. Its order holds a collectible’s rent for each card until it settles; the player or a key they named settles it, in parts when the cards don’t fit one transaction (14 Core cards a part).' },
180
+ ];
181
+
182
+ function typeOf(node) {
183
+ if (!node) return 'any';
184
+ if (node.$ref) return node.$ref.split('/').pop();
185
+ if (node.const !== undefined) return JSON.stringify(node.const);
186
+ if (node.enum) return node.enum.map((v) => JSON.stringify(v)).join(' | ');
187
+ if (node.anyOf ?? node.oneOf) return (node.anyOf ?? node.oneOf).map(typeOf).join(' | ');
188
+ if (node.type === 'object' && node.properties) return `{ ${Object.keys(node.properties).map((k) => (node.required?.includes(k) ? k : `${k}?`)).join(', ')} }`;
189
+ if (node.type === 'array') return `list of ${typeOf(node.items)}${node.maxItems ? ` (at most ${node.maxItems})` : ''}`;
190
+ if (node.type === 'integer' || node.type === 'number') return `${node.type}${node.minimum !== undefined || node.maximum !== undefined ? ` ${node.minimum ?? ''}..${node.maximum ?? ''}` : ''}`;
191
+ return node.type ?? 'object';
192
+ }
193
+
194
+ function fieldsOf(def) {
195
+ const required = new Set(def.required ?? []);
196
+ return Object.entries(def.properties ?? {}).map(([name, p]) => ({ name, type: typeOf(p), ...(required.has(name) ? { required: true } : {}), ...(p.default !== undefined ? { default: p.default } : {}), ...(p.description ? { says: p.description } : {}) }));
197
+ }
198
+ const fieldLine = (f) => ` ${f.name}${f.required ? '*' : ''}: ${f.type}${f.default !== undefined ? ` (default ${JSON.stringify(f.default)})` : ''}${f.says ? ` — ${f.says}` : ''}`;
199
+
200
+ /** The parts (one, or all) with their fields from the schema. */
201
+ export function catalog(only) {
202
+ const defs = SCHEMA.$defs;
203
+ return Object.entries(PARTS)
204
+ .filter(([name]) => !only || name === only)
205
+ .map(([name, p]) => {
206
+ const node = SCHEMA.properties[name];
207
+ const def = p.def ? defs[p.def] : node;
208
+ const nested = (only ? p.nested ?? [] : []).map((n) => ({ name: n, says: defs[n].description ?? '', fields: defs[n].type === 'array' ? [{ name: '(list)', type: typeOf(defs[n]) }] : fieldsOf(defs[n]) }));
209
+ return { part: name, type: typeOf(node), says: p.says, ...(p.acts ? { atPlayTime: p.acts } : {}), fields: def?.properties ? fieldsOf(def) : [], ...(nested.length ? { nested } : {}) };
210
+ });
211
+ }
212
+
213
+ /** The guide in words: every part (or one, with its nested shapes), the conventions and what loot cannot do. */
214
+ export function guideText(only) {
215
+ const parts = catalog(only);
216
+ return [
217
+ only ? `The part “${only}” of a loot economy (* = required):` : `A loot economy is one JSON spec (xsolana.json) on the shared loot program ${IDL.metadata.version}, devnet only. Nobody writes a contract: the spec is checked, simulated, and put live by the owner approving in their own wallet. Its parts (* = required field):`,
218
+ ...parts.flatMap((p) => [`\n${p.part} (${p.type}): ${p.says}${p.atPlayTime ? `\n At play time: ${p.atPlayTime}.` : ''}`, ...p.fields.map(fieldLine), ...(p.nested ?? []).flatMap((n) => [` ${n.name}: ${n.says}`, ...n.fields.map((f) => ` ${fieldLine(f)}`)])]),
219
+ ...(only
220
+ ? []
221
+ : [
222
+ '\nWriting it:',
223
+ ...CONVENTIONS.map((c) => `- ${c}`),
224
+ '\nAsks that look impossible but are one field or a short combination:',
225
+ ...HOW.map((h) => `- ${h.what}: ${h.how}.`),
226
+ '\nWhat no part of loot does (before saying so, try combining the parts above: most ideas are several parts together). Say what is missing, then offer the nearest versions, best first, and build the one the person picks; never end on "it can’t":',
227
+ ...CANNOT.map((c) => `- ${c.what} (${c.why}). Nearest: ${c.nearest.join('; or ')}.`),
228
+ `\nFees: ${sol(FEES.player)} SOL once when a player signs for something of their own (a grant's claim too), never again for what follows from it; ${sol(FEES.project)} SOL each time the owner or the authority calls loot, setting up included; ${sol(ORACLE_FEE)} SOL to MagicBlock’s oracle per random roll.`,
229
+ '\nNext: xsolana_examples to find the nearest example, then write the spec and run xsolana_check.',
230
+ ]),
231
+ ].join('\n');
232
+ }
@@ -0,0 +1,15 @@
1
+ // The checker's legal flags in plain words: what the person must hear before an economy goes live. xsolana_publish leads
2
+ // its answer with them, and the approval page shows them in a block of their own, whatever the agent said, and enables
3
+ // Approve only once the owner ticks that they read them. Pure.
4
+
5
+ /** Each legal flag's sentence, for the person who approves, by the checker's warning code. */
6
+ export const LEGAL = Object.freeze({
7
+ 'W-CHANCE-PRIZE': 'Players pay for a chance at money (a box or a lottery that pays a prize, a bet on an outcome). That is gambling in many places: before real money, get a lawyer, and gate it by age and region.',
8
+ 'W-CHANCE-ITEMS': 'Players pay for random items they can resell (a loot box). Some places treat that as gambling and many require the odds shown: show them on the page, and get a lawyer before real money.',
9
+ 'W-AUCTION-SHILL': 'Anyone, the seller included, can bid to push an auction’s price up (shill bidding): a reserve is the honest way to set a floor.',
10
+ 'W-REVENUE-SHARE': 'Stakers are paid from what players spend, which regulators look at when they decide whether a token is a security: before real money, get a lawyer, or pay rewards from emissions instead.',
11
+ });
12
+
13
+ /** The legal flags a checker verdict raises, each once, in the order the checker raised them: [{ code, says }]. */
14
+ export const legalFlags = (verdict) =>
15
+ [...new Set((verdict?.warnings ?? []).map((w) => w.code))].filter((code) => Object.hasOwn(LEGAL, code)).map((code) => ({ code, says: LEGAL[code] }));
@@ -0,0 +1,130 @@
1
+ // Going live: the plan's transactions for a real owner, and what the spec records once they land. Nothing here signs:
2
+ // the approval page (js/approve) has the owner's wallet sign, and the full simulation sends them to a local chain.
3
+ import { encodeBase58 } from '../chain/base58.js';
4
+ import { randomBytes } from '../chain/bytes.js';
5
+ import { LOOT_PROGRAM } from '../client/pda.js';
6
+ import { compressedTree, listOf, model } from './model.mjs';
7
+ import { auctionSeller, plan, setupContext } from './plan.mjs';
8
+
9
+ /** A key for each fresh account the plan creates. Their secret halves are made by whoever signs (the page). */
10
+ export function freshAddresses(names, random = randomBytes) {
11
+ return Object.fromEntries(names.map((n) => [n, encodeBase58(random(32))]));
12
+ }
13
+
14
+ /**
15
+ * The transactions that put `economy` live for `owner`, with `keys` the addresses of the fresh accounts (each signs
16
+ * the transaction that creates it). `live`: what is already live (a change publishes only what differs).
17
+ */
18
+ export function prepare(economy, { owner, keys, live = null, now = Math.floor(Date.now() / 1000), rent, only = null, slot = null }) {
19
+ const m = model(economy, { now });
20
+ // `slot`: a recent slot (in the slot hashes now), which the project's standing lookup table is made at.
21
+ const ctx = { ...setupContext(m, { owner, keys, live: live ?? {}, rent }), slot };
22
+ const p = plan(economy, { now, ctx, live, only });
23
+ if (!p.ok) return p;
24
+ // Going live always makes the project's standing lookup table (the plan's "table" step). Without a recent slot it
25
+ // can't be made: refused when a part's widest transaction needs it (`wideParts`), so no player is left with a
26
+ // transaction only a table carries in one approval.
27
+ const wide = p.steps.some((s) => s.id === 'table') && slot == null ? wideParts(m, p.warnings) : [];
28
+ if (wide.length)
29
+ return {
30
+ ok: false,
31
+ warnings: p.warnings,
32
+ refusals: [{ code: 'E-NO-TABLE', where: wide[0].where, message: `Going live makes the project’s standing lookup table, and no recent slot was read to make it at: ${wide.map((x) => x.why).join('; ')}. Without the table each of those is a wallet’s own table first (more approvals), or can’t be sent at all by a wallet that sends legacy transactions only.`, fix: 'Try again once the cluster answers (the approval page reads a recent slot from its slot hashes).' }],
33
+ };
34
+ return { ...p, ctx };
35
+ }
36
+
37
+ /**
38
+ * The parts of an economy whose widest transaction passes a legacy transaction, so a player sends it in one approval
39
+ * only with the project's standing lookup table: a box's open or settle and a craft the checker sizes past one
40
+ * (W-TX-V0, W-SETTLE-V0), an auction whose bids are NFTs (each moves with its own accounts), and a farm of another
41
+ * collection's NFTs (a pNFT's stake and exit carry its token record and rule set). [{ where, why }]
42
+ */
43
+ export function wideParts(m, warnings = []) {
44
+ const out = warnings.filter((w) => w.code === 'W-TX-V0' || w.code === 'W-SETTLE-V0').map((w) => ({ where: w.where, why: `${w.where} (${w.code === 'W-TX-V0' ? 'its open, buy or craft' : 'its settle'} passes a legacy transaction)` }));
45
+ for (const a of m.auctions) if (a.bidNfts) out.push({ where: `economy.auctions.${a.key}`, why: `the auction “${a.name}” (its bids are NFTs)` });
46
+ for (const f of m.farms) if (f.stakes === 'collectibles' && listOf(f.gate).some((g) => g?.collection)) out.push({ where: `economy.farms.${f.key}`, why: `the farm “${f.name}” (it stakes another collection’s NFTs, a pNFT’s stake and exit pass a legacy transaction)` });
47
+ return out;
48
+ }
49
+
50
+ /**
51
+ * The spec once its setup landed: every part keeps the program number it was given, and `live` names the game, its
52
+ * collection, the treasury, the mints made and the program, with a copy of what is now on chain.
53
+ */
54
+ export function recordLanded(economy, { ctx, signatures = [], at = new Date().toISOString(), now }) {
55
+ const m = model(economy, { now });
56
+ const ids = (list, field = 'id') => list.map((x) => x[field]);
57
+ const withIds = (key, list, field = 'id') => (economy[key] ?? []).map((part, i) => ({ ...part, [field]: ids(list, field)[i] }));
58
+ const out = { ...economy };
59
+ out.items = (economy.items ?? []).map((it, i) => {
60
+ const shop = m.shop.find((b) => b.shopOf === it.key);
61
+ return { ...it, id: m.items[i].id, ...(shop ? { shopBox: shop.id } : {}) };
62
+ });
63
+ out.pools = withIds('pools', m.pools, 'index');
64
+ out.boxes = withIds('boxes', m.boxes);
65
+ for (const k of ['stashes', 'tables', 'recipes', 'schedules', 'lotteries', 'farms', 'grants', 'auctions']) if (economy[k]) out[k] = withIds(k, m[k]);
66
+ // An auction's address names its seller: the page finds it by both.
67
+ if (economy.auctions) out.auctions = out.auctions.map((a) => ({ ...a, seller: a.seller ?? auctionSeller(a, ctx) }));
68
+ for (const k of ['items', 'pools', 'boxes']) if (!economy[k]) delete out[k];
69
+ const mints = { ...(economy.live?.mints ?? {}), ...Object.fromEntries([...m.tokens.values()].filter((t) => t.source === 'game').map((t) => [t.key, ctx.mints[t.key]])) };
70
+ const { live: _, previous: __, ...published } = out;
71
+ out.live = {
72
+ program: LOOT_PROGRAM,
73
+ project: ctx.project,
74
+ // A project made with no collection (`"collection": false`) has none to write: the schema takes an address only.
75
+ ...(ctx.collection ? { collection: ctx.collection } : {}),
76
+ ...(ctx.tree ? { tree: ctx.tree } : {}),
77
+ ...(ctx.tables?.length ? { tables: ctx.tables } : {}),
78
+ treasury: ctx.treasury,
79
+ authority: ctx.authority,
80
+ ...(Object.keys(mints).length ? { mints } : {}),
81
+ at,
82
+ published,
83
+ transactions: [...(economy.live?.transactions ?? []), ...signatures.map((signature) => ({ signature }))].slice(-60),
84
+ };
85
+ return out;
86
+ }
87
+
88
+ const PART_LISTS = { items: 'item', pools: 'pool', stashes: 'stash', boxes: 'box', recipes: 'recipe', schedules: 'schedule', lotteries: 'lottery', farms: 'farm', grants: 'grant', auctions: 'auction' };
89
+ /** Parts that never change once live: a farm and an auction are never edited, a pool's token is fixed. */
90
+ const FIXED = new Set(['farm', 'pool', 'auction']);
91
+
92
+ /**
93
+ * What a change to a live economy publishes: the parts that are new or differ from what is on chain (`live.published`),
94
+ * as the part ids setupSteps takes (`only`), and in words what cannot change. `settings`: the project's own settings
95
+ * the spec changes (its authority, treasury, collections), which a publish never writes.
96
+ */
97
+ export function changedParts(economy) {
98
+ const published = economy.live?.published;
99
+ if (!published) return { only: null, fixed: [], removed: [], settings: [] };
100
+ const only = new Set();
101
+ const fixed = [];
102
+ const removed = [];
103
+ const strip = ({ id, index, shopBox, seller, ...rest }) => JSON.stringify(rest);
104
+ for (const [list, kind] of Object.entries(PART_LISTS)) {
105
+ const before = new Map((published[list] ?? []).map((p) => [p.key, p]));
106
+ for (const part of economy[list] ?? []) {
107
+ const was = before.get(part.key);
108
+ if (!was) {
109
+ only.add(`${kind}:${part.key}`);
110
+ if (kind === 'pool') only.add(`seed:${part.key}`);
111
+ if (kind === 'grant') only.add(`fund:${part.key}`);
112
+ } else if (strip(was) !== strip(part)) {
113
+ if (FIXED.has(kind)) fixed.push(`${kind}:${part.key}`);
114
+ else only.add(`${kind}:${part.key}`);
115
+ }
116
+ before.delete(part.key);
117
+ }
118
+ for (const key of before.keys()) removed.push(`${kind}:${key}`);
119
+ }
120
+ for (const t of economy.tokens ?? []) if (!(published.tokens ?? []).some((x) => x.key === t.key)) only.add(`token:${t.key}`);
121
+ // The first compressed prize opens the project's tree.
122
+ if (!economy.live.tree && compressedTree(model(economy))) only.add('tree');
123
+ // A traits root set again replaces the last.
124
+ for (const t of economy.traits ?? []) if (!(published.traits ?? []).some((x) => x.collection === t.collection && x.root === t.root)) only.add(`traits:${t.collection}`);
125
+ const settings = [];
126
+ if ((published.keys?.authority ?? 'owner') !== (economy.keys?.authority ?? 'owner')) settings.push('its authority');
127
+ if ((published.treasury ?? null) !== (economy.treasury ?? null)) settings.push('its treasury');
128
+ if (JSON.stringify(published.collections ?? []) !== JSON.stringify(economy.collections ?? [])) settings.push('the collections it takes');
129
+ return { only, fixed, removed, settings };
130
+ }