@xgamesfun/solana 0.20.1 → 0.25.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 (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
@@ -6,34 +6,41 @@ winner nobody pays.
6
6
 
7
7
  ## Who sends each step
8
8
 
9
- | Step | Who can send it | If nobody does |
10
- | --- | --- | --- |
11
- | A box's roll and settle, a shop buy | the player, in their own purchase (`js/play` does both in one prompt) | anyone can settle a rolled order, a normal item too (the app's backend delivers it from the `OrderSettled` receipt of a finalized transaction whose `meta.err` is null, once per order: `loot.receipts(tx)` gives none for a transaction with an error, whose logs can still read as all-success; it reads the transaction again when its log is truncated); a roll that failed or never came refunds (`app.refund`) |
12
- | A lottery draw, completing its contributions, paying its places | **anyone**: the page (`app.claims()` lists each available step), or a sweep. Paying a winner with no prize account opens it in the same transaction (the sender pays its rent) | selling stops at draw time. A result must be followed by completion of its contributions and payment of every place before the next round. A round nobody draws waits, with no deadline. If the oracle never answers a draw (7 days after it was asked for), anyone can cancel and refund each recorded buyer. An empty round moves on with the next ticket. A one-time lottery ends after its round |
13
- | A schedule that is due (an emission, rent, a drip) | **anyone**: the page (`app.runSchedule`), a player, a sweep | it waits, and catches up one run at a time |
14
- | Ending an auction | **anyone**: usually the winner, from the page | it waits: whenever it is ended, the lot goes to the highest bidder |
15
- | A contest's entries back when it is off, or its winners' claims | **anyone** (each entry's money goes only to its player) | they wait; nothing is lost |
16
- | Opening a contest | the **authority** (the owner's wallet by default) | nothing happens |
17
- | Naming its winner, calling it off | the **authority**, or **anyone** sending a verdict one of its `deciders` signed (a results server's key: `decideIx` with `verdict`, after the Ed25519 check of `verdictBytes`) | after `resultDue` it is off by the clock and everyone claims back; a crowdfund short of its `minPot` when entries close is off at once |
18
- | Moving tokens out of a pool, minting, a `decisions` payout, rewriting a collectible | the **authority** | nothing happens |
19
- | A grant claim | the **winner** only, from the page: the winning wallet signs every claim, whatever proves it (a voucher, the list, an open grant); anyone may pay its fees, nobody claims for it | the prize waits for them |
20
- | A prize given directly (a grant's, a lottery place's, a contest's, a mission's, a recipe's: an item, tokens, an NFT of a stash) | the winner's own claim (a grant's: the winner signs it), the place's payment, the winning entry's claim, the unstake or the craft: it is given in that transaction, the sender paying what it opens (a collectible's rent, the winner's token account) | a grant's claim or a craft of a prize it can't give is refused; a lottery's place, a contest's claim or a mission's end goes on without it (an NFT already given, an item sold out or switched off, a pool short pays what it holds) |
21
- | A prize that is a roll of a box (a grant's, a lottery's place, a contest's winners, an auction's mystery lot, a mission's chest) | the winner's own claim (a grant's), the place's payment or the auction's end makes the winner's order (its rent goes back to the winner at settle); then **anyone** asks the oracle for the roll (`loot.askRollIx`, its fee the asker's) and **anyone** settles it (`loot.settleIx`), which hands over what it rolled, a stash's NFT included; `app.claims()` lists them | it waits, and is never refunded while it waits; asked and never answered, anyone asks again. What the box gives that has run out (an empty stash, a sold-out item) can't be won: the roll lands on its other outcomes, and a fixed prize its pool runs short of pays what the pool holds |
22
- | Putting more NFTs in a stash, or taking one out | the **owner** (or the authority), holding them: add them to the stash's `nfts` and publish again (only the new ones go in), or `loot.stashIx` with `out` to any wallet | a stash nobody refills just runs out |
9
+ | Step | Who can send it | If nobody does |
10
+ | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | A box's roll and settle, a shop buy | the player, in their own purchase (`js/play` does both in one prompt) | the player, a key the player named for the order (`settler`) or the project's owner or authority settle a rolled order, a normal item too (the app's backend delivers it from the `OrderSettled` receipt of a finalized transaction whose `meta.err` is null, once per order: `loot.receipts(tx)` gives none for a transaction with an error, whose logs can still read as all-success; a truncated log stays truncated when read again, so it then goes by the order's account); a roll that failed or never came refunds (`app.refund`) |
12
+ | A lottery draw, then each part pulled | **anyone**: the page (`app.claims()` lists each available step), or a sweep. The draw's ask fixes the round's money (what its places take of the pot, or of the round's own ticket money for a lottery with `rest`; each pool's prize) and opens the next round at once; paid from a pot, it first moves in what the round before left that pot, so the next round always sees it, whoever pulls and whenever. A pull pays one part into its owner's account, opened in the same transaction when it is not there (the sender pays its rent): an entry's own (a refund, a ticket back), its places' part of the pot or their prizes (a raffled NFT, an item, tokens, a roll), or a part owed in common (a share, what the places leave: to the pot, or to `rest`; a burned `rest` stays for the close to burn) | selling stops at draw time. A round nobody draws waits, with no deadline. Once drawn, every part waits in the round for its own pull, in any order, holding up nobody else; the pull that leaves nothing owed closes the round, and the rent of its record goes back to whoever fronted it (the lottery's `roundRent`, or its first buyer). An NFT that can't move is made a hold for its winner. If the oracle never answers a draw (7 days after it was first asked for), the next ask cancels it, and every buyer's refund is pulled; the page pulls the sender's own refund in the cancel's own transaction. A due round's draw rides in front of the next ticket purchase. An empty round moves on with the next ticket. A one-time lottery ends after its round |
13
+ | A schedule that is due (an emission, rent, a drip) | the project's owner or authority, or who it pays (a wallet, the treasury): `app.runSchedule` signed by one of them, or their sweep (a sealed burn: anyone) | **a run missed is missed**: one sent late pays once, for the latest run due, and the runs before it lapse, so keep a key of the project's sending it on time. Only a sealed schedule's fixed amount pays every run due (a backlog of years takes a few calls) |
14
+ | Ending an auction | **anyone**: usually the winner, from the page | it waits: whenever it is ended, the lot goes to the highest bidder. A seller whose account can't take the money then doesn't hold up the winner: the lot goes, the auction stays open owing it (`app.auctions()`: `delivered`, `proceedsOwed`), and the next end, anyone's, pays and closes it |
15
+ | Catching a farm up (`walk_farm`), when it has passed more of its seats' end times than a move can walk | **anyone**, for free: `app.claims()` lists it, `app.walkFarm(key)` sends it in parts (`app.farmWalk(key)` says how many) | moves of that farm fail until it is walked; nothing is lost by the wait: what each seat earned by its end is exact |
16
+ | A contest's entries back when it is off, or a winner's part of a contest with no prize or picks | **anyone** (each entry's money goes only to its player); a winner's first claim with a prize or picks: the **winner or the project's keys**, and past 30 days the project's keys may pick for them | they wait; nothing is lost. Each winner's part is written at the decision (the pot then, and a split prize's vault), so the order of claims changes nothing; tokens sent to the pot after it go to nobody (burned at the close), and a split prize's part a winner gives up goes back to its pool, not to later winners |
17
+ | Opening a contest | the **authority** (the owner's wallet by default) | nothing happens |
18
+ | Naming its winner, calling it off | the **authority**, or **anyone** sending a verdict one of its `deciders` signed (a results server's key: `decideIx` with `verdict`, after the Ed25519 check of `verdictBytes`); a winner only once entries close, a call-off any time | after `resultDue` it is off by the clock and everyone claims back; a crowdfund short of its `minPot` when entries close is off at once |
19
+ | Moving tokens out of a pool, minting, a `decisions` payout, rewriting a collectible | the **authority** | nothing happens |
20
+ | A grant claim | the **winner** only, from the page: the winning wallet signs every claim, whatever proves it (a voucher, the list, an open grant); anyone may pay its fees, nobody claims for it | the prize waits for them |
21
+ | A prize given directly (a grant's, a lottery place's, a contest's, a mission's, a recipe's: an item, tokens, an NFT of a stash) | the winner's own claim (a grant's: the winner signs it), the place's payment, the winning entry's claim, the unstake or the craft: it is given in that transaction, the sender paying what it opens (a collectible's rent, the winner's token account) | a grant's claim or a craft of a prize it can't give is refused; a lottery's place, a contest's claim or a mission's end goes on without it (an NFT already given, an item sold out or switched off, a pool short pays what it holds) |
22
+ | A prize that is a roll of a box (a grant's, a lottery's place, a contest's winners, an auction's mystery lot, a mission's chest) | the winner's own claim (a grant's), the place's payment or the auction's end makes the winner's order (its rent goes back to the winner at settle); then the **winner or the project's keys** ask the oracle for the roll (`loot.askRollIx`, its fee the asker's) and the winner, a key the winner named, or the project's keys settle it (`loot.settleIx`), which hands over what it rolled, a stash's NFT included; `app.claims()` lists them | it waits, and is never refunded while it waits; asked and never answered, they ask again. What the box gives that has run out (an empty stash, a sold-out item) can't be won: the roll lands on its other outcomes, and a fixed prize its pool runs short of pays what the pool holds. With nothing at all in stock, only the winner asks, and that ends the roll with nothing (an auction's mystery lot: the winning bid refunded); a roll whose answer finds nothing at all waits again, for a restock |
23
+ | Putting more NFTs in a stash, or taking one out | the **owner** (or the authority), holding them: add them to the stash's `nfts` and publish again (only the new ones go in), or `loot.stashIx` with `out` to any wallet | a stash nobody refills just runs out |
23
24
 
24
25
  "Anyone" means the page can offer a button for it, and any player can press it. Build those buttons with `js/play`:
25
- `app.claims()` lists what this player can finish now (a draw, its completion or cancellation, a payout, a refund, an
26
+ `app.claims()` lists what this player can finish now (a draw, its cancellation, a pull, a payout, a refund, an
26
27
  auction to end), and `app.claim(c)` sends one. A **sweep** is a server that sends overdue public steps for the
27
28
  projects it watches. One may or may not be running for the person's project, so never rely on it: the page's buttons
28
- are what finish them. A sweep never needs anyone's key but its own fee payer.
29
-
30
- A lottery's ticket payments stay separate from the prize pot until the draw succeeds: cuts, referrals and burns run
31
- after it. The buyer's share recipients are fixed when they buy; completing a successful contribution sends its shares
32
- and its prize funding. Cancelling instead (only for a draw the oracle never answered) returns its entire ticket
33
- payment and record rent to the recorded buyer, with no ticket split or burn; network and oracle fees stay spent. Prizes and
34
- outside donations already in the prize pot stay there for the lottery; they are not ticket refunds. Each purchase is
35
- a record of its own whose rent goes back to its buyer once it is completed (a winning one once its places are paid),
36
- so a round has no limit on buyers.
29
+ are what finish them. A sweep needs only its own fee payer for draws, refunds and returns; to settle or run a schedule, it needs a project key or a key the player named.
30
+
31
+ A round of a lottery is a record of its own, opened by its first purchase (the SOL the lottery holds for it,
32
+ `roundRent`, put in at setup by default and sized from the lottery's expected rounds and entries, pays its header, its
33
+ token accounts and each entry's bytes; with too little, its first buyer fronts the header and accounts, about 0.004 SOL
34
+ and up, and each buyer their entry; all back when the round closes, to whoever paid), and it holds its ticket money in
35
+ accounts of its own until each part is pulled: shares in common and burns run after the draw, and the pot holds only
36
+ what nobody is owed. While a round paid from a pot waits for the oracle, the next round's draw waits too: it needs that
37
+ round's answer to know what it left the pot. Each purchase adds an entry: funded by `roundRent`, the buyer pays only the fee and the price; a
38
+ buyer who pays its bytes themselves (about 0.0002 SOL) never gets them back (they go to the treasury when the round
39
+ closes). A share the
40
+ buyer chooses (a referrer) is paid as they buy. Cancelling (only for a draw the oracle never answered, or a round short of its least sale) owes every entry its
41
+ whole ticket payment back, with no split or burn; network and oracle fees stay spent. Prizes and outside donations
42
+ already in the pot stay there for the lottery; they are not ticket refunds. A round holds about 70,000 to 260,000
43
+ purchases, by how wide its entries are.
37
44
 
38
45
  Every account a payment goes to is opened by the transaction that pays it, and whoever sends it pays the rent: the
39
46
  treasury's or a payee's account at a settle, a winner's at a payout, an outbid bidder's at the next bid. Nothing waits
@@ -47,12 +54,14 @@ methods). What the project writes about one (a level, a use) goes in its Sheet (
47
54
  the authority with `collectible` `Set`, by a farm's growth, or by anyone proving the traits the project vouched for
48
55
  (`app.proveTraits`).
49
56
 
50
- A new mission is a new farm: add it to the spec and publish again; `runs` ends it that long after it goes live. Near the end of a farm that shares its pool, fund it at least its release span (its longest lock, a day at least) before the end and refresh any seat you suspect is dead a span before it: what is not released by the end, and what a seat gives back after it, stays in the pool, the project's. If rewards cannot move, unstaking can explicitly forfeit them.
57
+ A new mission is a new farm: add it to the spec and publish again; `runs` ends it that long after it goes live. Near the end of a farm that shares what lands, fund it at least its release span (its longest lock, a day at least) before the end: what is not released by the end, and what a seat forfeits after it, stays in its rewards vault, where the authority may take back what is free (`loot.moveTokensIx` with `from: { Farm: [id] }`), never what the farm owes its seats. No seat needs a refresh at its end. If rewards cannot move, unstaking can explicitly forfeit them.
51
58
 
52
- Changing a live box's roll (its outcomes, odds, `stockOdds`, `rollsOn`, `then`) fails every roll of it still waiting
53
- (`W-LIVE-ROLLS`): a purchase gets its payment back, but a roll won on another route (a grant's, a mission's chest, a
54
- lottery's place, an auction's lot) paid nothing in and refunds 0. Publish such a change once none waits, or make a new
55
- box. Its price, rise, fall, split, switch, window and gate change freely. A set's way out (the recipe that unbinds it)
59
+ Changing a live box's roll (its outcomes, odds, `stockOdds`, `whenOut`, `rollsOn`, `then`) fails a purchase still waiting for its
60
+ roll, and an auction's roll lot, each of which gets its payment back at once (`W-LIVE-ROLLS`). Any other roll waiting (a
61
+ grant's, a mission's chest, a lottery's place, a recipe's) holds no payment and rolls on the box as edited: nothing spent is
62
+ lost. A roll asked and left unanswered 7 days can be refunded when it holds a payment or an NFT (its refund names the
63
+ box, whose count of draws waiting it leaves: `app.refund` does). A box that stops selling when an outcome runs out (the
64
+ default) sells again once the project restocks it (more units, tokens into its pool, NFTs into its stash) or edits it. Its price, rise, fall, split, switch, window and gate change freely. A set's way out (the recipe that unbinds it)
56
65
  should take no `pay` line (`W-UNBIND-PAY`): a recipe with one waits while the project is paused or the platform
57
66
  suspends it, and the pieces stay locked in their farm meanwhile.
58
67
 
@@ -77,12 +86,21 @@ the winner claims themselves. The grant's limits (per wallet in all and per wind
77
86
  total, between two dates) are the most anything can ever pay out, whatever happens to a server.
78
87
 
79
88
  - **A list** (an airdrop, a known set of winners): the owner writes the wallets in the grant's `list`; no server.
80
- - **Promo codes, claim links**: the grant's `keys` are the public keys of one keypair per code (made offline from each
81
- code; only public keys go in the spec), with `limits.perKey: 1`. The player types the code; the page derives its
82
- keypair, signs a voucher for the player's own wallet, and claims (`app.claimGrant(key, { voucher })`). A code pays
83
- once in all; a signature someone copies pays only the wallet it names.
89
+ - **Promo codes, claim links**: make the codes with `makeCodes(n)` (`@xgamesfun/solana/results`: 80 bits each, never
90
+ fewer than 64, since each code's key is public in the spec and a short code can be found by trying them all
91
+ offline) and put `await codeAddresses(codes)` in the grant's `keys`, with `limits.perKey: 1`. Keep the codes out of
92
+ the repository. The player types a code; the page makes `codeVoucher({ code, game, grant, incarnation, wallet,
93
+ now })` for the player's own wallet and claims (`app.claimGrant(key, { voucher })`). A code pays once in all; a
94
+ signature someone copies pays only the wallet it names.
84
95
  - **Show a badge, claim once**: an open grant (no `vouchers`, `keys` or `list`) with a `gate` and `limits.perWallet`:
85
96
  any wallet that shows the collectible claims, and each collectible shown counts too. No server.
97
+ - **A wallet is not a person.** Wallets are free, so a per-wallet limit never limits a person, and a game runs in the
98
+ player's browser, so its own claims prove nothing. For a reward worth farming: the server decides or checks the
99
+ result; it pays only a wallet the player proved (`proofRequest` / `verifyProof` from `@xgamesfun/solana/results`: a
100
+ text the wallet signs, with the game, a nonce the server takes once, and an expiry); it counts people where it can
101
+ (`perPerson`, with a person hash the voucher names: that needs an outside sign-in such as X, and a wallet-only
102
+ player has none); it checks what a wallet holds on the chain, never the page's word (`collectiblesOf`, `holds`); and
103
+ the grant's `perDay` and `total` bound everything whatever happens to a key. Keep token rewards for results small.
86
104
  - **Vouchers from a results server**: the grant names the server's public key in `vouchers`; the server checks the
87
105
  game's result and signs a voucher (for a grant of tokens, with each winner's own `amount`, up to the grant's: rewards
88
106
  by rank); the winner's page claims with it (`app.claimGrant(key, { voucher })`).
@@ -101,11 +119,13 @@ total, between two dates) are the most anything can ever pay out, whatever happe
101
119
  - **A random draw nobody can rig** (a free draw for members): a results server's computed draws (drand), or a lottery.
102
120
 
103
121
  Per wallet per day ("2 wins a day each") uses `limits.perWalletEach: { "count": 2, "every": "1d" }`;
104
- `perDay` separately counts all wallets together. A wallet can claim at most 256 distinct signed voucher rounds
105
- within one grant incarnation, in any order. Plan a new campaign when that bound would be exceeded; changing
106
- limits or voucher keys does not clear its history. Closing and recreating a grant starts a new campaign with new
107
- receipts and invalidates old vouchers. A list written in xsolana.json pays each wallet once per grant incarnation: editing it lets newly added wallets
108
- claim, never a wallet already paid (for repeated payouts, list the wallet again under a new `round`, or use vouchers). A results server must read the current grant incarnation when accepting
122
+ `perDay` separately counts all wallets together. A grant keeps every round it paid each wallet within one incarnation,
123
+ 16 bytes a round (from the grant's funds), and each wallet's count in 28: one voucher may name several rounds, each
124
+ paid once, and the same voucher sent again later pays what a limit held back. Changing limits or voucher keys does not
125
+ clear its history. Closing and recreating a grant starts a new campaign with new records and invalidates old vouchers;
126
+ anyone may then close the old records (`loot.closeShardIx`), their rent to the owner. A grant for more than about
127
+ 100,000 wallet-rounds names `wallets` (and `rounds`) so its records are kept in enough lists. A list written in xsolana.json pays each wallet once per grant incarnation: editing it lets newly added wallets
128
+ claim, never a wallet already paid (for repeated payouts, list the wallet again under a new `round`, or use vouchers). A list leaf names no grant or incarnation, so a grant closed and made again with the same list pays every listed round again: give a re-made grant a fresh list (new rounds); the approval page warns when a change does that. A results server must read the current grant incarnation when accepting
109
129
  a result and include it in the voucher it signs.
110
130
 
111
131
  ## Before you act on what the person says happened
@@ -12,82 +12,117 @@ npm install @xgamesfun/solana
12
12
  ```js
13
13
  import { solanaApp, walletStandardSigner } from '@xgamesfun/solana/play';
14
14
 
15
- const spec = await (await fetch('./xsolana.json')).json(); // the live spec, served with the page
15
+ const spec = await (await fetch('./xsolana.json')).json(); // the live spec, served with the page
16
16
 
17
17
  // Connect a wallet (Phantom, Solflare, Backpack all register through the Wallet Standard).
18
18
  const wallets = [];
19
- const api = { register: (...w) => { wallets.push(...w); return () => {}; } };
19
+ const api = {
20
+ register: (...w) => {
21
+ wallets.push(...w);
22
+ return () => {};
23
+ },
24
+ };
20
25
  window.addEventListener('wallet-standard:register-wallet', ({ detail }) => detail(api));
21
26
  window.dispatchEvent(Object.assign(new Event('wallet-standard:app-ready'), { detail: api }));
22
- const wallet = wallets.find((w) => w.chains.some((c) => c.startsWith('solana:')));
23
- const { accounts: [account] } = await wallet.features['standard:connect'].connect();
27
+ const wallet = wallets.find(w => w.chains.some(c => c.startsWith('solana:')));
28
+ const {
29
+ accounts: [account],
30
+ } = await wallet.features['standard:connect'].connect();
24
31
 
25
32
  const app = solanaApp({ spec, rpcUrl: 'https://api.devnet.solana.com', wallet: walletStandardSigner(wallet, account) });
26
33
 
27
- const shop = await app.shop(); // [{ key, name, price, priceUnits, currency, active, left }]
28
- await app.buy('badge', { listing: shop.find((l) => l.key === 'badge') }); // the price shown is the most it pays
29
- const boxes = await app.boxes(); // each box's price and outcome chances
30
- await app.openBox('chest', { listing: boxes.find((l) => l.key === 'chest') }); // pays, waits for the roll, settles
31
- await app.pay('entry'); // a payment of the spec
34
+ const shop = await app.shop(); // [{ key, name, price, priceUnits, currency, active, left }]
35
+ await app.buy('badge', { listing: shop.find(l => l.key === 'badge') }); // the price shown is the most it pays
36
+ const boxes = await app.boxes(); // each box's price, outcome chances and whenOut (show it: stops selling, or draws among what is left)
37
+ await app.openBox('chest', { listing: boxes.find(l => l.key === 'chest') }); // pays, waits for the roll, settles
38
+ await app.pay('entry'); // a payment of the spec
32
39
  const lotteries = await app.lotteries(); // [{ key, name, price, currency, status, round, sold, drawsAt, nftTicket, … }]
33
- await app.buyTickets('weekly', 2, { listing: lotteries.find((l) => l.key === 'weekly') }); // an NFT ticket: { nft: asset, listing }
40
+ await app.buyTickets('weekly', 2, { listing: lotteries.find(l => l.key === 'weekly') }); // an NFT ticket: { nft: asset, listing }
34
41
  await app.payNft(asset, { to, reference }); // pay with an NFT (burned, or given to `to`): admit the player by the receipt
35
- await app.stake('farm', '10', 0); // tokens, at lock tier 0; app.stakeNft('farm', asset) for a collectible
36
- await app.harvest('farm'); // rewards; app.unstake('farm', '10') once the lock has run
37
- await app.staked('farm'); // the player's seats: [{ asset, amount, freeAt, boostPercent, waiting, rewards }]
38
- await app.refresh('guild', asset); // a farm with growth: writes the NFT's points and level now (anyone may)
39
- const recipes = await app.recipes(); // [{ key, name, active, version }]
40
- await app.craft('merge', [assetA, assetB], 1, { listing: recipes.find((r) => r.key === 'merge') }); // a changed recipe is refused
41
- await app.auctions(); // each auction: high, nextBid, leading, secondsLeft, open, over, waiting (SOL)
42
- await app.bid('comet', '0.6'); // the bid it tops goes back to its bidder in the same transaction
43
- await app.endAuction('comet'); // once it is over, anyone: the lot to the winner { winner, asset? }
44
- await app.unwrap(); // SOL back from a bid that was topped, from wrapped SOL to plain SOL
45
- await app.claimGrant('welcome'); // a wallet on the grant's list (or { voucher } from your server)
46
- await app.claimWins({ server, token }); // every prize a results server vouched for or listed, claimed by the winner; [{ round, signature | error }]
47
- // (`token`: the player's sign-in with that server, when it asks for one: a stash drop's NFT is given only at a signed-in claim)
42
+ await app.stake('farm', '10', 0); // tokens, at lock tier 0; app.stakeNft('farm', asset) for a collectible
43
+ await app.harvest('farm'); // rewards; app.unstake('farm', '10') once the lock has run
44
+ await app.staked('farm'); // the player's seats: [{ asset, amount, freeAt, boostPercent, waiting, rewards }]
45
+ await app.refresh('guild', asset); // a farm with growth: writes the NFT's points and level now (anyone may)
46
+ const recipes = await app.recipes(); // [{ key, name, active, version }]
47
+ await app.craft('merge', [assetA, assetB], 1, { listing: recipes.find(r => r.key === 'merge') }); // a changed recipe is refused
48
+ await app.auctions(); // each auction: high, nextBid, leading, secondsLeft, open, over, waiting (SOL)
49
+ await app.bid('comet', '0.6'); // the bid it tops goes back to its bidder in the same transaction
50
+ await app.endAuction('comet'); // once it is over, anyone: the lot to the winner { winner, asset?, rolled? }
51
+ await app.unwrap(); // SOL back from a bid that was topped, from wrapped SOL to plain SOL
52
+ await app.claimGrant('welcome'); // a wallet on the grant's list (or { voucher } from your server)
53
+ await app.claimWins({ server, token }); // every prize a results server vouched for or listed, claimed by the winner; [{ round, signature | error }]
54
+ // (`token`: the player's sign-in with that server, when it asks for one: a stash drop's NFT is given only at a signed-in claim)
48
55
  const loan = await app.loanOffer(lender, 1); // a lender's offer: { amount, repay, lasts, risks }: show every risk's `says`
49
56
  await app.borrow(lender, 1, asset, { acceptRisks: true }); // only once the player saw the risks (refused without it)
50
- for (const c of await app.claims()) await app.claim(c); // what this player can finish: a roll to collect, a refund,
51
- // a contest payout, a due draw, an auction to end
52
- await app.runSchedule('drip'); // a schedule that is due (anyone may run it); null when not yet
53
- await app.collectibles(); // what the player owns: [{ asset, item, name, uri, attributes }] (`item`: its key)
54
- await app.balances(); // what they hold of each of the economy's tokens
57
+ for (const c of await app.claims()) await app.claim(c); // what this player can finish: a roll to collect, a refund,
58
+ // a contest payout, a due draw, an auction to end
59
+ await app.runSchedule('drip'); // a schedule that is due (the project's keys or who it pays); null when not yet. A run missed is missed: only a sealed fixed amount pays every run due
60
+ await app.farmWalk('guild'); // { ends, needsWalk, parts }: end times the farm has passed; a move refuses (FarmNeedsWalk) past what it can walk
61
+ await app.walkFarm('guild'); // walks it in parts (anyone, free); claims() lists it when it is needed
62
+ await app.collectibles(); // what the player owns: [{ asset, item, name, uri, attributes }] (`item`: its key)
63
+ await app.balances(); // what they hold of each of the economy's tokens
55
64
  ```
56
65
 
57
66
  - **Serving it**: a plain `index.html` with a `<script type="module">` can't import a bare `@xgamesfun/solana/play` by
58
67
  itself. Either use a bundler (Vite: `npm create vite@latest`, then `npm install @xgamesfun/solana`), or put this import
59
68
  map before the module script and serve the folder (`npx serve .`):
60
69
  ```html
61
- <script type="importmap">{ "imports": {
62
- "@xgamesfun/solana/play": "./node_modules/@xgamesfun/solana/js/play/index.js",
63
- "@xgamesfun/solana/client": "./node_modules/@xgamesfun/solana/js/client/index.js"
64
- } }</script>
70
+ <script type="importmap">
71
+ {
72
+ "imports": {
73
+ "@xgamesfun/solana/play": "./node_modules/@xgamesfun/solana/js/play/index.js",
74
+ "@xgamesfun/solana/client": "./node_modules/@xgamesfun/solana/js/client/index.js"
75
+ }
76
+ }
77
+ </script>
65
78
  ```
66
79
  Tell the person the one command that starts it, and that it serves on devnet.
67
80
  - **What a roll looks like**: a box of several outcomes is rolled by the oracle a few seconds after the open; `openBox`
68
81
  waits for it. Show the player that it is rolling.
82
+ - **A page that closes before the settle**: the SOL its settle key holds is not lost in the same browser (on another device, with nothing stored, it can't be found yet). Build the wallet with
83
+ `walletStandardSigner(wallet, account)`: with a wallet that signs messages, the page asks once a session for a
84
+ signature over a fixed text ("loot: the keys that settle your orders…"; it moves nothing), and every settle key comes
85
+ from it, so a reload makes it again. With one that signs no messages, the key is kept in the page's storage,
86
+ encrypted. Call `app.recover()` when the page opens (after the wallet connects): it settles what rolled, sends back
87
+ what a key holds for an order that is gone, and leaves one still rolling for later. Say so to the player in a line
88
+ ("Finishing an earlier purchase…").
89
+ - **Packs**: `app.openBox(key, { listing, count: 10 })` opens ten packs in one purchase: one wallet prompt, one oracle
90
+ answer, `maxPrice` a pack's. It returns `won` (every card) and `assets`; a pack too big for one transaction is
91
+ settled in parts, each its own transaction (`parts`: their signatures), sent by the throwaway key the purchase funds (`oneSignature`, the default),
92
+ so the player signs once. More packs than one oracle answer draws (or than keep an order's rent and fee within 0.01
93
+ SOL: `loot.opensAnOrder(box)`) are several orders in that one purchase (`orders`). A pack of fixed cards (one outcome, nothing random) is bought in one step: its cards come
94
+ in the purchase, no oracle and no settle (one too big for a transaction is opened instead).
95
+ - **A roll a route makes** (a craft, a bundle bought, a grant claimed, an auction won): the page asks for it in the
96
+ route's own transaction and settles it after with a throwaway key, so the player signs once. When the rolled box has
97
+ nothing in stock, nothing is sent and the call rejects with SoldOut: tell the player to try again later. A compressed card's id is not in `assets` (it is the tree's count when minted): read it
98
+ from `claims()` or the player's assets. Show each card as it lands.
69
99
  - **What an auction looks like**: show the high bid, the least next bid (`nextBid`) and a countdown (`secondsLeft`;
70
100
  a late bid moves the end, so read `auctions()` again after every bid). A player who was outbid has their bid back
71
101
  already, nothing to claim: in SOL it waits in their wrapped-SOL account (`waiting`), so say "You were outbid:
72
102
  0.5 SOL is back in your wallet" and offer `unwrap()` to turn it into plain SOL (any later purchase does it too).
73
103
  When `over` is true, show an "End the auction" button to everyone (the winner usually presses it; it mints or
74
- hands over the lot). There is no deadline: however late it is ended, the lot goes to the highest bidder.
104
+ hands over the lot). There is no deadline: however late it is ended, the lot goes to the highest bidder. An auction with
105
+ `delivered` has given its lot, and still owes its seller, whose account couldn't take the money (`proceedsOwed`): keep it
106
+ on the seller's page until an end (`app.endAuction`, anyone's) pays and closes it. Don't treat it as closed at its end.
75
107
  - **NFTs the project has no authority over**: a gate or odds that name a collection find the player's Core NFTs of it
76
108
  themselves; for a Token Metadata NFT pass `shown: { mint, owner }`, for a compressed one `shown: { compressed:
77
- assetId, tree }` after a `loot.proveLeafIx` in the same transaction. `app.stakeNft`, `app.unstakeNft`, `app.refresh`,
109
+ assetId, tree }` after a `loot.proveLeafIx` in the same transaction. `app.stakeNft`, `app.unstakeNft`, `app.refresh`,
78
110
  `app.harvest`, `app.hold` and `app.endAuction` take any NFT: a Core asset, a Token Metadata mint (legacy or pNFT),
79
111
  or a compressed NFT's asset id; the page finds its kind itself, and proves a compressed one first (its RPC must
80
112
  offer DAS: `getAsset`, `getAssetProof`). In a farm that watches, `app.harvest` shows the NFT is still the player's,
81
113
  and `app.stakeNft` of an NFT someone else staked and sold takes their seat over. What the project wrote about one is in
82
114
  its Sheet (`loot.decodeSheet`).
83
115
  - **Off-chain traits** (the spec's `traits`): the project builds the tree with `loot.traitsTree([{ nft, attributes
84
- }])` from its NFTs' metadata and publishes each NFT's `attributes` and `proof` with the page; a player (or the page,
116
+ }])` from its NFTs' metadata and publishes each NFT's `attributes` and `proof` with the page; a player (or the page,
85
117
  before a stake) calls `app.proveTraits(nft, attributes, proof)` once per NFT, and its boosts and gates read them.
86
118
  - **Rolls on an NFT**: `app.openBox(boxKey, { on: asset })` for a box that rolls on one (an enchant). Leaving a
87
- mission that rolls, `app.unstakeNft` returns `{ signature, roll }`; a recipe that rolls, `app.craft` returns `roll`.
88
- `app.collect(roll)` asks the oracle when nobody has yet (`app.askRoll`), waits and settles it: show what it gave
89
- (`won`, each prize the roll set aside, and `order.fate`: the numbers moved and where the NFT went). A page should offer to collect any
90
- roll of the player's still waiting.
119
+ mission that rolls, `app.unstakeNft` returns `{ signature, roll, rolled }`; a recipe that rolls, `app.craft` returns
120
+ `roll` and `rolled`; a grant that gives a roll, `app.claimGrant` returns `{ signature, rolls }`; the winner ending an
121
+ auction of a roll, `app.endAuction` returns `rolled`. Each asks the oracle in the player's one transaction, and a
122
+ throwaway key that transaction funds settles it: one prompt. `rolled` (and each of `rolls`) is what the roll gave
123
+ (`won`, each prize it set aside, and `order.fate`: the numbers moved and where the NFT went). When the step and its
124
+ ask can't go together, the roll waits: `app.collect(roll)` asks for it, waits and settles it, and a page should offer
125
+ to collect any roll of the player's still waiting.
91
126
  - **What a staking page shows**: `app.staked(farmKey)` lists the player's seats in a farm, each with what it is owed
92
127
  now (`waiting`, in `rewards`), the program's own sum from a dry run of the seat (nothing signed or sent), and
93
128
  when its lock is over (`freeAt`). Show that; don't work out rewards from the rate yourself (a sharing farm splits
@@ -95,24 +130,27 @@ await app.balances(); // what they hold of each of the econo
95
130
  or a harvest.
96
131
  - **Getting out of a farm**: after the farm lock ends, `app.unstakeNft(farmKey, asset)` releases the collectible;
97
132
  `app.unstake(farmKey, amount)` returns tokens. If reward transfers are blocked, offer an explicit choice to give
98
- up rewards with `{ forfeit: true }` as the last argument. Remove a token seat's booster before withdrawing all
99
- its tokens. Forfeiting does not bypass the lock or the token's or NFT issuer's own controls: say which custody
133
+ up rewards with `{ forfeit: true }` as the last argument. Remove a token seat's booster before taking out all
134
+ its tokens. An early exit (a tier's `exitFee`) keeps the rewards the seat was credited; what is taken out leaves at
135
+ once (no unbonding wait). Forfeiting does not bypass the lock or the token's or NFT issuer's own controls: say which custody
100
136
  the farm uses and never promise an unconditional exit (`telling-the-person.md`, NFT custody).
101
137
  - **Farm token hooks and Core adapters**: `stake`, `unstake`, `stakeNft`, `unstakeNft`, and `harvest` accept
102
138
  `extra` account metas or `transfers` keyed by mint in their options. Static Token-2022 hooks and required memos
103
139
  resolve automatically. For dynamic hooks, provide every actual `{ source, destination, authority, amount }`
104
- transfer: reward pool to vault, then vault to player when the action pays; token principal too when it moves.
105
- Incoming transfer fees reduce reward credit before calculating the payout. Do not guess amounts from the gross
106
- pool balance. Without that plan the app fails before signing; refresh the plan if balances or fee terms change.
140
+ transfer: the farm's rewards vault to the player when the action pays (crediting moves no tokens); token principal
141
+ too when it moves. Do not guess amounts from the vault's balance. Without that plan the app fails before signing; refresh the plan if balances or fee terms change.
107
142
  Explicit `extra` replaces automatic resolution and must include all hook, memo and Core adapter accounts.
108
143
  For example, `app.unstakeNft('heroes', asset, { transfers: { [rewardsMint]: rewardTransfers } })`, or
109
144
  `app.unstakeNft('heroes', asset, { extra: adapterAndTokenAccounts })`. Harvest takes options third:
110
145
  `app.harvest('heroes', asset, { transfers: { [rewardsMint]: rewardTransfers } })`.
111
146
  A forfeit exit skips reward-mint reads and reward ATA creation; principal transfers still need their own mint.
112
- - **Lottery steps**: `claims()` lists a due draw, the completion of its recorded tickets, each place's prize, and,
113
- for a draw the oracle never answered, the cancel-and-refund (`after-launch.md` says how the money moves). Show each
114
- as a button and read the list again after each one: a drawn round's tickets must be completed before its winners
115
- collect.
147
+ - **Lottery steps**: `claims()` lists a due draw (`reveal`), what this player is owed (`pull`), paying everyone else
148
+ and the shares owed in common (`payout`), and for a draw the oracle never answered, asking again or, after its
149
+ timeout, the cancel-and-refund (`cancel-draw`; `after-launch.md` says how the money moves). A draw that cancels the
150
+ round (that one, or a `reveal` of a round short of its least sale) pulls the sender's own refund in the same
151
+ transaction. A ticket bought when the round's draw is due sends the draw first, in the same prompt, and buys into
152
+ the next round (the buyer pays the oracle's 0.0008 SOL for it). Show each as a button
153
+ and read the list again after each one. The parts are pulled in any order: nobody waits on anyone else.
116
154
  - **Gates**: a gated buy, ticket, stake, entry, bid or claim finds the player's collectible that passes the gate by
117
155
  itself ("You need a Guild Pass to buy this" when there is none). Use `collectibles()` and its `item` key to show
118
156
  what a player holds; never match NFTs by name. A list gate (`{ "list": [...] }`) takes the player's proof: the
@@ -120,8 +158,17 @@ await app.balances(); // what they hold of each of the econo
120
158
  power of two as the plan's root does), or takes it from a server that keeps a long list. A stake gate
121
159
  (`{ "staked": … }`) needs nothing from the page: js/play passes the player's seat and the farm, and says plainly when
122
160
  the stake is too small or its lock is over (show the farm's locked tiers next to the gate).
123
- - **A no-loss lottery** (`noLoss`): completing the draw sends every ticket back to its buyer (anyone may complete
124
- it, so say "your tickets come back after the draw"), and the winners then collect the pot.
161
+ - **A no-loss lottery** (`noLoss`): once the round is drawn every ticket is owed back to its buyer (a pull, which
162
+ anyone may send, so say "your tickets come back after the draw"), and the winners then collect the pot.
163
+ - **A lottery round** finishes in a few steps whatever its size: `app.claims()` lists the draw (`reveal`, whose ask
164
+ fixes the round's money and opens the next round at once), what this player is owed (`pull`: a refund, a place's
165
+ part of the pot, its prize; a raffled NFT; a roll asked for in the same transaction and settled in the same prompt)
166
+ and paying everyone else and the parts owed in common (`payout`); `app.claim(c)` sends one. The last pull closes
167
+ the round.
168
+ - **Repaying a loan** (the player's own, or a friend's): `app.loan(asset, due)` reads it; show its `repay` (base units
169
+ of its `mint`), then `app.repay(asset, due, { repay })` pays exactly that, in the payer's one prompt (a loan whose
170
+ debt changed since is refused); a throwaway key that transaction funds sends the NFT home to its borrower, a pNFT's
171
+ or a compressed NFT's too (one prompt). A lender sees an overdue unpaid loan in `claims()` (`release`, to them).
125
172
  - **A grant that pays by rank**: a voucher with an `amount` (base units of the grant's token) pays that much, up to
126
173
  the grant's; `app.claimGrant(key, { voucher })` passes it, and `{ proof, round, personHash, amount }` for a list.
127
174
  - **The price the player saw is the most they pay**: `app.buy` and `app.openBox` need the listing `shop()` or