@xgamesfun/solana 0.25.0 → 0.25.2

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 (50) hide show
  1. package/DISCLAIMER.md +1 -1
  2. package/NOTICE +4 -4
  3. package/README.md +81 -31
  4. package/bin/xsolana.mjs +19 -5
  5. package/examples/stakers-draw.json +1 -0
  6. package/js/client/index.js +1 -0
  7. package/js/client/kit.js +36 -0
  8. package/js/client/random.js +1 -1
  9. package/js/economy/check.mjs +11 -0
  10. package/js/economy/cost.mjs +1 -1
  11. package/js/economy/explain.mjs +10 -3
  12. package/js/economy/guide.mjs +1 -1
  13. package/js/economy/live.mjs +2 -1
  14. package/js/economy/review.mjs +760 -0
  15. package/js/economy/schema.json +8 -4
  16. package/js/economy/trust.mjs +15 -0
  17. package/js/index.js +1 -0
  18. package/js/mcp/http.mjs +136 -0
  19. package/js/mcp/server.mjs +34 -10
  20. package/js/mcp/tools.mjs +50 -2
  21. package/js/play/index.js +116 -2
  22. package/js/play/sponsor.js +302 -0
  23. package/js/results/index.js +2 -2
  24. package/package.json +35 -8
  25. package/release.json +1 -1
  26. package/skills/xsolana-economy/SKILL.md +32 -13
  27. package/skills/xsolana-economy/references/after-launch.md +2 -1
  28. package/skills/xsolana-economy/references/apps.md +28 -0
  29. package/skills/xsolana-economy/references/sentence-to-spec.md +1 -0
  30. package/skills/xsolana-economy/references/telling-the-person.md +6 -0
  31. package/skills/xsolana-economy/references/trust-and-cheating.md +126 -0
  32. package/types/client/index.d.ts +1 -0
  33. package/types/client/kit.d.ts +49 -0
  34. package/types/economy/cost.d.mts +1 -1
  35. package/types/economy/explain.d.mts +3 -0
  36. package/types/economy/plan.d.mts +14 -14
  37. package/types/economy/review.d.mts +45 -0
  38. package/types/economy/trust.d.mts +8 -0
  39. package/types/index.d.ts +1 -0
  40. package/types/mcp/http.d.mts +30 -0
  41. package/types/mcp/server.d.mts +321 -0
  42. package/types/mcp/tools.d.mts +71 -1
  43. package/types/play/index.d.ts +3 -1
  44. package/types/play/sponsor.d.ts +149 -0
  45. package/types/results/index.d.ts +1 -1
  46. package/examples/holder-den.json +0 -38
  47. package/examples/holders-drop.json +0 -105
  48. package/examples/match-predictions.json +0 -24
  49. package/examples/pack-league.json +0 -108
  50. package/examples/stride-sneakers.json +0 -164
package/DISCLAIMER.md CHANGED
@@ -35,7 +35,7 @@ Read this before you use this package, its tools or the loot program. Using them
35
35
 
36
36
  ## Limitation of liability
37
37
 
38
- To the fullest extent the law allows, games.fun, XGamesFun and the contributors to this package are not liable for any
38
+ To the fullest extent the law allows, xgames.fun, XGamesFun and the contributors to this package are not liable for any
39
39
  loss or damage of any kind, direct or indirect, including lost funds, tokens, NFTs, profits or data, arising from the
40
40
  use of, or inability to use, this package, the loot program, the results server or anything built with them, even if
41
41
  told such loss was possible.
package/NOTICE CHANGED
@@ -1,10 +1,10 @@
1
1
  loot
2
- Copyright 2026 games.fun
2
+ Copyright 2026 xgames.fun
3
3
 
4
- This product includes software developed at games.fun.
4
+ This product includes software developed at xgames.fun.
5
5
 
6
- Parts of the JavaScript client and tools (js/) are adapted from the games.fun kit,
7
- MIT License, Copyright (c) 2026 games.fun.
6
+ Parts of the JavaScript client and tools (js/) are adapted from the xgames.fun kit,
7
+ MIT License, Copyright (c) 2026 xgames.fun.
8
8
 
9
9
  The tests run upstream Solana programs (SPL Token, Token-2022, Associated Token, Memo,
10
10
  Metaplex Core, Token Metadata, Bubblegum, Account Compression, Noop, MagicBlock VRF) as
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
- # solana-kit
1
+ # solana-kit: Solana game and NFT economies without a smart contract
2
2
 
