@xgamesfun/solana 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/DISCLAIMER.md +1 -1
  2. package/NOTICE +4 -4
  3. package/README.md +100 -52
  4. package/bin/xsolana.mjs +32 -11
  5. package/examples/blind-forge.json +2 -4
  6. package/examples/coin-flip-arena.json +2 -5
  7. package/examples/duel-arena.json +1 -4
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/hero-trials.json +14 -2
  10. package/examples/ideas.json +788 -788
  11. package/examples/nft-raffle.json +1 -0
  12. package/examples/partner-vault.json +19 -3
  13. package/examples/sacrifice-raffle.json +2 -10
  14. package/examples/shelter-fair.json +1 -0
  15. package/examples/stakers-draw.json +4 -7
  16. package/examples/trade-in-shop.json +2 -3
  17. package/js/approve/details.mjs +117 -48
  18. package/js/approve/ed25519.mjs +5 -1
  19. package/js/approve/live-check.mjs +83 -26
  20. package/js/approve/page.html +217 -78
  21. package/js/approve/page.js +43 -15
  22. package/js/approve/server.mjs +276 -74
  23. package/js/chain/base58.js +7 -1
  24. package/js/chain/borsh.js +15 -11
  25. package/js/chain/bytes.js +11 -8
  26. package/js/chain/core.js +21 -5
  27. package/js/chain/curve.js +1 -1
  28. package/js/chain/lookup.js +118 -36
  29. package/js/chain/mint.js +6 -2
  30. package/js/chain/pda.js +2 -2
  31. package/js/chain/programs.js +6 -6
  32. package/js/chain/rpc.js +40 -14
  33. package/js/chain/token.js +13 -2
  34. package/js/chain/token2022.js +55 -16
  35. package/js/chain/tx.js +32 -22
  36. package/js/client/accounts.js +270 -80
  37. package/js/client/compressed.js +4 -3
  38. package/js/client/grant.js +165 -40
  39. package/js/client/idl/loot.js +1 -1
  40. package/js/client/idl/loot.json +104 -16
  41. package/js/client/ix.js +2121 -457
  42. package/js/client/pda.js +20 -18
  43. package/js/client/policy.js +12 -5
  44. package/js/client/random.js +2 -2
  45. package/js/client/sizes.js +220 -65
  46. package/js/client/traits.js +14 -5
  47. package/js/connect/connect.mjs +59 -17
  48. package/js/economy/check.mjs +3884 -690
  49. package/js/economy/cost.mjs +381 -57
  50. package/js/economy/examples.mjs +176 -35
  51. package/js/economy/explain.mjs +468 -110
  52. package/js/economy/guide.mjs +282 -59
  53. package/js/economy/legal.mjs +12 -6
  54. package/js/economy/live.mjs +62 -20
  55. package/js/economy/model.mjs +320 -97
  56. package/js/economy/plan.mjs +1022 -203
  57. package/js/economy/review.mjs +760 -0
  58. package/js/economy/schema.json +134 -372
  59. package/js/economy/secrets.mjs +17 -7
  60. package/js/economy/simulate.mjs +771 -178
  61. package/js/economy/trust.mjs +15 -0
  62. package/js/economy/txsize.mjs +15 -3
  63. package/js/economy/validate.mjs +61 -24
  64. package/js/index.js +1 -0
  65. package/js/mcp/server.mjs +43 -15
  66. package/js/mcp/tools.mjs +318 -80
  67. package/js/play/index.js +1960 -461
  68. package/js/play/keys.js +35 -9
  69. package/js/results/index.js +225 -32
  70. package/js/results/server.js +230 -0
  71. package/js/results/wire.js +64 -16
  72. package/package.json +6 -4
  73. package/release.json +5 -5
  74. package/skills/xsolana-economy/SKILL.md +52 -22
  75. package/skills/xsolana-economy/references/after-launch.md +32 -21
  76. package/skills/xsolana-economy/references/apps.md +47 -36
  77. package/skills/xsolana-economy/references/sentence-to-spec.md +149 -129
  78. package/skills/xsolana-economy/references/telling-the-person.md +15 -6
  79. package/skills/xsolana-economy/references/trust-and-cheating.md +126 -0
  80. package/types/chain/core.d.ts +1 -1
  81. package/types/chain/lookup.d.ts +1 -1
  82. package/types/client/accounts.d.ts +24 -3
  83. package/types/client/grant.d.ts +2 -2
  84. package/types/client/idl/loot.d.ts +22 -0
  85. package/types/client/ix.d.ts +54 -45
  86. package/types/client/pda.d.ts +3 -1
  87. package/types/economy/cost.d.mts +1 -1
  88. package/types/economy/explain.d.mts +3 -0
  89. package/types/economy/guide.d.mts +5 -0
  90. package/types/economy/model.d.mts +48 -3
  91. package/types/economy/plan.d.mts +44 -2
  92. package/types/economy/review.d.mts +45 -0
  93. package/types/economy/trust.d.mts +8 -0
  94. package/types/index.d.ts +1 -0
  95. package/types/mcp/tools.d.mts +62 -0
  96. package/types/play/index.d.ts +14 -25
  97. package/types/results/index.d.ts +8 -7
  98. package/types/results/server.d.ts +126 -0
  99. package/types/results/wire.d.ts +1 -1