3
- The tools for **loot**, one shared Solana program for game and community economies. This repository is the npm
4
- package `@xgamesfun/solana`:
3
+ Loot boxes and gacha, shops, lotteries and raffles, auctions, NFT staking farms, crafting, quests and capped rewards on
4
+ Solana, written as a JSON spec instead of a new Anchor program. They all run on **loot**, one shared, open Solana
5
+ program, and an AI coding agent (Claude Code, Codex, Cursor or any MCP client) can design, check, simulate and price
6
+ one from a sentence. This repository is the npm package `@xgamesfun/solana`:
5
7
 
6
8
  - a JavaScript client with no Solana SDK (`@xgamesfun/solana/client`), a player module for apps (`/play`) and a results
7
9
  client (`/results`);
8
10
  - the economy spec and its checker, quick simulation, cost and plain-words explainer;
9
- - the `xsolana` CLI and an MCP server with ten `xsolana_*` tools;
11
+ - the `xsolana` CLI and an MCP server with eleven `xsolana_*` tools;
10
12
  - the `xsolana-economy` skill, and one plugin that carries the tools and the skill to Claude Code and Codex;
11
13
  - a local approval page where a project's owner approves going live in their own wallet.
12
14
 
@@ -35,7 +37,10 @@ An economy is a JSON spec, not a new smart contract. Every project shares the on
35
37
  - **Players also trust the token and the collection.** A mint's issuer (a freeze authority, a pause, a transfer hook, a
36
38
  permanent delegate) or a collection's controller can stop or move what loot holds. USDC has a freeze authority.
37
39
  - **The chain can't see a game.** Results a game reports (a win, a score, a clear) are vouched for by a key the project
38
- trusts, a results server or deciders. Checking that a player really did it is the project's job, on its own server.
40
+ trusts, a results server or deciders. Checking that a player really did it is the project's job, on its own server:
41
+ a server that decides the result, replays it, or only bounds what the page reports leaves a cheater very different
42
+ amounts. A grant says which in `trust`, and `xsolana review` says what a cheater gets on every route
43
+ ([trust and cheating](skills/xsolana-economy/references/trust-and-cheating.md)).
39
44
  - **The hosted results server is optional.** `https://play.xgames.fun/xsolana` (`@xgamesfun/solana/results`'s
40
45
  default) signs vouchers for games without a server of their own. It can only pay what a grant allows, and only for a
41
46
  grant whose owner named its key in `vouchers`. It sends nothing on chain, and it is offered as is: it can be down, and
@@ -102,13 +107,24 @@ codex plugin add xsolana@xgamesfun
102
107
  ```
103
108
 
104
109
  **Any agent with MCP** (Cursor, opencode, Cline…): run the server with
105
- `npx -y --package=@xgamesfun/solana@0.25.0 xsolana mcp`. Or let `npx -y @xgamesfun/solana@0.25.0 connect` register it, with
110
+ `npx -y --package=@xgamesfun/solana@0.25.2 xsolana mcp`. Or let `npx -y @xgamesfun/solana@0.25.2 connect` register it, with
106
111
  the skill, for Claude Code and Codex.
107
112
 
113
+ **The skill alone, in any agent that reads skills** (Claude Code, Codex, Copilot, Gemini CLI, Cursor, Windsurf, Cline,
114
+ opencode…): `npx skills add XGamesFun/solana-kit` finds it and installs it for the agents you have. Without the MCP
115
+ server it runs the same tools through `npx`. Each GitHub release also carries `xsolana-economy.zip`, the skill as one
116
+ file for apps that take a skill as an upload (the Claude and ChatGPT desktop apps).
117
+
118
+ **Over HTTP**: `npx -y --package=@xgamesfun/solana@0.25.2 xsolana mcp --http` serves the tools that read nothing but
119
+ their arguments (guide, examples, check, simulate, cost, explain, review, status, client) at
120
+ `http://127.0.0.1:8787/mcp`, MCP's Streamable HTTP transport. `@xgamesfun/solana/mcp/http` exports the same handler
121
+ (`mcpHttp(request) → response`) for a hosted endpoint: it reads no files, and the agent hands the person the spec and the
122
+ `approve` command to go live.
123
+
108
124
  Then ask for an economy, for example _"a fishing game where each cast costs 0.02 SOL, with a rare golden fish and a
109
125
  monthly draw"_.
110
126
 
111
- Going live is always yours: `npx -y @xgamesfun/solana@0.25.0 approve` opens a page on your computer where you connect a
127
+ Going live is always yours: `npx -y @xgamesfun/solana@0.25.2 approve` opens a page on your computer where you connect a
112
128
  devnet wallet and approve each transaction. The agent never signs and never asks for a key.
113
129
 
114
130
  ## Use it from a terminal
@@ -116,12 +132,13 @@ devnet wallet and approve each transaction. The agent never signs and never asks
116
132
  Every tool runs from the CLI too, on a spec file (`xsolana.json` by default):