@@ -11,7 +11,10 @@ export const CLUSTERS = Object.freeze({ devnet: 0, mainnet: 1 });
11
11
  export const CERT_KINDS = Object.freeze({
12
12
  host: { title: 'loot host key', fields: ['cluster', 'game', 'host', 'until'] },
13
13
  revoke: { title: 'loot revoke host key', fields: ['cluster', 'game', 'host'] },
14
- draw: { title: 'loot draw', fields: ['cluster', 'game', 'draw', 'engine', 'winners', 'prizes', 'payout', 'identity', 'slug', 'rates'] },
14
+ draw: {
15
+ title: 'loot draw',
16
+ fields: ['cluster', 'game', 'draw', 'engine', 'winners', 'prizes', 'payout', 'identity', 'slug', 'rates'],
17
+ },
15
18
  lock: { title: 'loot draw lock', fields: ['cluster', 'game', 'draw', 'roster', 'players', 'beacon'] },
16
19
  entry: { title: 'loot entry', fields: ['cluster', 'game', 'draw'] },
17
20
  giveaway: { title: 'loot giveaway host', fields: ['cluster', 'game', 'who', 'slug', 'grant', 'until'] },
@@ -20,13 +23,16 @@ export const CERT_KINDS = Object.freeze({
20
23
 
21
24
  /** One field's value, on its own line: a value with a line break in it (which would read as another field) is refused. */
22
25
  const field = (name, value) => {
23
- if (/[\r\n]/.test(String(value))) throw new RangeError(`A certificate's ${name} is one line: ${JSON.stringify(String(value))} has a line break.`);
26
+ if (/[\r\n]/.test(String(value)))
27
+ throw new RangeError(`A certificate's ${name} is one line: ${JSON.stringify(String(value))} has a line break.`);
24
28
  return value;
25
29
  };
26
- const write = (kind, values) => [CERT_KINDS[kind].title, ...CERT_KINDS[kind].fields.map((f) => `${f}: ${field(f, values[f])}`)].join('\n');
30
+ const write = (kind, values) =>
31
+ [CERT_KINDS[kind].title, ...CERT_KINDS[kind].fields.map(f => `${f}: ${field(f, values[f])}`)].join('\n');
27
32
 
28
33
  /** The text the owner signs to let `host` report `game`'s results until `until` (unix seconds). */
29
- export const hostCertText = ({ cluster = 'devnet', game, host, until }) => write('host', { cluster, game, host, until });
34
+ export const hostCertText = ({ cluster = 'devnet', game, host, until }) =>
35
+ write('host', { cluster, game, host, until });
30
36
  /** The text the owner signs to take `host` back. */
31
37
  export const revokeText = ({ cluster = 'devnet', game, host }) => write('revoke', { cluster, game, host });
32
38
 
@@ -38,10 +44,33 @@ export const revokeText = ({ cluster = 'devnet', game, host }) => write('revoke'
38
44
  * `slug`: the game's play-host name (its tokens' audience), or '-'; `rates`: the sha256 of the engine's rates (canonical
39
45
  * JSON), or 'default'.
40
46
  */
41
- export const drawText = ({ cluster = 'devnet', game, draw, engine, winners, prizes = [], payout = 'vouchers', identity = 'linked', slug = '-', rates = 'default' }) =>
42
- write('draw', { cluster, game, draw, engine, winners, prizes: prizes.length ? prizes.map((p) => `${p.place}:${p.grant}`).join(' ') : 'none', payout, identity, slug, rates });
47
+ export const drawText = ({
48
+ cluster = 'devnet',
49
+ game,
50
+ draw,
51
+ engine,
52
+ winners,
53
+ prizes = [],
54
+ payout = 'vouchers',
55
+ identity = 'linked',
56
+ slug = '-',
57
+ rates = 'default',
58
+ }) =>
59
+ write('draw', {
60
+ cluster,
61
+ game,
62
+ draw,
63
+ engine,
64
+ winners,
65
+ prizes: prizes.length ? prizes.map(p => `${p.place}:${p.grant}`).join(' ') : 'none',
66
+ payout,
67
+ identity,
68
+ slug,
69
+ rates,
70
+ });
43
71
  /** The text the organizer signs to lock a draw's players (`roster`: rosterHash) to a beacon round still to come. */
44
- export const lockText = ({ cluster = 'devnet', game, draw, roster, players, beacon, net = 'quicknet' }) => write('lock', { cluster, game, draw, roster, players, beacon: `${net} ${beacon}` });
72
+ export const lockText = ({ cluster = 'devnet', game, draw, roster, players, beacon, net = 'quicknet' }) =>
73
+ write('lock', { cluster, game, draw, roster, players, beacon: `${net} ${beacon}` });
45
74
  /** The text a player's wallet signs to enter a draw (identity 'wallet'). */
46
75
  export const entryText = ({ cluster = 'devnet', game, draw }) => write('entry', { cluster, game, draw });
47
76
 
@@ -49,20 +78,31 @@ export const entryText = ({ cluster = 'devnet', game, draw }) => write('entry',
49
78
  * The text the owner (or authority) signs to appoint a giveaway host: `who` is "x:<X account id>" (the host signs in
50
79
  * with that X account through the play host) or a wallet address; their giveaways pay from grant `grant` alone.
51
80
  */
52
- export const giveawayHostText = ({ cluster = 'devnet', game, who, slug, grant, until }) => write('giveaway', { cluster, game, who, slug, grant, until });
81
+ export const giveawayHostText = ({ cluster = 'devnet', game, who, slug, grant, until }) =>
82
+ write('giveaway', { cluster, game, who, slug, grant, until });
53
83
  /** The text the owner (or authority) signs to take a giveaway host back. */
54
- export const revokeGiveawayText = ({ cluster = 'devnet', game, who }) => write('revokeGiveaway', { cluster, game, who });
84
+ export const revokeGiveawayText = ({ cluster = 'devnet', game, who }) =>
85
+ write('revokeGiveaway', { cluster, game, who });
55
86
 
56
87
  /** A draw's players, as one hash: each `id<TAB>wallet`, sorted by id, one a line; sha256, hex. */
57
88
  export async function rosterHash(entries) {
58
- const text = [...entries].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).map((e) => `${e.id}\t${e.wallet}`).join('\n');
59
- return Array.from(new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', utf8(text))), (b) => b.toString(16).padStart(2, '0')).join('');
89
+ const text = [...entries]
90
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
91
+ .map(e => `${e.id}\t${e.wallet}`)
92
+ .join('\n');
93
+ return Array.from(new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', utf8(text))), b =>
94
+ b.toString(16).padStart(2, '0'),
95
+ ).join('');
60
96
  }
61
97
 
62
98
  /** JSON with every object's keys sorted: one text for one value, so its hash means one thing. */
63
99
  export function canonicalJson(value) {
64
100
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
65
- if (value && typeof value === 'object') return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
101
+ if (value && typeof value === 'object')
102
+ return `{${Object.keys(value)
103
+ .sort()
104
+ .map(k => `${JSON.stringify(k)}:${canonicalJson(value[k])}`)
105
+ .join(',')}}`;
66
106
  return JSON.stringify(value);
67
107
  }
68
108
 
@@ -74,9 +114,17 @@ export function canonicalJson(value) {
74
114
  * place past 255 and more than 255 winners are refused, never written as something else.
75
115
  */
76
116
  export function resultBytes({ cluster = 'devnet', game, round, endedAt, winners }) {
77
- if (!Object.hasOwn(CLUSTERS, cluster)) throw new RangeError(`Unknown cluster ${JSON.stringify(cluster)}: a result names ${Object.keys(CLUSTERS).join(' or ')}.`);
78
- const roundAt = typeof round === 'string' && /^[0-9a-f]{32}$/i.test(round) ? fromHex(round) : round instanceof Uint8Array && round.length === 16 ? round : null;
79
- if (!roundAt) throw new RangeError('A result\'s round is 16 bytes (32 hex characters).');
117
+ if (!Object.hasOwn(CLUSTERS, cluster))
118
+ throw new RangeError(
119
+ `Unknown cluster ${JSON.stringify(cluster)}: a result names ${Object.keys(CLUSTERS).join(' or ')}.`,
120
+ );
121
+ const roundAt =
122
+ typeof round === 'string' && /^[0-9a-f]{32}$/i.test(round)
123
+ ? fromHex(round)
124
+ : round instanceof Uint8Array && round.length === 16
125
+ ? round
126
+ : null;
127
+ if (!roundAt) throw new RangeError("A result's round is 16 bytes (32 hex characters).");
80
128
  if (!Array.isArray(winners) || winners.length > 255) throw new RangeError('A result names at most 255 winners.');
81
129
  return concat(
82
130
  utf8(RESULT_DOMAIN),
@@ -85,6 +133,6 @@ export function resultBytes({ cluster = 'devnet', game, round, endedAt, winners
85
133
  roundAt,
86
134
  uintLE(BigInt(endedAt), 8),
87
135
  Uint8Array.of(winners.length),
88
- ...winners.map((w) => concat(addressBytes(w.wallet), uintLE(w.place, 1), uintLE(w.grant ?? NO_GRANT, 2))),
136
+ ...winners.map(w => concat(addressBytes(w.wallet), uintLE(w.place, 1), uintLE(w.grant ?? NO_GRANT, 2))),
89
137
  );
90
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xgamesfun/solana",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "mcpName": "io.github.XGamesFun/solana-kit",
5
5
  "description": "One shared Solana program for game and community economies, its JS client, and the tools that let an AI agent design, check, simulate and launch an economy on it.",
6
6
  "license": "Apache-2.0",
@@ -71,10 +71,11 @@
71
71
  ],
72
72
  "scripts": {
73
73
  "test": "node --test \"js/test/**/*.test.mjs\"",
74
- "lint": "eslint .",
74
+ "lint": "eslint . && prettier --check .",
75
+ "format": "prettier --write .",
75
76
  "types": "tsc -p tsconfig.types.json",
76
77
  "fixtures": "node scripts/fetch-fixtures.mjs",
77
- "check": "node scripts/fetch-fixtures.mjs --check && node scripts/sync-skills.mjs --check && node scripts/release-manifest.mjs --check && eslint . && npm test",
78
+ "check": "node scripts/fetch-fixtures.mjs --check && node scripts/sync-skills.mjs --check && node scripts/release-manifest.mjs --check && npm run lint && npm test",
78
79
  "check:idl": "node scripts/sync-idl.mjs --check",
79
80
  "sync-idl": "node scripts/sync-idl.mjs",
80
81
  "sync-skills": "node scripts/sync-skills.mjs",
@@ -85,10 +86,11 @@
85
86
  "@eslint/js": "10.0.1",
86
87
  "@modelcontextprotocol/sdk": "1.32.2",
87
88
  "ajv": "8.20.0",
88
- "eslint": "10.13.0",
89
+ "eslint": "10.14.0",
89
90
  "globals": "17.13.0",
90
91
  "litesvm": "1.6.1",
91
92
  "playwright-core": "1.64.1",
93
+ "prettier": "3.9.10",
92
94
  "typescript": "7.0.2"
93
95
  },
94
96
  "bin": {
package/release.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.24.0",
2
+ "version": "0.25.0",
3
3
  "programId": "9cwqM1TXytPnFZsLfmUyv2dg4uW6Ch9C6UJVPns7NFQ",
4
- "sbfBytes": 1727272,
5
- "sbfSha256": "1bf01fbceb390c8888921df95f711d32e805554401e2c128e83eb16fcb11098c",
6
- "idlSha256": "32dc0d5ed56fa7fb2ef8e90ef8d96d1d8bbe940b624d3d5b636933af20ae56b4",
7
- "clientSha256": "f2a0f00386a08ecbd1fd939d53889d1a7a151997bf16684c641dd98a838b8d72"
4
+ "sbfBytes": 1741840,
5
+ "sbfSha256": "ea19d15d03211f7bd0b25179d51f75544342225127fcb631e687eae6750c9ba7",
6
+ "idlSha256": "41e621990c17b8645f59bb2a170271645ad370d1c1bf9f1dbdd04f817b82d4cf",
7
+ "clientSha256": "78b426dc003d261537934856302fe9767db149fd37b814af2569b95cc1333624"
8
8
  }
@@ -9,8 +9,8 @@ An economy is one JSON spec (`xsolana.json` in the project) on **loot**, a Solan
9
9
  writes or deploys a contract: the spec is checked, simulated and priced by the `xsolana_*` tools, and the project's
10
10
  owner puts it live by approving in their own wallet. It runs on **devnet** until the program is audited.
11
11
 
12
- The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.24.0 <guide|examples|check|simulate|cost|explain|publish|status> [xsolana.json]`,
13
- or `npx -y @xgamesfun/solana@0.24.0 tool <name> '<json>'` for any tool (always with the exact version).
12
+ The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.25.1 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
13
+ or `npx -y @xgamesfun/solana@0.25.1 tool <name> '<json>'` for any tool (always with the exact version).
14
14
 
15
15
  ## Rules (always)
16
16
 
@@ -25,12 +25,15 @@ or `npx -y @xgamesfun/solana@0.24.0 tool <name> '<json>'` for any tool (always w
25
25
  5. **Numbers come from the tools.** What a box gives back, how long a pool lasts, what going live costs: quote
26
26
  `xsolana_simulate`, `xsolana_cost` and `xsolana_explain`, never your own arithmetic. What a player pays is the tools'
27
27
  "in all" number for that action (price, fee, oracle, network and rent that never comes back): quote it as is.
28
- 6. **Flag the law.** When the checker raises `W-CHANCE-PRIZE` (paid chance at money: boxes with prizes, lotteries,
29
- contests), `W-CHANCE-ITEMS` (paid random collectibles a player can resell: loot boxes, gacha) or `W-REVENUE-SHARE` (stakers paid from sales), tell the person in plain words, every time, and never
30
- call anything an investment, a return or passive income. For an auction, always say `W-AUCTION-SHILL`: anyone,
31
- the seller included, can bid to push the price up. Before they go live, say as well that the program is not
32
- independently audited and comes as is, with no warranty: they use it at their own risk, and making sure the
33
- economy is legal where it is offered is theirs. Never promise it is safe, and never give legal advice.
28
+ 6. **Flag the law once.** The first time the checker raises `W-CHANCE-PRIZE` (paid chance at money: boxes with
29
+ prizes, lotteries, contests), `W-CHANCE-ITEMS` (paid random collectibles a player can resell: loot boxes, gacha) or
30
+ `W-REVENUE-SHARE` (stakers paid from sales), say it in one plain sentence. Don't repeat it in later answers, or add
31
+ legal notes to pages and apps you build, unless the person asks or a flag of a new kind appears. When they ask about
32
+ the law, answer what you know and leave the rest to their lawyer: you are not one. Never call anything an
33
+ investment, a return or passive income, and never promise it is safe. The approval page shows the flags and that
34
+ the program is not independently audited and comes as is, and asks the owner to tick that they read them, so going
35
+ live needs nothing more from you. For an auction, say once how bidding works (`W-AUCTION-SHILL`): anyone, the
36
+ seller included, can bid to push the price up.
34
37
  7. **Take addresses from the tools, never from memory**: the program, its accounts, the IDL. Every answer names them.
35
38
  8. **Public data only in a spec**: names, prices, percents, public addresses. Leave out `id`, `index`, `shopBox`,
36
39
  and `live`: they are written when it goes live.
@@ -38,7 +41,9 @@ or `npx -y @xgamesfun/solana@0.24.0 tool <name> '<json>'` for any tool (always w
38
41
  anything a limit, break the idea into its pieces (what players pay, what they get, what is random, what the chain
39
42
  can't see, what is held and for how long, who is paid) and find the part for each (`references/sentence-to-spec.md`).
40
43
  What the chain can't see (a match, a score, something done offline) is vouched for by a signer: a results server's voucher, a
41
- decider's key, a list. Build the combination and say which part does which piece. When the tools' words and the
44
+ decider's key, a list. The signer is not the end of the story: the program checks its signature and limits, never
45
+ what happened, so a signed result is worth what that server checks (it decides the result, replays it, or only
46
+ bounds what the page reports: rule 12). Build the combination and say which part does which piece. When the tools' words and the
42
47
  checker disagree (a field the schema takes that a sentence somewhere calls impossible), the checker and
43
48
  `xsolana_guide`'s parts decide: try it in a spec and run `xsolana_check`.
44
49
  A piece is a limit only when no part or combination does it. Then:
@@ -49,13 +54,29 @@ or `npx -y @xgamesfun/solana@0.24.0 tool <name> '<json>'` for any tool (always w
49
54
  second step). Don't withdraw a draft that checks, and don't send them elsewhere while a version on loot is on the
50
55
  table.
51
56
 
52
- 10. **People finish what follows; build the button, not a server.** Every step after a player's ask (settle a roll,
53
- draw a lottery, pull its parts (each place, a refund, a share), refund a draw the oracle never answered, claim a
54
- win, end an auction, run a sealed schedule, take a rental's NFT home) can be sent by the person who
55
- gains from it, from the page (`app.claims()` lists them, `app.claim(c)` sends one). A
56
- grant's claim is the winner's alone: the winning wallet signs it, whatever proves it won; anyone may pay its fees,
57
- nobody claims for a player. loot needs no server to finish anything. Never make a key in a page or a server for
58
- the owner to fund: whoever signs pays.
57
+ 10. **Design for many wallets and bots.** Wallets are free and bots play well: bound every reward with totals
58
+ (`perDay`, `total`), gate open rewards on holding something or count people by vouchers (`perPerson`, with the
59
+ server naming an X account, never a wallet), keep token rewards for results small, and have a server decide or
60
+ replay a result worth paying rather than take the page's word (`@xgamesfun/solana/results`: `collectiblesOf`,
61
+ `verifyProof`, `makeCodes`). Keep entry fees and prize pools for results the server decides and for chance, and
62
+ give a minted farm an end. The checker warns about the rest (`W-GRANT-TOTAL`, `W-GRANT-OPEN`, `W-FARM-MINT`);
63
+ `references/trust-and-cheating.md` says why.
64
+
65
+ 11. **People finish what follows; build the button, not a server.** Every step after a player's ask (settle a roll,
66
+ draw a lottery, pull its parts (each place, a refund, a share), refund a draw the oracle never answered, claim a
67
+ win, end an auction, run a sealed schedule, take a rental's NFT home) can be sent by the person who
68
+ gains from it, from the page (`app.claims()` lists them, `app.claim(c)` sends one). A
69
+ grant's claim is the winner's alone: the winning wallet signs it, whatever proves it won; anyone may pay its fees,
70
+ nobody claims for a player. loot needs no server to finish anything. Never make a key in a page or a server for
71
+ the owner to fund: whoever signs pays.
72
+
73
+ 12. **Review before going live, and let the person choose the trade-off.** Run `xsolana_review` before the owner
74
+ approves. Fix every high finding, or have the person accept it in plain words. Declare `"trust"` honestly on every
75
+ voucher grant: `"decides"` (the server runs the game or match; the page sends only inputs), `"replays"` (it re-runs
76
+ the page's inputs from randomness it keeps) or `"bounds"` (it takes the page's word within a pace and caps). Then
77
+ tell the person, for each signed route, which level it uses, what a cheater gets (the review's ceiling, with what
78
+ the prize goes on to earn), and what the stronger level would cost to build. They choose; you don't choose for
79
+ them by leaving it out. `references/trust-and-cheating.md` has the levels, the costs and the checklist.
59
80
 
60
81
  ## The loop
61
82
 
@@ -78,18 +99,26 @@ or `npx -y @xgamesfun/solana@0.24.0 tool <name> '<json>'` for any tool (always w
78
99
  draws an open, a guaranteed rare), opened several at once from the page; cards whose traits differ are a fresh
79
100
  item drawing from `tables`. Every other shape (bundles, an NFT as the price, stashes, gates, no-loss lotteries,
80
101
  amounts by rank) is in `references/sentence-to-spec.md` and `xsolana_guide`.
102
+ A lottery always names where its places are paid from, and there is no default: ask the person if their words
103
+ don't say. `"pot": 0` keeps a pot from round to round: a seed, sponsors, shares of other sales and what earlier
104
+ rounds left all count, and the next round starts with what the round before left (a jackpot that grows, a no-loss
105
+ lottery). `"rest": "<destination>"` pays each round from its own ticket money alone, which nothing from outside can
106
+ change, and sends what the places leave there (`"treasury"`, a wallet, `"burn"`; never a pool or `"chosen"`).
81
107
  Then say what stays the project's job (`references/telling-the-person.md`): the files behind every item's link
82
108
  (every value of a fresh item's `{attribute}`), a table's rent, and the collectible rent a pack's settle pays.
83
109
  6. **Iterate on what they say.** "Make legendaries 0.5%", "a bigger seed": change, check, simulate, explain again.
84
110
  7. **Save it**: `xsolana_save {"spec": …}` writes `xsolana.json`, only if it checks. A live file keeps its program numbers.
85
- 8. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the command. Tell them to
111
+ 8. **Review it before they approve**: `xsolana_review` (rule 12). Fix the high findings, declare `trust` on every
112
+ voucher grant, and tell the person which level each signed route uses, what a cheater gets and what the stronger
113
+ level would cost. Run it again after every change that touches a route that pays.
114
+ 9. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the command. Tell them to
86
115
  run it themselves in the project folder, exactly as `xsolana_publish` gives it (it names the exact version:
87
- `npx -y @xgamesfun/solana@0.24.0 approve xsolana.json`; never drop the version). It opens a page on their computer where
116
+ `npx -y @xgamesfun/solana@0.25.1 approve xsolana.json`; never drop the version). It opens a page on their computer where
88
117
  they connect a devnet wallet and approve; what lands is written back into `xsolana.json`. You publish nothing.
89
- 9. **After it is live**: `xsolana_status` shows each part on chain; read it before acting on what the person says
90
- happened. A later change is the same loop: save, publish, and the approval sends only what changed. What only the
91
- owner signs (open or decide a contest, pay out) goes on an owner page they approve in their own wallet:
92
- `references/after-launch.md`.
118
+ 10. **After it is live**: `xsolana_status` shows each part on chain; read it before acting on what the person says
119
+ happened. A later change is the same loop: save, publish, and the approval sends only what changed. What only the
120
+ owner signs (open or decide a contest, pay out) goes on an owner page they approve in their own wallet:
121
+ `references/after-launch.md`.
93
122
 
94
123
  ## Building the app around it
95
124
 
@@ -107,3 +136,4 @@ For staking an NFT of another collection, show what custody means and get explic
107
136
  - Telling the person what they get, and what stays the project's job: `references/telling-the-person.md`
108
137
  - Building the app for a live economy: `references/apps.md`
109
138
  - After it goes live (who sends each step, owner pages, results from a game, a results server): `references/after-launch.md`
139
+ - What a server decides, replays or bounds, who gets paid, and the exploit checklist: `references/trust-and-cheating.md`
@@ -6,21 +6,21 @@ 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) | 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, each pool's prize) and opens the next round at once. 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 pot gets back) | 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 |
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 |
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 |
24
24
 
25
25
  "Anyone" means the page can offer a button for it, and any player can press it. Build those buttons with `js/play`:
26
26
  `app.claims()` lists what this player can finish now (a draw, its cancellation, a pull, a payout, a refund, an
@@ -33,7 +33,8 @@ A round of a lottery is a record of its own, opened by its first purchase (the S
33
33
  token accounts and each entry's bytes; with too little, its first buyer fronts the header and accounts, about 0.004 SOL
34
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
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. Each purchase adds an entry: funded by `roundRent`, the buyer pays only the fee and the price; a
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
37
38
  buyer who pays its bytes themselves (about 0.0002 SOL) never gets them back (they go to the treasury when the round
38
39
  closes). A share the
39
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
@@ -85,13 +86,23 @@ the winner claims themselves. The grant's limits (per wallet in all and per wind
85
86
  total, between two dates) are the most anything can ever pay out, whatever happens to a server.
86
87
 
87
88
  - **A list** (an airdrop, a known set of winners): the owner writes the wallets in the grant's `list`; no server.
88
- - **Promo codes, claim links**: the grant's `keys` are the public keys of one keypair per code (made offline from each
89
- code; only public keys go in the spec), with `limits.perKey: 1`. The player types the code; the page derives its
90
- keypair, signs a voucher for the player's own wallet, and claims (`app.claimGrant(key, { voucher })`). A code pays
91
- 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.
92
95
  - **Show a badge, claim once**: an open grant (no `vouchers`, `keys` or `list`) with a `gate` and `limits.perWallet`:
93
96
  any wallet that shows the collectible claims, and each collectible shown counts too. No server.
94
- - **Vouchers from a results server**: the grant names the server's public key in `vouchers`; the server checks the
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.
104
+ - **Vouchers from a results server**: the grant names the server's public key in `vouchers`, and says in `trust`
105
+ what the server checks (`"decides"`, `"replays"` or `"bounds"`: `trust-and-cheating.md`); the server checks the
95
106
  game's result and signs a voucher (for a grant of tokens, with each winner's own `amount`, up to the grant's: rewards
96
107
  by rank); the winner's page claims with it (`app.claimGrant(key, { voucher })`).
97
108
  - The server can be the person's own game server (`signVoucher` from `@xgamesfun/solana/results`). It holds only that
@@ -12,58 +12,69 @@ 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, outcome chances and whenOut (show it: stops selling, or draws among what is left)
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?, rolled? }
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 (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
53
- await app.farmWalk('guild'); // { ends, needsWalk, parts }: end times the farm has passed; a move refuses (FarmNeedsWalk) past what it can walk
54
- await app.walkFarm('guild'); // walks it in parts (anyone, free); claims() lists it when it is needed
55
- await app.collectibles(); // what the player owns: [{ asset, item, name, uri, attributes }] (`item`: its key)
56
- 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
57
64
  ```
58
65
 
59
66
  - **Serving it**: a plain `index.html` with a `<script type="module">` can't import a bare `@xgamesfun/solana/play` by
60
67
  itself. Either use a bundler (Vite: `npm create vite@latest`, then `npm install @xgamesfun/solana`), or put this import
61
68
  map before the module script and serve the folder (`npx serve .`):
62
69
  ```html
63
- <script type="importmap">{ "imports": {
64
- "@xgamesfun/solana/play": "./node_modules/@xgamesfun/solana/js/play/index.js",
65
- "@xgamesfun/solana/client": "./node_modules/@xgamesfun/solana/js/client/index.js"
66
- } }</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>
67
78
  ```
68
79
  Tell the person the one command that starts it, and that it serves on devnet.
69
80
  - **What a roll looks like**: a box of several outcomes is rolled by the oracle a few seconds after the open; `openBox`
@@ -95,14 +106,14 @@ await app.balances(); // what they hold of each of the econo
95
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.
96
107
  - **NFTs the project has no authority over**: a gate or odds that name a collection find the player's Core NFTs of it
97
108
  themselves; for a Token Metadata NFT pass `shown: { mint, owner }`, for a compressed one `shown: { compressed:
98
- 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`,
99
110
  `app.harvest`, `app.hold` and `app.endAuction` take any NFT: a Core asset, a Token Metadata mint (legacy or pNFT),
100
111
  or a compressed NFT's asset id; the page finds its kind itself, and proves a compressed one first (its RPC must
101
112
  offer DAS: `getAsset`, `getAssetProof`). In a farm that watches, `app.harvest` shows the NFT is still the player's,
102
113
  and `app.stakeNft` of an NFT someone else staked and sold takes their seat over. What the project wrote about one is in
103
114
  its Sheet (`loot.decodeSheet`).
104
115
  - **Off-chain traits** (the spec's `traits`): the project builds the tree with `loot.traitsTree([{ nft, attributes
105
- }])` 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,
106
117
  before a stake) calls `app.proveTraits(nft, attributes, proof)` once per NFT, and its boosts and gates read them.
107
118
  - **Rolls on an NFT**: `app.openBox(boxKey, { on: asset })` for a box that rolls on one (an enchant). Leaving a
108
119
  mission that rolls, `app.unstakeNft` returns `{ signature, roll, rolled }`; a recipe that rolls, `app.craft` returns