117
133
 
118
134
  ```bash
119
- npx -y @xgamesfun/solana@0.25.0 examples shop with loot boxes # the nearest worked examples
120
- npx -y @xgamesfun/solana@0.25.0 check xsolana.json # refusals and warnings
121
- npx -y @xgamesfun/solana@0.25.0 simulate xsolana.json # where the money goes, day by day
122
- npx -y @xgamesfun/solana@0.25.0 cost xsolana.json # what going live and each action cost
123
- npx -y @xgamesfun/solana@0.25.0 explain xsolana.json # the economy in plain words
124
- npx -y @xgamesfun/solana@0.25.0 approve xsolana.json # go live, approved in your own wallet
135
+ npx -y @xgamesfun/solana@0.25.2 examples shop with loot boxes # the nearest worked examples
136
+ npx -y @xgamesfun/solana@0.25.2 check xsolana.json # refusals and warnings
137
+ npx -y @xgamesfun/solana@0.25.2 simulate xsolana.json # where the money goes, day by day
138
+ npx -y @xgamesfun/solana@0.25.2 cost xsolana.json # what going live and each action cost
139
+ npx -y @xgamesfun/solana@0.25.2 explain xsolana.json # the economy in plain words
140
+ npx -y @xgamesfun/solana@0.25.2 review xsolana.json # what a cheater gets on each route (exits 1 on a high finding)
141
+ npx -y @xgamesfun/solana@0.25.2 approve xsolana.json # go live, approved in your own wallet
125
142
  ```
126
143
 
127
144
  `xsolana help` lists every command. Every command names the exact version, so `npx` runs that release and never whatever
@@ -138,30 +155,63 @@ import * as loot from '@xgamesfun/solana/client';
138
155
  const ix = loot.openBoxIx({ player, project, box, nonce }); // one builder per instruction, no Solana SDK
139
156
  ```
140
157
 
141
- | Import | What |
142
- | -------------------------------- | ------------------------------------------------------------------------------------ |
143
- | `@xgamesfun/solana` | the economy tools: `check`, `simulate`, `plan`, `cost`, `explainEconomy`, `model` |
144
- | `@xgamesfun/solana/client` | every PDA, one builder per instruction, account and receipt decoders, grant vouchers |
145
- | `@xgamesfun/solana/play` | `solanaApp`: what a player does with a live economy, from a browser app |
146
- | `@xgamesfun/solana/results` | results and vouchers a game's server signs |
147
- | `@xgamesfun/solana/chain/*` | browser-safe Solana primitives: base58, Borsh, transactions, PDAs, tokens, Core |
148
- | `@xgamesfun/solana/idl.json` | the program's IDL, the authority for account and argument layouts |
149
- | `@xgamesfun/solana/release.json` | the program build, IDL and client this release is pinned to |
158
+ Players with no SOL can still play when the project sponsors their fees: `solanaApp({ sponsor })` has a key on the
159
+ project's own server pay each step's fees and rent (never its price), and `@xgamesfun/solana/sponsor` is that server's
160
+ side: `sponsorSigner` dry-runs and checks every transaction before it signs, under the project's own per-player budget.
161
+
162
+ Building on `@solana/kit`? `loot.toKitInstruction(ix)` gives any builder's instruction in Kit's shape
163
+ (`{ programAddress, accounts, data }`), and `loot.fromKitInstruction` turns one back.
164
+
165
+ | Import | What |
166
+ | -------------------------------- | ------------------------------------------------------------------------------------------- |
167
+ | `@xgamesfun/solana` | the economy tools: `check`, `simulate`, `plan`, `cost`, `explainEconomy`, `review`, `model` |
168
+ | `@xgamesfun/solana/client` | every PDA, one builder per instruction, account and receipt decoders, grant vouchers |
169
+ | `@xgamesfun/solana/play` | `solanaApp`: what a player does with a live economy, from a browser app |
170
+ | `@xgamesfun/solana/results` | results and vouchers a game's server signs |
171
+ | `@xgamesfun/solana/chain/*` | browser-safe Solana primitives: base58, Borsh, transactions, PDAs, tokens, Core |
172
+ | `@xgamesfun/solana/idl.json` | the program's IDL, the authority for account and argument layouts |
173
+ | `@xgamesfun/solana/release.json` | the program build, IDL and client this release is pinned to |
150
174
 
151
175
  The client, chain and play modules run in browsers and Node 22 or later, with no bundler required. The package
152
176
  ships TypeScript declarations for every entry point.
153
177
 
178
+ ## Questions people ask
179
+
180
+ **How do I make a loot box, gacha or pack opening on Solana without writing a smart contract?** Describe it as a box
181
+ in a spec (its price, its outcomes and their odds, what each one pays), run `xsolana check` and `xsolana simulate`, then
182
+ approve it in your wallet with `xsolana approve`. Rolls come from MagicBlock's VRF, and a buyer is refunded if the
183
+ oracle never answers. [`examples/`](examples) has whole worked economies.
184
+
185
+ **How do I add staking to an NFT collection, even one I didn't mint?** A staking farm takes any collection, with lock
186
+ tiers, boosts and fixed or shared reward rates. The project needs no authority over the NFTs: see
187
+ [`docs/nfts.md`](docs/nfts.md).
188
+
189
+ **Can I run a lottery, raffle or auction on Solana without deploying a program?** Yes: lotteries (including no-loss
190
+ ones), raffles with a Merkle allowlist, and auctions that refund the outbid at once are all routes in the same spec.
191
+
192
+ **Is there an MCP server or Claude Code plugin for building on Solana?** Yes: the `xsolana` plugin for Claude Code and
193
+ Codex, and `npx -y --package=@xgamesfun/solana@0.25.2 xsolana mcp` for any MCP client. It is listed in the MCP
194
+ Registry as `io.github.XGamesFun/solana-kit`.
195
+
196
+ **How is this different from writing my own Anchor program?** Nothing is written, deployed or upgraded per project:
197
+ every project's rules are checked by the one program, and its tests run every instruction against that program in
198
+ LiteSVM. The trade-off is that an economy can only be what the spec can say ([known limits](docs/program.md#known-limits)).
199
+
200
+ **Does it run on mainnet?** Not yet: devnet only until the program is independently audited.
201
+
154
202
  ## Documentation
155
203
 
156
- | | |
157
- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
158
- | [`docs/program.md`](docs/program.md) | the program: who signs, accounts, every instruction, fees, limits, receipts, costs |
159
- | [`docs/nfts.md`](docs/nfts.md) | NFTs and tokens, with and without the project's authority |
160
- | [`docs/harnesses.md`](docs/harnesses.md) | how the plugin reaches Claude Code and Codex, and the release checklist |
161
- | [`skills/xsolana-economy`](skills/xsolana-economy/SKILL.md) | the skill an agent follows |
162
- | [`examples/`](examples) | whole economies that check and go live in the tests; `ideas.json`, 112 real economies rated |
163
- | [`evals/`](evals/README.md) | fresh-session evals of agents building on loot |
164
- | [`CHANGELOG.md`](CHANGELOG.md), [`SECURITY.md`](SECURITY.md) | versions, the trust model, reporting a vulnerability |
204
+ | | |
205
+ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
206
+ | [`docs/tutorial-loot-box.md`](docs/tutorial-loot-box.md) | a loot box from one sentence to going live, step by step |
207
+ | [`docs/program.md`](docs/program.md) | the program: who signs, accounts, every instruction, fees, limits, receipts, costs |
208
+ | [`docs/nfts.md`](docs/nfts.md) | NFTs and tokens, with and without the project's authority |
209
+ | [`docs/harnesses.md`](docs/harnesses.md) | how the plugin reaches Claude Code and Codex, and the release checklist |
210
+ | [`skills/xsolana-economy`](skills/xsolana-economy/SKILL.md) | the skill an agent follows |
211
+ | [`trust-and-cheating.md`](skills/xsolana-economy/references/trust-and-cheating.md) | what a server decides, replays or bounds; who gets paid; an exploit checklist |
212
+ | [`examples/`](examples) | whole economies that check and go live in the tests; `ideas.json`, 112 real economies rated |
213
+ | [`evals/`](evals/README.md) | fresh-session evals of agents building on loot |
214
+ | [`CHANGELOG.md`](CHANGELOG.md), [`SECURITY.md`](SECURITY.md) | versions, the trust model, reporting a vulnerability |
165
215
 
166
216
  ## Layout
167
217
 
package/bin/xsolana.mjs CHANGED
@@ -3,9 +3,10 @@
3
3
  // the same answer, for an agent (or a person) without MCP.
4
4
  //
5
5
  // xsolana mcp serve the tools over stdio (what a plugin's MCP config runs)
6
+ // xsolana mcp --http [--port n] [--host h] serve the hosted tools over HTTP at /mcp (127.0.0.1:8787 by default)
6
7
  // xsolana guide [part] [--json] what an economy can be made of, and what loot cannot do
7
8
  // xsolana examples [words…] [--json] the nearest worked examples and rated economies
8
- // xsolana check|simulate|cost|explain|publish|status [file] [--json] one tool on a spec file (default xsolana.json)
9
+ // xsolana check|simulate|cost|explain|review|publish|status [file] [--json] one tool on a spec file (default xsolana.json)
9
10
  // xsolana approve [file] [--port n] [--no-open] open the local page where the owner approves going live in their wallet
10
11
  // xsolana connect [--agent claude|codex] [--scope user|project] [--local] [--remove] [--dry-run] register the MCP server and the skill
11
12
  // xsolana tool --list | xsolana tool <name> '<json>' [--args-file f] [--json] any tool by name
@@ -25,31 +26,35 @@ const USAGE = `xsolana: the tools for loot, one shared Solana program for game a
25
26
  xsolana simulate [file] the quick simulation
26
27
  xsolana cost [file] what going live and each action cost
27
28
  xsolana explain [file] the economy in plain words
29
+ xsolana review [file] what a cheater gets on each route, and what to fix before going live
28
30
  xsolana publish [file] what going live does, and how to approve it
29
31
  xsolana approve [file] approve going live in your own wallet (a local page; --port n, --no-open)
30
32
  xsolana status [file] a live economy on chain
31
33
  xsolana connect add the xsolana tools to Claude Code or Codex (--agent claude|codex, --scope user|project, --local, --remove, --dry-run)
32
34
  xsolana mcp serve the tools over MCP (stdio)
35
+ xsolana mcp --http serve the hosted tools over HTTP at http://127.0.0.1:8787/mcp (--port n, --host h)
33
36
  xsolana tool --list every tool; xsolana tool <name> '<json>' runs one (--args-file f)
34
37
  xsolana version this package's xsolana version and program
35
38
 
36
- A tool's command (guide, examples, check … status, tool) takes --json for its whole answer as JSON.`;
39
+ A tool's command (guide, examples, check … status, tool) takes --json for its whole answer as JSON.
40
+ review exits 1 when it finds something high.`;
37
41
 
38
42
  /** A failure the person caused (a bad flag, a missing file): said in one line, without a stack. */
39
43
  class UsageError extends Error {}
40
44
 
41
45
  /** The flags that take a value: `--name value` or `--name=value`. */
42
- const VALUED = new Set(['--args-file', '--agent', '--scope', '--port']);
46
+ const VALUED = new Set(['--args-file', '--agent', '--scope', '--port', '--host']);
43
47
  /** The flags each command takes; any other is refused. */
44
48
  const TOOL_FLAGS = ['--json'];
45
49
  const FLAGS = {
46
- mcp: [],
50
+ mcp: ['--http', '--port', '--host'],
47
51
  guide: TOOL_FLAGS,
48
52
  examples: TOOL_FLAGS,
49
53
  check: TOOL_FLAGS,
50
54
  simulate: TOOL_FLAGS,
51
55
  cost: TOOL_FLAGS,
52
56
  explain: TOOL_FLAGS,
57
+ review: TOOL_FLAGS,
53
58
  publish: TOOL_FLAGS,
54
59
  status: TOOL_FLAGS,
55
60
  approve: ['--port', '--no-open'],
@@ -105,8 +110,10 @@ async function main(argv) {
105
110
  const { runTool } = await import('../js/mcp/server.mjs');
106
111
  const r = await runTool(name, input, { cwd: process.cwd() });
107
112
  const failed = Boolean(r.isError || r.ok === false);
113
+ // A review is a report whatever it finds: printed on stdout, its exit code saying whether a finding is high.
114
+ const report = name === 'xsolana_review' && !r.isError && !r.refusals;
108
115
  if (flags.has('--json')) console.log(JSON.stringify(r, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2));
109
- else (failed ? console.error : console.log)(r.text ?? JSON.stringify(r, null, 2));
116
+ else (failed && !report ? console.error : console.log)(r.text ?? JSON.stringify(r, null, 2));
110
117
  if (failed) process.exitCode = 1;
111
118
  }
112
119
 
@@ -114,6 +121,12 @@ async function main(argv) {
114
121
 
115
122
  switch (command) {
116
123
  case 'mcp': {
124
+ if (flags.has('--http')) {
125
+ const { serveHttp } = await import('../js/mcp/http.mjs');
126
+ await serveHttp({ port: portOf(values.get('--port')) ?? 8787, host: values.get('--host') ?? '127.0.0.1' });
127
+ break;
128
+ }
129
+ if (flags.has('--port') || flags.has('--host')) throw new UsageError('--port and --host go with --http.');
117
130
  const { serveStdio } = await import('../js/mcp/server.mjs');
118
131
  await serveStdio();
119
132
  break;
@@ -128,6 +141,7 @@ async function main(argv) {
128
141
  case 'simulate':
129
142
  case 'cost':
130
143
  case 'explain':
144
+ case 'review':
131
145
  case 'publish':
132
146
  case 'status':
133
147
  await tool(`xsolana_${command}`, fileInput());
@@ -84,6 +84,7 @@
84
84
  "grants": [
85
85
  {
86
86
  "key": "race",
87
+ "trust": "decides",
87
88
  "name": "Weekly race rewards",
88
89
  "prize": {
89
90
  "gives": "tokens",
@@ -7,4 +7,5 @@ export * from './sizes.js';
7
7
  export * from './policy.js';
8
8
  export * from './compressed.js';
9
9
  export * from './traits.js';
10
+ export * from './kit.js';
10
11
  export * as random from './random.js';
@@ -0,0 +1,36 @@
1
+ // Instructions in @solana/kit's shape. The builders here return { programId, keys, data } (the shape of
2
+ // @solana/web3.js 1.x, which js/chain/tx.js compiles); an app built on @solana/kit takes { programAddress, accounts,
3
+ // data } instead, with each account's role as a number. These convert one to the other, with no dependency on Kit.
4
+
5
+ /** @solana/kit's AccountRole: bit 1 is a signer, bit 0 is writable. */
6
+ export const KIT_ACCOUNT_ROLE = Object.freeze({ READONLY: 0, WRITABLE: 1, READONLY_SIGNER: 2, WRITABLE_SIGNER: 3 });
7
+
8
+ /**
9
+ * One instruction from a builder here, as @solana/kit takes it: `appendTransactionMessageInstruction(toKitInstruction(ix), m)`.
10
+ * @param {{ programId: string, keys: { pubkey: string, isSigner: boolean, isWritable: boolean }[], data: Uint8Array }} ix
11
+ * @returns {{ programAddress: string, accounts: { address: string, role: number }[], data: Uint8Array }}
12
+ */
13
+ export function toKitInstruction(ix) {
14
+ return {
15
+ programAddress: ix.programId,
16
+ accounts: ix.keys.map(k => ({ address: k.pubkey, role: (k.isSigner ? 2 : 0) | (k.isWritable ? 1 : 0) })),
17
+ data: ix.data,
18
+ };
19
+ }
20
+
21
+ /**
22
+ * A @solana/kit instruction in the shape this package's transactions take (js/chain/tx.js, js/play).
23
+ * @param {{ programAddress: string, accounts?: { address: string, role: number }[], data?: Uint8Array }} ix
24
+ * @returns {{ programId: string, keys: { pubkey: string, isSigner: boolean, isWritable: boolean }[], data: Uint8Array }}
25
+ */
26
+ export function fromKitInstruction(ix) {
27
+ return {
28
+ programId: ix.programAddress,
29
+ keys: (ix.accounts ?? []).map(a => ({
30
+ pubkey: a.address,
31
+ isSigner: (a.role & 2) !== 0,
32
+ isWritable: (a.role & 1) !== 0,
33
+ })),
34
+ data: ix.data ?? new Uint8Array(),
35
+ };
36
+ }
@@ -1,7 +1,7 @@
1
1
  // The loot program's rolling module (`src/random`), mirrored: every random choice it makes is drawn from a stream of
2
2
  // the oracle's answer, by a path that names what is drawn, with one unbiased sampler. A page shows from an order's
3
3
  // answer what each draw gave (and a fresh unit's range traits) exactly as the program drew them. Held to the
4
- // program's own vectors (`js/test/fixtures/roll-vectors.json`, from gamesfun-loot's `tests/fixtures`).
4
+ // program's own vectors (`js/test/fixtures/roll-vectors.json`, from the program's own tests).
5
5
  import { concat, uintLE } from '../chain/bytes.js';
6
6
  import { sha256 } from '../chain/sha256.js';
7
7
 
@@ -4909,6 +4909,16 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
4909
4909
  ? 'Make each claim cost a sybil something: a gate on holding a collectible the game sells or a locked stake, or vouchers from your server that count people ("perPerson", with an outside sign-in such as X). Keep "perDay" and "total" small either way.'
4910
4910
  : 'Give it a gate (a collectible each claimer must hold), or keep "total" small.',
4911
4911
  );
4912
+ // `trust` says what the server holding a voucher key checks: a grant with no voucher key or code has no such server.
4913
+ if (g.trust && !vouchers.length && !keys.length)
4914
+ refuse(
4915
+ 'E-GRANT-TRUST',
4916
+ `${where}.trust`,
4917
+ `${label(g)} says its server ${g.trust} the result ("trust"), but it is paid ${open ? 'to any wallet that asks (it is open)' : 'to the wallets on its list'}: no server signs for it, so nothing checks what a claimer did.`,
4918
+ open
4919
+ ? 'Remove "trust", or name the server’s public key in "vouchers" so its claims are signed.'
4920
+ : 'Remove "trust": the list is the owner’s own choice, written in the spec.',
4921
+ );
4912
4922
  for (const [i, v] of vouchers.entries())
4913
4923
  if (!isAddress(v))
4914
4924
  refuse(
@@ -5175,6 +5185,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
5175
5185
  'W-VOUCHERS',
5176
5186
  'economy.grants',
5177
5187
  `A voucher key signs who earned a grant: whatever holds it (your server, a play host) decides who gets rewards, within the limits the program keeps. The program does not check that a player really did what the voucher says. The keys: ${voucherKeys.join(', ')}.`,
5188
+ 'Say on each such grant what its server checks before it signs ("trust": "decides", "replays" or "bounds"), run xsolana_review, and tell the person what a cheater gets.',
5178
5189
  );
5179
5190
 
5180
5191
  if (randomItems && !chance)
@@ -108,7 +108,7 @@ export function setupCost(plan, economy) {
108
108
  }
109
109
 
110
110
  /**
111
- * What each action costs whoever signs it while the economy runs: games.fun's fee, the network's, and for anything
111
+ * What each action costs whoever signs it while the economy runs: xgames.fun's fee, the network's, and for anything
112
112
  * that asks MagicBlock's oracle for a random number (a box's open, a lottery's draw, the authority's `roll`) the
113
113
  * oracle's own fee (`oracleSol`, model.mjs ORACLE_FEE).
114
114
  */
@@ -26,6 +26,7 @@ import {
26
26
  whenOutWords,
27
27
  } from './model.mjs';
28
28
  import { farmEnds, playSteps } from './plan.mjs';
29
+ import { TRUST_WORDS } from './trust.mjs';
29
30
  import { inAll, poolFate, round, simulate, unnamedExceptions, unnamedWords } from './simulate.mjs';
30
31
 
31
32
  const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
@@ -75,7 +76,7 @@ const paysWords = (l, treasury) =>
75
76
  ? `its places are paid from its pot (an account of its own, which only its winners are paid from): each place takes its percent of what the pot holds when the draw is asked, with the round’s ticket money, so whatever reaches the pot counts (the seed, a sponsor, shares of other sales, yield) and so does what earlier rounds’ places left; what this round’s places leave stays in the pot, and the next round always starts with it`
76
77
  : `its places are paid from each round’s own ticket money alone, which nothing sent from outside can change (no seed, no gift, no carry-over); what the places leave, and the part of places nobody won, goes ${toWords(destination(l.rest), treasury)}${l.rest === 'burn' ? ' when the round closes' : ''}`;
77
78
 
78
- const ruleWords = r => {
79
+ export const ruleWords = r => {
79
80
  if (!r) return '';
80
81
  if (r.wallets) return `they are one of the ${r.wallets.length} wallets it names`;
81
82
  if (r.items) return `any of ${r.items.map(k => `“${k}”`).join(', ')}`;
@@ -99,7 +100,7 @@ const stakeWords = (staked, m) => {
99
100
  );
100
101
  };
101
102
  /** A gate in a sentence: "Only players who show …", "Only the wallets it names …", "Only wallets with … staked …". */
102
- const gateWords = (gate, verb, m) =>
103
+ export const gateWords = (gate, verb, m) =>
103
104
  !gate
104
105
  ? ''
105
106
  : gate.wallets
@@ -464,8 +465,14 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
464
465
  const who = signers.length ? signers.join(' or ') : 'that asks (it is open to any wallet)';
465
466
  // A grant of a fixed amount of tokens, paid by voucher or list: each claim may name its own amount, up to the grant's.
466
467
  const named = signers.length > 0 && src.prize?.gives === 'tokens';
468
+ // What the voucher's server checks before it signs (`trust`): the spec's own word, never sent on chain.
469
+ const trust = src.trust
470
+ ? ` What its server checks: ${TRUST_WORDS[src.trust]}.`
471
+ : (src.vouchers ?? []).length
472
+ ? ' What its server checks before it signs is not said ("trust"): xsolana_review reads it as the weakest, a server that only bounds what the page reports.'
473
+ : '';
467
474
  actions.push(
468
- `Claim “${g.name}” (only the winning wallet claims, signing it; anyone may pay its fees): gives ${g.gives}, to a wallet ${who}${limits ? `; limits ${limits}` : '; no limits'}${src.gate ? (src.gate.wallets ? `; only the ${src.gate.wallets.length} wallets it names` : src.gate.list || src.gate.staked ? `; ${gateWords(src.gate, 'claim it', m).replace(/^Only/, 'only')}` : `; the wallet must hold ${ruleWords(src.gate)}${!signers.length ? ', and each one of those claims once too' : ''}`) : ''}${named ? `; a voucher or the list can name each claim’s own amount, up to that (a reward by rank or share)` : ''}. Each claim costs the grant’s funds about ${num(g.fundsPerClaimSol)} SOL of rent, and whoever sends it the ${fee} SOL player fee.`,
475
+ `Claim “${g.name}” (only the winning wallet claims, signing it; anyone may pay its fees): gives ${g.gives}, to a wallet ${who}${limits ? `; limits ${limits}` : '; no limits'}${src.gate ? (src.gate.wallets ? `; only the ${src.gate.wallets.length} wallets it names` : src.gate.list || src.gate.staked ? `; ${gateWords(src.gate, 'claim it', m).replace(/^Only/, 'only')}` : `; the wallet must hold ${ruleWords(src.gate)}${!signers.length ? ', and each one of those claims once too' : ''}`) : ''}${named ? `; a voucher or the list can name each claim’s own amount, up to that (a reward by rank or share)` : ''}. Each claim costs the grant’s funds about ${num(g.fundsPerClaimSol)} SOL of rent, and whoever sends it the ${fee} SOL player fee.${trust}`,
469
476
  );
470
477
  }
471
478
  for (const st of sim.stashes ?? []) {
@@ -139,7 +139,7 @@ export const CONVENTIONS = [
139
139
  export const SAFETY = [
140
140
  'A wallet is not a person: wallets are free, so a limit per wallet never limits a person. What bounds a reward is its totals ("perDay", "total": the most it can ever pay), and what makes farming cost something: a gate on holding a collectible the game sells or a locked stake, or vouchers from your server that count people ("perPerson": the voucher signer names a person hash, which needs an outside sign-in such as X; a wallet-only player has none).',
141
141
  'Open grants (no voucher, keys or list) pay any wallet: keep their prizes small or soulbound, gate them, and set "perDay". `xsolana_simulate` says what a crowd of wallets could take next to one.',
142
- 'The page can’t be trusted: a game runs in the player’s browser, so a result worth paying is decided or checked by your server, which signs the voucher. A server checks what a wallet holds with `collectiblesOf` / `holds`, pays only wallets a player proved (`proofRequest` / `verifyProof`), and makes promo codes with `makeCodes` (80 bits: a code’s key is public, so a short code can be guessed offline) and `codeAddresses` for the grant’s `keys` (all from `@xgamesfun/solana/results`).',
142
+ 'The page can’t be trusted: a game runs in the player’s browser, so a result worth paying is decided or checked by your server, which signs the voucher. A server checks what a wallet holds with `collectiblesOf` / `holds`, pays only wallets a player proved (`proofRequest` / `verifyProof`), and makes promo codes with `makeCodes` (80 bits: a code’s key is public, so a short code can be guessed offline) and `codeAddresses` for the grant’s `keys` (all from `@xgamesfun/solana/results`). How far a signed result can be trusted is what the server checks: it decides the result (runs the game or match; the page sends only inputs), replays it (re-runs the page’s inputs from randomness it keeps), or only bounds it (pace, order, caps: a script at human speed gets through). Say which on each voucher grant (`"trust"`), and run xsolana_review before going live: it says what a cheater gets on every route.',
143
143
  'Bots play well: even a verified result pays a bot what it pays a good player. Keep token rewards for results small and capped (a grant’s limits), give SOL prizes only by chance (the oracle) or by what your server decides, and don’t let a minted farm rate run without end into what its token buys (it compounds: give it an end, a term or an upkeep).',
144
144
  'A gate costs a sybil most when it asks for something the game sells: a rule takes any of several items (`{ "items": ["oak", "pine"] }`, up to 16), and can read the attributes only the program writes on every minted collectible, `item_id`, `rarity`, `serial` and `made` (`{ "attribute": "rarity", "atLeast": 2 }`: any of the game’s Rare or better).',
145
145
  ];
@@ -137,7 +137,8 @@ export function changedParts(economy) {
137
137
  const only = new Set();
138
138
  const fixed = [];
139
139
  const removed = [];
140
- const strip = ({ id, index, shopBox, seller, ...rest }) => JSON.stringify(rest);
140
+ // A grant's `trust` is the spec's own note about its server, never sent on chain: changing it publishes nothing.
141
+ const strip = ({ id, index, shopBox, seller, trust, ...rest }) => JSON.stringify(rest);
141
142
  for (const [list, kind] of Object.entries(PART_LISTS)) {
142
143
  const before = new Map((published[list] ?? []).map(p => [p.key, p]));
143
144
  for (const part of economy[list] ?? []) {