@xgamesfun/solana 0.20.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
package/js/mcp/tools.mjs CHANGED
@@ -40,7 +40,7 @@ export function plainName(rel, paths = path) {
40
40
  const name = String(rel).split(paths.sep).join('/');
41
41
  return SAVE_NAME.test(name) ? name : null;
42
42
  }
43
- const NOT_PLAIN = (rel) => `${JSON.stringify(String(rel))} is not a plain file name`;
43
+ const NOT_PLAIN = rel => `${JSON.stringify(String(rel))} is not a plain file name`;
44
44
  const RENAME = 'use letters, digits and . _ - (and / for a folder), as in xsolana.json';
45
45
  /**
46
46
  * The command an owner runs to approve going live in their own wallet, pinned to this package's exact version, with
@@ -55,11 +55,17 @@ export function approveCommand(file = '', paths = path) {
55
55
  export const CLUSTER = 'devnet';
56
56
  export const DEVNET_RPC = 'https://api.devnet.solana.com';
57
57
  /** What every answer carries, so an agent never has to remember the program's address. */
58
- export const PROGRAM_INFO = Object.freeze({ program: LOOT_PROGRAM, cluster: CLUSTER, version: VERSION, idl: '@xgamesfun/solana/idl.json', package: `@xgamesfun/solana@${PACKAGE_VERSION}` });
58
+ export const PROGRAM_INFO = Object.freeze({
59
+ program: LOOT_PROGRAM,
60
+ cluster: CLUSTER,
61
+ version: VERSION,
62
+ idl: '@xgamesfun/solana/idl.json',
63
+ package: `@xgamesfun/solana@${PACKAGE_VERSION}`,
64
+ });
59
65
  const footer = `\n\nloot ${VERSION} · program ${LOOT_PROGRAM} · ${CLUSTER}`;
60
66
  const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
61
- const nowOf = (ctx) => ctx?.now ?? Math.floor(Date.now() / 1000);
62
- const cwdOf = (ctx) => ctx?.cwd ?? process.cwd();
67
+ const nowOf = ctx => ctx?.now ?? Math.floor(Date.now() / 1000);
68
+ const cwdOf = ctx => ctx?.cwd ?? process.cwd();
63
69
  export const DEFAULT_FILE = 'xsolana.json';
64
70
 
65
71
  /** The real path of `p`, or of the nearest folder above it that exists, with the rest added back. */
@@ -79,22 +85,24 @@ function realPath(p) {
79
85
  export function projectFile(rel, ctx) {
80
86
  const root = realPath(path.resolve(cwdOf(ctx)));
81
87
  const file = path.resolve(root, String(rel));
82
- const inside = (p) => {
88
+ const inside = p => {
83
89
  const r = path.relative(root, p);
84
90
  return r !== '' && !r.startsWith('..') && !path.isAbsolute(r);
85
91
  };
86
- if (!inside(file) || !inside(realPath(file))) throw new Error(`${rel} is outside the project folder (${root}): a spec file must be inside it.`);
92
+ if (!inside(file) || !inside(realPath(file)))
93
+ throw new Error(`${rel} is outside the project folder (${root}): a spec file must be inside it.`);
87
94
  if (path.extname(file).toLowerCase() !== '.json') throw new Error(`${rel} is not a .json file: a spec file is JSON.`);
88
95
  return file;
89
96
  }
90
97
 
91
98
  /** The spec's own top-level keys (schema.json). */
92
99
  const SPEC_KEYS = new Set(Object.keys(SCHEMA.properties));
93
- const plainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
100
+ const plainObject = v => v !== null && typeof v === 'object' && !Array.isArray(v);
94
101
  /** A file's spec: the JSON as it is, or what it holds under `economy` when that is its only key. */
95
- const specOf = (value) => (plainObject(value) && Object.keys(value).length === 1 && plainObject(value.economy) ? value.economy : value);
102
+ const specOf = value =>
103
+ plainObject(value) && Object.keys(value).length === 1 && plainObject(value.economy) ? value.economy : value;
96
104
  /** The spec to read: what a file holds under `economy` (a game file keeps it beside its own keys), else the JSON. */
97
- const economyOf = (value) => (plainObject(value) && plainObject(value.economy) ? value.economy : value);
105
+ const economyOf = value => (plainObject(value) && plainObject(value.economy) ? value.economy : value);
98
106
  /**
99
107
  * Whether a file's JSON is an economy spec, so xsolana_save may write over it: version 2 with its tokens (every spec that
100
108
  * checks has them), a currency if it names one (a spec of one token checks without: that token is its currency)
@@ -103,15 +111,30 @@ const economyOf = (value) => (plainObject(value) && plainObject(value.economy) ?
103
111
  */
104
112
  export function isSpec(value) {
105
113
  const s = specOf(value);
106
- return plainObject(s) && s.version === 2 && (s.currency === undefined || typeof s.currency === 'string') && Array.isArray(s.tokens) && Object.keys(s).every((k) => SPEC_KEYS.has(k));
114
+ return (
115
+ plainObject(s) &&
116
+ s.version === 2 &&
117
+ (s.currency === undefined || typeof s.currency === 'string') &&
118
+ Array.isArray(s.tokens) &&
119
+ Object.keys(s).every(k => SPEC_KEYS.has(k))
120
+ );
107
121
  }
108
122
 
109
123
  // ---------------------------------------------------------------- where a spec comes from
110
124
 
111
125
  const SOURCE = {
112
- spec: { type: 'object', description: 'An economy spec (version 2) given inline. Takes precedence over file and example.' },
113
- file: { type: 'string', description: `A JSON file holding the spec, relative to the project folder (default: ${DEFAULT_FILE} when it exists).` },
114
- example: { type: 'string', description: 'A worked example by name, from xsolana_examples (coin-flip-arena, stud-farm, …).' },
126
+ spec: {
127
+ type: 'object',
128
+ description: 'An economy spec (version 2) given inline. Takes precedence over file and example.',
129
+ },
130
+ file: {
131
+ type: 'string',
132
+ description: `A JSON file holding the spec, relative to the project folder (default: ${DEFAULT_FILE} when it exists).`,
133
+ },
134
+ example: {
135
+ type: 'string',
136
+ description: 'A worked example by name, from xsolana_examples (coin-flip-arena, stud-farm, …).',
137
+ },
115
138
  };
116
139
 
117
140
  async function load(input, ctx) {
@@ -121,7 +144,9 @@ async function load(input, ctx) {
121
144
  const file = projectFile(rel, ctx);
122
145
  if (!existsSync(file)) {
123
146
  if (input.file) throw new Error(`${rel} does not exist.`);
124
- throw new Error(`There is no ${DEFAULT_FILE} in ${cwdOf(ctx)} yet. Write a spec (xsolana_guide, xsolana_examples) and pass it as "spec", or save it with xsolana_save.`);
147
+ throw new Error(
148
+ `There is no ${DEFAULT_FILE} in ${cwdOf(ctx)} yet. Write a spec (xsolana_guide, xsolana_examples) and pass it as "spec", or save it with xsolana_save.`,
149
+ );
125
150
  }
126
151
  let value;
127
152
  try {
@@ -157,11 +182,13 @@ export function carryIds(previous, next) {
157
182
  const out = { ...next, live: previous.live };
158
183
  const carry = (list, fields) => {
159
184
  if (!next[list]) return;
160
- const old = new Map((previous[list] ?? []).map((p) => [p.key, p]));
161
- out[list] = next[list].map((p) => {
185
+ const old = new Map((previous[list] ?? []).map(p => [p.key, p]));
186
+ out[list] = next[list].map(p => {
162
187
  const was = old.get(p.key);
163
188
  if (!was) return p;
164
- const ids = Object.fromEntries(fields.filter((f) => was[f] !== undefined && p[f] === undefined).map((f) => [f, was[f]]));
189
+ const ids = Object.fromEntries(
190
+ fields.filter(f => was[f] !== undefined && p[f] === undefined).map(f => [f, was[f]]),
191
+ );
165
192
  return { ...p, ...ids };
166
193
  });
167
194
  };
@@ -169,7 +196,7 @@ export function carryIds(previous, next) {
169
196
  carry('pools', ['index']);
170
197
  for (const list of ['stashes', 'boxes', 'recipes', 'schedules', 'lotteries', 'grants']) carry(list, ['id']);
171
198
  // A farm that pays nothing keeps the number of the empty pool made for it; an auction keeps the seller its address names.
172
- carry('farms', ['id', 'rewardsIndex']);
199
+ carry('farms', ['id']);
173
200
  carry('auctions', ['id', 'seller']);
174
201
  return out;
175
202
  }
@@ -179,7 +206,13 @@ export function carryIds(previous, next) {
179
206
  /** The legal flags to pass on before the person approves, as lines for an answer: [] when there are none. */
180
207
  function flagsToTell(economy, now) {
181
208
  const flags = legalFlags(check(economy, { now }));
182
- return flags.length ? ['Before anything else, tell the person, in plain words (the approval page shows these too, and asks them to tick that they read them):', ...flags.map((f) => `- ${f.says}`), ''] : [];
209
+ return flags.length
210
+ ? [
211
+ "If you haven't told the person these yet, say each once, in a plain sentence (the approval page shows them too, and asks them to tick that they read them; after that, only when they ask):",
212
+ ...flags.map(f => `- ${f.says}`),
213
+ '',
214
+ ]
215
+ : [];
183
216
  }
184
217
 
185
218
  /**
@@ -191,8 +224,12 @@ function liveChange(economy) {
191
224
  const { only, fixed, removed, settings } = changedParts(economy);
192
225
  const notes = [
193
226
  settings.length ? `\n${settingsNote(settings, economy.live)}` : '',
194
- fixed.length ? `\nThese can't change once live, and stay as they were: ${fixed.join(', ')} (a farm, an auction or a pool's token).` : '',
195
- removed.length ? `\nTaken out of the spec but still on chain: ${removed.join(', ')}. To stop one, keep it and set "active": false.` : '',
227
+ fixed.length
228
+ ? `\nThese can't change once live, and stay as they were: ${fixed.join(', ')} (a farm, an auction or a pool's token).`
229
+ : '',
230
+ removed.length
231
+ ? `\nTaken out of the spec but still on chain: ${removed.join(', ')}. To stop one, keep it and set "active": false.`
232
+ : '',
196
233
  ].join('');
197
234
  return { scope: { only, live: economy.live }, notes, settings };
198
235
  }
@@ -207,16 +244,26 @@ export const tools = [
207
244
  annotations: READ,
208
245
  description:
209
246
  'Read this first whenever someone wants a web3 economy, game economy, token economy, NFT mechanic or onchain reward system: a shop, loot boxes or packs, gacha, a lottery or raffle, an auction (highest bid wins, the outbid refunded at once), staking or a farm, crafting or merging, seasons and emissions, a prediction pool or contest, quests and airdrops, holder-gated drops, using NFTs of any other collection in a game. loot is ONE shared Solana program (devnet until it is audited): the economy is a JSON spec, not a new smart contract, so never write Anchor/Rust for what it covers. Returns every part an economy can have (fields from the schema, who acts at play time, fees), how to write a spec, and what loot cannot do (with the nearest alternative). Pass `part` for one part with its nested shapes.',
210
- inputSchema: { type: 'object', properties: { part: { type: 'string', enum: Object.keys(PARTS), description: 'One part, with its nested shapes.' } }, additionalProperties: false },
247
+ inputSchema: {
248
+ type: 'object',
249
+ properties: {
250
+ part: { type: 'string', enum: Object.keys(PARTS), description: 'One part, with its nested shapes.' },
251
+ },
252
+ additionalProperties: false,
253
+ },
211
254
  async handler({ part } = NO_OPTIONS) {
212
- return { text: guideText(part) + footer, parts: catalog(part), ...(part ? {} : { cannot: CANNOT }), ...PROGRAM_INFO };
255
+ return {
256
+ text: guideText(part) + footer,
257
+ parts: catalog(part),
258
+ ...(part ? {} : { cannot: CANNOT }),
259
+ ...PROGRAM_INFO,
260
+ };
213
261
  },
214
262
  },
215
263
  {
216
264
  name: 'xsolana_examples',
217
265
  annotations: READ,
218
- description:
219
- `Find an economy to start from. With \`query\` (the person’s own words, as they said them), returns the closest worked examples (whole specs that check and go live in the tests), the closest of ${ideas().length} real or trending economies rated against loot (great / some / a-lot / cannot, with what carries it or what is missing), and what loot can’t do exactly in the ask, each with the nearest versions it can. Call it again with the words of any part that looks impossible: the answer is a nearest version to offer, not a dead end. With \`name\`, returns that example’s whole spec to copy and change. With neither, lists every example.`,
266
+ description: `Find an economy to start from. With \`query\` (the person’s own words, as they said them), returns the closest worked examples (whole specs that check and go live in the tests), the closest of ${ideas().length} real or trending economies rated against loot (great / some / a-lot / cannot, with what carries it or what is missing), and what loot can’t do exactly in the ask, each with the nearest versions it can. Call it again with the words of any part that looks impossible: the answer is a nearest version to offer, not a dead end. With \`name\`, returns that example’s whole spec to copy and change. With neither, lists every example.`,
220
267
  inputSchema: {
221
268
  type: 'object',
222
269
  properties: {
@@ -231,24 +278,53 @@ export const tools = [
231
278
  if (name) {
232
279
  const ex = example(name);
233
280
  const verdict = check(ex.spec, { now: nowOf(ctx) });
234
- return { name: ex.name, spec: ex.spec, checks: verdict.ok ? 'clean' : verdict.refusals.map((r) => r.code), text: `${ex.spec.name}: ${ex.spec.prompt ?? ''}\nCopy the spec, change it to what the person asked, then run xsolana_check on it. Existing mints in examples are devnet stand-ins: use the person’s own.${footer}` };
281
+ return {
282
+ name: ex.name,
283
+ spec: ex.spec,
284
+ checks: verdict.ok ? 'clean' : verdict.refusals.map(r => r.code),
285
+ text: `${ex.spec.name}: ${ex.spec.prompt ?? ''}\nCopy the spec, change it to what the person asked, then run xsolana_check on it. Existing mints in examples are devnet stand-ins: use the person’s own.${footer}`,
286
+ };
235
287
  }
236
288
  if (!query) {
237
- const all = examples().map(({ name: n, spec }) => ({ name: n, title: spec.name, prompt: spec.prompt, parts: partsUsed(spec) }));
238
- return { examples: all, ideas: { count: ideas().length }, text: [`${all.length} worked examples (xsolana_examples {"name": "<name>"} gives the whole spec):`, ...all.map((e) => `- ${e.name}: ${e.title}. ${e.prompt ?? ''} [${e.parts.join(', ')}]`), `\nAnd ${ideas().length} rated economies: pass a query to search them.`].join('\n') + footer };
289
+ const all = examples().map(({ name: n, spec }) => ({
290
+ name: n,
291
+ title: spec.name,
292
+ prompt: spec.prompt,
293
+ parts: partsUsed(spec),
294
+ }));
295
+ return {
296
+ examples: all,
297
+ ideas: { count: ideas().length },
298
+ text:
299
+ [
300
+ `${all.length} worked examples (xsolana_examples {"name": "<name>"} gives the whole spec):`,
301
+ ...all.map(e => `- ${e.name}: ${e.title}. ${e.prompt ?? ''} [${e.parts.join(', ')}]`),
302
+ `\nAnd ${ideas().length} rated economies: pass a query to search them.`,
303
+ ].join('\n') + footer,
304
+ };
239
305
  }
240
306
  const r = search(query, { limit, fit });
241
307
  const text = [
242
- r.examples.length ? `Examples closest to “${query}” (xsolana_examples {"name": …} gives the whole spec):` : `No worked example matches “${query}”. Start from the nearest idea below, or from xsolana_guide.`,
243
- ...r.examples.map((e) => `- ${e.name}: ${e.title}. ${e.prompt ?? ''} [${e.parts.join(', ')}]`),
308
+ r.examples.length
309
+ ? `Examples closest to “${query}” (xsolana_examples {"name": …} gives the whole spec):`
310
+ : `No worked example matches “${query}”. Start from the nearest idea below, or from xsolana_guide.`,
311
+ ...r.examples.map(e => `- ${e.name}: ${e.title}. ${e.prompt ?? ''} [${e.parts.join(', ')}]`),
244
312
  r.hows.length ? '\nThese look impossible but are one field or a short combination (build them):' : '',
245
- ...r.hows.map((h) => `- ${h.what}: ${h.how}.`),
246
- r.ideas.length ? '\nRated economies (rated against this version of loot: a rating is a hint, the parts in xsolana_guide decide):' : '',
247
- ...r.ideas.map((i) => `- #${i.n} ${i.name} (${i.fit}): ${i.how}`),
248
- r.limits.length ? '\nWhat no part of loot does here, and the nearest versions (first check that no combination of parts does it; then offer these, best first, with their numbers from xsolana_cost; never end on “it can’t”):' : '',
249
- ...r.limits.flatMap((l) => [`- ${l.what}: ${l.why}.`, ...l.nearest.map((n, i) => ` ${i + 1}. ${n}`)]),
250
- r.ideas.some((i) => i.fit === 'cannot') ? '\nA "cannot" means a defining piece was missing when it was rated: check that piece against xsolana_guide’s parts and their combinations before telling the person, then offer the nearest version.' : '',
251
- ].filter(Boolean).join('\n');
313
+ ...r.hows.map(h => `- ${h.what}: ${h.how}.`),
314
+ r.ideas.length
315
+ ? '\nRated economies (rated against this version of loot: a rating is a hint, the parts in xsolana_guide decide):'
316
+ : '',
317
+ ...r.ideas.map(i => `- #${i.n} ${i.name} (${i.fit}): ${i.how}`),
318
+ r.limits.length
319
+ ? '\nWhat no part of loot does here, and the nearest versions (first check that no combination of parts does it; then offer these, best first, with their numbers from xsolana_cost; never end on “it can’t”):'
320
+ : '',
321
+ ...r.limits.flatMap(l => [`- ${l.what}: ${l.why}.`, ...l.nearest.map((n, i) => ` ${i + 1}. ${n}`)]),
322
+ r.ideas.some(i => i.fit === 'cannot')
323
+ ? '\nA "cannot" means a defining piece was missing when it was rated: check that piece against xsolana_guide’s parts and their combinations before telling the person, then offer the nearest version.'
324
+ : '',
325
+ ]
326
+ .filter(Boolean)
327
+ .join('\n');
252
328
  return { examples: r.examples, ideas: r.ideas, hows: r.hows, limits: r.limits, text: text + footer };
253
329
  },
254
330
  },
@@ -262,8 +338,16 @@ export const tools = [
262
338
  const { economy, from } = await load(input, ctx);
263
339
  const verdict = check(economy, { now: nowOf(ctx) });
264
340
  const head = verdict.refusals.length || verdict.warnings.length ? describe(verdict) : 'No problems found.';
265
- const tail = verdict.ok ? `\nReady (${plural(verdict.warnings.length, 'warning')}). Next: xsolana_simulate and xsolana_explain, and tell the person what it means in plain words.` : `\n${plural(verdict.refusals.length, 'refusal')}: fix each as its fix says, then check again.`;
266
- return { ok: verdict.ok, from, refusals: verdict.refusals, warnings: verdict.warnings, text: `${head}${tail}${footer}` };
341
+ const tail = verdict.ok
342
+ ? `\nReady (${plural(verdict.warnings.length, 'warning')}). Next: xsolana_simulate and xsolana_explain, and tell the person what it means in plain words.`
343
+ : `\n${plural(verdict.refusals.length, 'refusal')}: fix each as its fix says, then check again.`;
344
+ return {
345
+ ok: verdict.ok,
346
+ from,
347
+ refusals: verdict.refusals,
348
+ warnings: verdict.warnings,
349
+ text: `${head}${tail}${footer}`,
350
+ };
267
351
  },
268
352
  },
269
353
  {
@@ -271,7 +355,15 @@ export const tools = [
271
355
  annotations: READ,
272
356
  description:
273
357
  'The quick simulation (expected values from the spec, instant): what each box, ticket and craft gives back against its price, the house edge, each pool’s money in and out per day and whether it runs dry, settles or grows, farm rewards per unit staked, what players, the treasury and the platform get each day. `scenario` overrides the spec’s own ({"perDay": {"box:flip": 1000}, "days": 30, "staked": {"<farm>": 1000}}). Quote its numbers; never your own arithmetic.',
274
- inputSchema: { type: 'object', properties: { ...SOURCE, scenario: { type: 'object', description: 'What to assume instead of the spec’s scenario.' }, json: { type: 'boolean', description: 'Also return every row.' } }, additionalProperties: false },
358
+ inputSchema: {
359
+ type: 'object',
360
+ properties: {
361
+ ...SOURCE,
362
+ scenario: { type: 'object', description: 'What to assume instead of the spec’s scenario.' },
363
+ json: { type: 'boolean', description: 'Also return every row.' },
364
+ },
365
+ additionalProperties: false,
366
+ },
275
367
  async handler(input, ctx) {
276
368
  const { economy: e, from } = await load(input, ctx);
277
369
  const economy = input.scenario ? { ...e, scenario: { ...e.scenario, ...input.scenario } } : e;
@@ -279,32 +371,48 @@ export const tools = [
279
371
  if (!verdict.ok) return refused(verdict, from);
280
372
  const sim = simulate(economy, { now: nowOf(ctx) });
281
373
  const summary = {
282
- boxes: sim.boxes.map((b) => ({ key: b.key, returnPercent: b.returnPercent, houseEdgePercent: b.houseEdgePercent })),
283
- pools: sim.pools.map((p) => ({ key: p.key, lasts: p.lasts, lastsDays: p.lastsDays, settlesAt: p.settlesAt })),
284
- farms: sim.farms.map((f) => ({ key: f.key, rewardsPerDay: f.rewardsPerDay, yearlyPercent: f.yearlyPercent })),
374
+ boxes: sim.boxes.map(b => ({
375
+ key: b.key,
376
+ returnPercent: b.returnPercent,
377
+ houseEdgePercent: b.houseEdgePercent,
378
+ })),
379
+ pools: sim.pools.map(p => ({ key: p.key, lasts: p.lasts, lastsDays: p.lastsDays, settlesAt: p.settlesAt })),
380
+ farms: sim.farms.map(f => ({ key: f.key, rewardsPerDay: f.rewardsPerDay, yearlyPercent: f.yearlyPercent })),
285
381
  platformSolPerDay: sim.perDay.platform.totalSol,
286
382
  };
287
- return { ok: true, from, summary, ...(input.json ? { simulation: sim } : {}), text: explain(sim, economy) + footer };
383
+ return {
384
+ ok: true,
385
+ from,
386
+ summary,
387
+ ...(input.json ? { simulation: sim } : {}),
388
+ text: explain(sim, economy) + footer,
389
+ };
288
390
  },
289
391
  },
290
392
  {
291
393
  name: 'xsolana_cost',
292
394
  annotations: READ,
293
- description: 'What going live costs (each account’s rent, the program’s fees, network fees, the seeds, the transactions and wallet approvals it takes) and what each action costs whoever signs it while it runs.',
395
+ description:
396
+ 'What going live costs (each account’s rent, the program’s fees, network fees, the seeds, the transactions and wallet approvals it takes) and what each action costs whoever signs it while it runs.',
294
397
  inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
295
398
  async handler(input, ctx) {
296
399
  const { economy, from } = await load(input, ctx);
297
400
  const change = liveChange(economy);
298
401
  const c = cost(economy, { now: nowOf(ctx), ...change.scope });
299
402
  if (!c.ok) return refused(c, from);
300
- const head = change.scope.live ? (change.scope.only.size ? `This economy is live: this prices only the change (${[...change.scope.only].join(', ')}).\n` : 'This economy is live and nothing has changed since it went live: publishing costs nothing.\n') : '';
403
+ const head = change.scope.live
404
+ ? change.scope.only.size
405
+ ? `This economy is live: this prices only the change (${[...change.scope.only].join(', ')}).\n`
406
+ : 'This economy is live and nothing has changed since it went live: publishing costs nothing.\n'
407
+ : '';
301
408
  return { ok: true, from, setup: c.setup, actions: c.actions, fees: c.fees, text: head + explainCost(c) + footer };
302
409
  },
303
410
  },
304
411
  {
305
412
  name: 'xsolana_explain',
306
413
  annotations: READ,
307
- description: 'The economy in plain words for the person: what players do and what it costs and is worth, how the pools fill and drain, what runs by itself, what the authority must sign, what going live costs, and what is flagged for the law. Tell them the essentials in a few lines, from this text.',
414
+ description:
415
+ 'The economy in plain words for the person: what players do and what it costs and is worth, how the pools fill and drain, what runs by itself, what the authority must sign, what going live costs, and what is flagged for the law. Tell them the essentials in a few lines, from this text.',
308
416
  inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
309
417
  async handler(input, ctx) {
310
418
  const { economy, from } = await load(input, ctx);
@@ -316,7 +424,12 @@ export const tools = [
316
424
  name: 'xsolana_save',
317
425
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
318
426
  description: `Save a spec into the project (default ${DEFAULT_FILE}), only if it checks. A file that is already live keeps its program numbers and its live section. Never put a key in it: the checker refuses one.`,
319
- inputSchema: { type: 'object', properties: { spec: SOURCE.spec, file: SOURCE.file }, required: ['spec'], additionalProperties: false },
427
+ inputSchema: {
428
+ type: 'object',
429
+ properties: { spec: SOURCE.spec, file: SOURCE.file },
430
+ required: ['spec'],
431
+ additionalProperties: false,
432
+ },
320
433
  async handler(input, ctx) {
321
434
  const rel = input.file ?? DEFAULT_FILE;
322
435
  let file;
@@ -326,8 +439,18 @@ export const tools = [
326
439
  return { ok: false, isError: true, text: `Not saved: ${e.message}` };
327
440
  }
328
441
  // a name the approve command can carry as it is: letters, digits and . _ - in each part, parts split by /
329
- if (plainName(rel) === null) return { ok: false, isError: true, text: `Not saved: ${NOT_PLAIN(rel)}. Use letters, digits and . _ - (and / for a folder), as in ${DEFAULT_FILE}.` };
330
- if (findSecrets(input.spec).length) return { ok: false, isError: true, text: 'Not saved: the spec holds what looks like a private key or a seed phrase. Remove it (and treat that wallet as exposed).' };
442
+ if (plainName(rel) === null)
443
+ return {
444
+ ok: false,
445
+ isError: true,
446
+ text: `Not saved: ${NOT_PLAIN(rel)}. Use letters, digits and . _ - (and / for a folder), as in ${DEFAULT_FILE}.`,
447
+ };
448
+ if (findSecrets(input.spec).length)
449
+ return {
450
+ ok: false,
451
+ isError: true,
452
+ text: 'Not saved: the spec holds what looks like a private key or a seed phrase. Remove it (and treat that wallet as exposed).',
453
+ };
331
454
  let previous = null;
332
455
  if (existsSync(file)) {
333
456
  try {
@@ -337,42 +460,84 @@ export const tools = [
337
460
  }
338
461
  // Only a spec is ever overwritten: never another JSON file that happens to be there (a package.json has a
339
462
  // `version` too). A spec is version 2 with only the spec's own keys, as it is or under `economy`.
340
- if (!isSpec(previous)) return { ok: false, isError: true, text: `Not saved: ${rel} is there already and is not an economy spec. Pick another file name.` };
463
+ if (!isSpec(previous))
464
+ return {
465
+ ok: false,
466
+ isError: true,
467
+ text: `Not saved: ${rel} is there already and is not an economy spec. Pick another file name.`,
468
+ };
341
469
  }
342
470
  // (a spec wrapped under `economy`, given or already in the file, is read the same way xsolana_check reads it)
343
471
  const given = economyOf(input.spec);
344
472
  const spec = carryIds(previous && economyOf(previous), given);
345
- const dropped = given.live && !previous?.live ? ' The live section given was not kept: only `xsolana approve` writes one, as what it sends lands.' : '';
473
+ const dropped =
474
+ given.live && !previous?.live
475
+ ? ' The live section given was not kept: only `xsolana approve` writes one, as what it sends lands.'
476
+ : '';
346
477
  const verdict = check(spec, { now: nowOf(ctx) });
347
- if (!verdict.ok) return { ...refused(verdict, 'the spec given'), isError: true, text: `Not saved.\n${refused(verdict, 'the spec given').text}` };
478
+ if (!verdict.ok)
479
+ return {
480
+ ...refused(verdict, 'the spec given'),
481
+ isError: true,
482
+ text: `Not saved.\n${refused(verdict, 'the spec given').text}`,
483
+ };
348
484
  // A folder that isn't there yet is made, inside the project folder (projectFile checked the whole path).
349
485
  try {
350
486
  await mkdir(path.dirname(file), { recursive: true });
351
487
  } catch (e) {
352
- return { ok: false, isError: true, text: `Not saved: the folder for ${rel} can’t be made (${e.code ?? 'error'}).` };
488
+ return {
489
+ ok: false,
490
+ isError: true,
491
+ text: `Not saved: the folder for ${rel} can’t be made (${e.code ?? 'error'}).`,
492
+ };
353
493
  }
354
494
  await writeFile(file, `${JSON.stringify(spec, null, 2)}\n`);
355
- return { ok: true, file: rel, warnings: verdict.warnings, text: `Saved ${rel} (${plural(verdict.warnings.length, 'warning')}).${dropped}${spec.live ? ' It is live: xsolana_publish publishes only what changed.' : ' Next: xsolana_publish when the person says to put it live.'}${footer}` };
495
+ return {
496
+ ok: true,
497
+ file: rel,
498
+ warnings: verdict.warnings,
499
+ text: `Saved ${rel} (${plural(verdict.warnings.length, 'warning')}).${dropped}${spec.live ? ' It is live: xsolana_publish publishes only what changed.' : ' Next: xsolana_publish when the person says to put it live.'}${footer}`,
500
+ };
356
501
  },
357
502
  },
358
503
  {
359
504
  name: 'xsolana_publish',
360
505
  annotations: { ...READ, readOnlyHint: true },
361
- description:
362
- `When the person says to put it live: returns what going live does (every step, in order, and its cost) and the one command they run themselves to approve it in their own wallet (\`${approveCommand()}\`, pinned to this exact version: give it as it is): it opens a local page where they connect a devnet wallet and approve each transaction. You never sign, never ask for a key, and never run the approval for them.`,
506
+ description: `When the person says to put it live: returns what going live does (every step, in order, and its cost) and the one command they run themselves to approve it in their own wallet (\`${approveCommand()}\`, pinned to this exact version: give it as it is): it opens a local page where they connect a devnet wallet and approve each transaction. You never sign, never ask for a key, and never run the approval for them.`,
363
507
  inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
364
508
  async handler(input, ctx) {
365
509
  const { economy, from, file } = await load(input, ctx);
366
510
  const change = liveChange(economy);
367
- if (change.scope.live && !change.scope.only.size && change.settings.length) return { ok: false, transactions: 0, approvals: 0, steps: [], text: `${from} is live, and its parts are as they are on chain. ${settingsNote(change.settings, economy.live)}${footer}` };
368
- if (change.scope.live && !change.scope.only.size) return { ok: true, transactions: 0, approvals: 0, steps: [], text: `${from} is live, and nothing in it has changed since it went live: there is nothing to publish.${change.notes}${footer}` };
511
+ if (change.scope.live && !change.scope.only.size && change.settings.length)
512
+ return {
513
+ ok: false,
514
+ transactions: 0,
515
+ approvals: 0,
516
+ steps: [],
517
+ text: `${from} is live, and its parts are as they are on chain. ${settingsNote(change.settings, economy.live)}${footer}`,
518
+ };
519
+ if (change.scope.live && !change.scope.only.size)
520
+ return {
521
+ ok: true,
522
+ transactions: 0,
523
+ approvals: 0,
524
+ steps: [],
525
+ text: `${from} is live, and nothing in it has changed since it went live: there is nothing to publish.${change.notes}${footer}`,
526
+ };
369
527
  const p = plan(economy, { now: nowOf(ctx), ...change.scope });
370
528
  if (!p.ok) return refused(p, from);
371
529
  const c = cost(economy, { now: nowOf(ctx), ...change.scope });
372
- const steps = p.transactions.flatMap((t) => t.steps.map((s) => s.says));
530
+ const steps = p.transactions.flatMap(t => t.steps.map(s => s.says));
373
531
  // the file as the approve command names it, relative to the project folder (projectFile's), and plain (plainName)
374
532
  const target = file ? path.relative(realPath(path.resolve(cwdOf(ctx))), file) || DEFAULT_FILE : DEFAULT_FILE;
375
- if (plainName(target) === null) return { ok: false, transactions: 0, approvals: 0, steps: [], text: `Not published: ${NOT_PLAIN(target)}, and the approve command carries the file's name as it is. Ask the person to rename it (${RENAME}), then run xsolana_publish again with the new name.${footer}` };
533
+ if (plainName(target) === null)
534
+ return {
535
+ ok: false,
536
+ transactions: 0,
537
+ approvals: 0,
538
+ steps: [],
539
+ text: `Not published: ${NOT_PLAIN(target)}, and the approve command carries the file's name as it is. Ask the person to rename it (${RENAME}), then run xsolana_publish again with the new name.${footer}`,
540
+ };
376
541
  const command = approveCommand(target);
377
542
  const text = [
378
543
  ...flagsToTell(economy, nowOf(ctx)),
@@ -381,21 +546,34 @@ export const tools = [
381
546
  '',
382
547
  `To put it live, the person runs this in a terminal in the project folder (not you): ${command}`,
383
548
  'It opens a page on their own computer; they connect a wallet set to devnet and approve. What lands is written back into the file.',
384
- 'Tell them too, as the page does: the program is not independently audited and comes as is, with no warranty; they use it at their own risk, and making sure the economy is legal where it is offered is up to them (DISCLAIMER.md in the package).',
549
+ 'The page also says the program is not independently audited and comes as is, with no warranty; they use it at their own risk, and making sure the economy is legal where it is offered is up to them (DISCLAIMER.md in the package). They tick that they read it, so it needs no repeating unless they ask.',
385
550
  ...(file ? [] : [`(Save it first with xsolana_save: the approval reads ${DEFAULT_FILE}.)`]),
386
551
  ...(change.notes ? [change.notes.trim()] : []),
387
552
  ].join('\n');
388
- return { ok: true, command, transactions: p.transactions.length, approvals: p.approvals, setupSol: c.setup.totalSol, steps, text: text + footer };
553
+ return {
554
+ ok: true,
555
+ command,
556
+ transactions: p.transactions.length,
557
+ approvals: p.approvals,
558
+ setupSol: c.setup.totalSol,
559
+ steps,
560
+ text: text + footer,
561
+ };
389
562
  },
390
563
  },
391
564
  {
392
565
  name: 'xsolana_status',
393
566
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
394
- description: 'A live economy as it is on chain: the project, every pool’s balance, every record the spec names (found or missing). Reads devnet (or XSOLANA_RPC_URL).',
567
+ description:
568
+ 'A live economy as it is on chain: the project, every pool’s balance, every record the spec names (found or missing). Reads devnet (or XSOLANA_RPC_URL).',
395
569
  inputSchema: { type: 'object', properties: { file: SOURCE.file }, additionalProperties: false },
396
570
  async handler(input, ctx) {
397
571
  const { economy, from } = await load(input, ctx);
398
- if (!economy.live?.project) return { ok: false, text: `${from} is not live yet (no "live" section). xsolana_publish says how to put it live.${footer}` };
572
+ if (!economy.live?.project)
573
+ return {
574
+ ok: false,
575
+ text: `${from} is not live yet (no "live" section). xsolana_publish says how to put it live.${footer}`,
576
+ };
399
577
  const m = model(economy);
400
578
  // Devnet only: a mainnet URL is refused, and so is a chain whose first block is mainnet's or testnet's.
401
579
  const rpc = (ctx?.rpc ?? rpcClient)(notMainnet(process.env.XSOLANA_RPC_URL || DEVNET_RPC));
@@ -403,38 +581,72 @@ export const tools = [
403
581
  const project = economy.live.project;
404
582
  const wanted = [
405
583
  ['project', project],
406
- ...m.pools.map((p) => [`pool:${p.key}`, pda.pool(project, p.index)]),
407
- ...m.allBoxes.map((b) => [b.shopOf ? `item:${b.shopOf} (shop)` : `box:${b.key}`, pda.box(project, b.id)]),
408
- ...m.items.map((it) => [`item:${it.key}`, pda.item(project, it.id)]),
409
- ...m.farms.map((f) => [`farm:${f.key}`, pda.farm(project, f.id)]),
410
- ...m.lotteries.map((l) => [`lottery:${l.key}`, pda.lottery(project, l.id)]),
411
- ...m.schedules.map((s) => [`schedule:${s.key}`, pda.schedule(project, s.id)]),
412
- ...m.recipes.map((r) => [`recipe:${r.key}`, pda.recipe(project, r.id)]),
413
- ...m.grants.map((g) => [`grant:${g.key}`, pda.grant(project, g.id)]),
414
- ...m.stashes.map((s) => [`stash:${s.key}`, pda.stash(project, s.id)]),
415
- ...(economy.auctions ?? []).filter((a) => a.seller).map((a) => [`auction:${a.key}`, pda.auction(project, a.seller, a.id)]),
584
+ ...m.pools.map(p => [`pool:${p.key}`, pda.pool(project, p.index)]),
585
+ ...m.allBoxes.map(b => [b.shopOf ? `item:${b.shopOf} (shop)` : `box:${b.key}`, pda.box(project, b.id)]),
586
+ ...m.items.map(it => [`item:${it.key}`, pda.item(project, it.id)]),
587
+ ...m.farms.map(f => [`farm:${f.key}`, pda.farm(project, f.id)]),
588
+ ...m.lotteries.map(l => [`lottery:${l.key}`, pda.lottery(project, l.id)]),
589
+ ...m.lotteries
590
+ .filter(l => Number.isInteger(l.pot))
591
+ .map(l => [`lottery:${l.key} (pot ${l.pot})`, pda.lotteryPot(pda.lottery(project, l.id), l.pot)]),
592
+ ...m.schedules.map(s => [`schedule:${s.key}`, pda.schedule(project, s.id)]),
593
+ ...m.recipes.map(r => [`recipe:${r.key}`, pda.recipe(project, r.id)]),
594
+ ...m.grants.map(g => [`grant:${g.key}`, pda.grant(project, g.id)]),
595
+ ...m.stashes.map(s => [`stash:${s.key}`, pda.stash(project, s.id)]),
596
+ ...(economy.auctions ?? [])
597
+ .filter(a => a.seller)
598
+ .map(a => [`auction:${a.key}`, pda.auction(project, a.seller, a.id)]),
416
599
  ];
417
600
  const accounts = await rpc.accounts(wanted.map(([, a]) => a));
418
601
  const rows = wanted.map(([part, address], i) => {
419
602
  const a = accounts[i];
420
603
  if (!a) return { part, address, found: false };
421
604
  // A grant's funds are the SOL it holds beyond its rent: what is left to pay its claims' fees and rent.
422
- if (part.startsWith('grant:')) return { part, address, found: true, kind: 'Grant', sol: Number(BigInt(a.lamports ?? 0) - rentOf(a.data?.length ?? 0)) / 1e9 };
423
- const tokenAmount = a.data?.length >= 72 && part.startsWith('pool:') ? Number(new DataView(a.data.buffer, a.data.byteOffset).getBigUint64(64, true)) : undefined;
605
+ if (part.startsWith('grant:'))
606
+ return {
607
+ part,
608
+ address,
609
+ found: true,
610
+ kind: 'Grant',
611
+ sol: Number(BigInt(a.lamports ?? 0) - rentOf(a.data?.length ?? 0)) / 1e9,
612
+ };
613
+ const tokenAmount =
614
+ a.data?.length >= 72 && (part.startsWith('pool:') || part.includes(' (pot '))
615
+ ? Number(new DataView(a.data.buffer, a.data.byteOffset).getBigUint64(64, true))
616
+ : undefined;
424
617
  const decoded = a.owner === LOOT_PROGRAM ? decodeAny(a.data) : null;
425
618
  // A stash: how many NFTs it still holds to give away.
426
619
  const nfts = decoded?.kind === 'Stash' ? decoded.value.nfts.length : undefined;
427
- return { part, address, found: true, ...(tokenAmount !== undefined ? { amount: tokenAmount } : {}), ...(nfts !== undefined ? { nfts } : {}), ...(decoded ? { kind: decoded.kind } : {}) };
620
+ return {
621
+ part,
622
+ address,
623
+ found: true,
624
+ ...(tokenAmount !== undefined ? { amount: tokenAmount } : {}),
625
+ ...(nfts !== undefined ? { nfts } : {}),
626
+ ...(decoded ? { kind: decoded.kind } : {}),
627
+ };
428
628
  });
429
- const missing = rows.filter((r) => !r.found);
430
- const text = [`${economy.name ?? 'The economy'} on ${CLUSTER}: project ${project}.`, ...rows.map((r) => `- ${r.part}: ${r.found ? `found${r.amount !== undefined ? `, holds ${r.amount} base units` : ''}${r.nfts !== undefined ? `, holds ${r.nfts} NFT${r.nfts === 1 ? '' : 's'} to give away` : ''}${r.sol !== undefined ? `, holds ${r.sol} SOL for claims` : ''}` : 'missing'} (${r.address})`), missing.length ? `\n${plural(missing.length, 'part')} not on chain: publish again (xsolana_publish) to add them.` : ''].filter(Boolean).join('\n');
629
+ const missing = rows.filter(r => !r.found);
630
+ const text = [
631
+ `${economy.name ?? 'The economy'} on ${CLUSTER}: project ${project}.`,
632
+ ...rows.map(
633
+ r =>
634
+ `- ${r.part}: ${r.found ? `found${r.amount !== undefined ? `, holds ${r.amount} base units` : ''}${r.nfts !== undefined ? `, holds ${r.nfts} NFT${r.nfts === 1 ? '' : 's'} to give away` : ''}${r.sol !== undefined ? `, holds ${r.sol} SOL for claims` : ''}` : 'missing'} (${r.address})`,
635
+ ),
636
+ missing.length
637
+ ? `\n${plural(missing.length, 'part')} not on chain: publish again (xsolana_publish) to add them.`
638
+ : '',
639
+ ]
640
+ .filter(Boolean)
641
+ .join('\n');
431
642
  return { ok: true, rows, text: text + footer };
432
643
  },
433
644
  },
434
645
  {
435
646
  name: 'xsolana_client',
436
647
  annotations: READ,
437
- description: 'How an app (a game, a site, a bot) uses a live economy: the program’s address and cluster, the npm package and IDL, and the calls a page makes with the player’s own wallet (open a box, buy, stake, craft, enter, claim). Use this instead of writing chain code from memory.',
648
+ description:
649
+ 'How an app (a game, a site, a bot) uses a live economy: the program’s address and cluster, the npm package and IDL, and the calls a page makes with the player’s own wallet (open a box, buy, stake, craft, enter, claim). Use this instead of writing chain code from memory.',
438
650
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
439
651
  async handler() {
440
652
  const text = [
@@ -449,8 +661,10 @@ export const tools = [
449
661
  ' app.collectibles(), app.balances() what a player does. `listing`: the entry shop(), boxes(), lotteries() or recipes() showed the',
450
662
  ' player; its price (or version) is the most they pay: one that moved since is refused, never paid unseen',
451
663
  ' a prize (a grant, a lottery place, a contest win, a mission, a recipe) is given in the instruction that wins it,',
452
- ' its accounts from loot.prizeMetas({ project, collection, prize, winner, poolMints, items, nft }) (a roll a buy, a craft',
453
- ' or a grant claim makes: strict: true, boxes, rollPools, for the box’s prize mints, loot.rollMints); app.claims() finds',
664
+ ' its accounts from loot.prizeMetas({ project, collection, prize, winner, poolMints, items, nft }) (a roll: its box and',
665
+ ' its order; ask for it in the same transaction, loot.askRollIx, which fails the whole transaction, SoldOut, when the box',
666
+ ' has nothing in stock (only the order’s player asks then, a slot later, and that ends the roll with nothing); a write naming a',
667
+ ' box to roll passes loot.boxesAccounts after it, and the box is written first); app.claims() finds',
454
668
  ' what a player is owed. A roll (a prize of a roll, an auction lot, a box) is an order: app.claim / app.collect, or',
455
669
  ' loot.askRollIx({ payer, orderAddress, order, box }) then loot.settleIx(...) hands over what it rolled (a stash NFT too)',
456
670
  ' loot.stashIx({ signer, project, id, asset, out }) the owner or authority puts an NFT in a stash, or takes one out',
@@ -458,20 +672,25 @@ export const tools = [
458
672
  ' app.buyListing(asset, { listing }), app.rentListing(asset, { periods, listing }) (`listing`: what listing() or listings()',
459
673
  ' showed the player), app.unlist(asset), app.rentedTo(asset) (one account read) listings, sale and rent',
460
674
  ' app.raffle(id, nft, { price, drawsAt, minSold }), app.unraffle(id, nft) a raffle from the lottery’s own stash',
461
- ' app.compound(key), app.withdraw(key), app.runSchedule(key) compound, take out unbonded tokens, catch a schedule up',
462
- ' app.claims() also lists a raffle’s prize step (given before the money), unbonded tokens, vested runs, a rental over',
675
+ ' app.compound(key), app.runSchedule(key) compound, send a schedule that is due (a run missed is missed: only a sealed fixed amount pays every run due)',
676
+ ' app.farmWalk(key), app.walkFarm(key) a farm idle past many end times: how many it must walk, and walking them in parts (anyone, free; moves ask for the compute to walk a few themselves)',
677
+ ' app.claims() also lists a lottery’s parts to pull (a refund, a place, a raffle’s NFT), vested runs, a rental over',
463
678
  'Lower level, for what the player module does not cover:',
464
679
  ' import * as loot from "@xgamesfun/solana/client";',
465
680
  ' loot.pda.project(owner, number), loot.pda.box(project, id), … every address the program derives',
466
681
  ' loot.openBoxIx({ player, project, box, nonce }), loot.buyIx({ player, project, box, items }) … one builder per instruction; each returns { programId, keys, data }',
467
682
  ' loot.payNftIx({ player, project, asset, to }) pay with an NFT (burned, or given to `to`): the app admits by the receipt',
468
683
  ' loot.openListingIx / takeListingIx / closeListingIx, loot.listingTerms(listing), loot.pda.listing(project, asset), loot.renterOf(listing, now)',
469
- ' loot.lotteryStep(lottery, contribution) then loot.finishLotteryContributionIx(...) a drawn round’s prizes first, then its money',
470
- ' loot.withdrawable(seat, now), loot.runsDue(schedule, now, vaultBalance), loot.pda.vest(schedule), loot.pda.prizeVault(contest), loot.pda.lotteryStash(lottery)',
684
+ ' a take or a bid signs the record’s terms: loot.listingTerms / offerTerms / auctionTerms(record), the hash of its bytes as read, but what a take changes',
685
+ ' loot.decodeRound(data), loot.partsOwed(round, entry), loot.commonOwed(round), loot.pullIx(...) a round’s parts, each pulled in any order (a place’s roll: loot.pullRolls, asked in the same transaction)',
686
+ ' loot.drawLotteryIx({ ..., lottery, round, owing }) a round paid from a pot first moves in what the round before left it (`lottery.owing`, its record as `owing`); loot.pda.lotteryPot(lottery, id), loot.potIdOf(terms), loot.leftoverTo(lottery, terms)',
687
+ ' loot.decodeEnds(data), loot.endsToWalk(farm, ends, now), loot.farmNeedsWalk(farm, ends, now), loot.walkFarmParts({ project, id, farm, ends, now }), loot.pda.ends(farm) a farm’s record of when seats stop earning',
688
+ ' loot.orderRefundable(order, slot, now), loot.rollAskable(order, slot, now), loot.lotteryDrawStuck(round, now), loot.lotteryCancellable(round, now) deadlines, by the chain’s clock',
689
+ ' loot.runsDue(schedule, now, vaultBalance), loot.scheduleCatchesUp(schedule), loot.pda.vest(schedule), loot.pda.prizeVault(contest), loot.pda.lotteryStash(lottery), loot.pda.rewards(farm)',
471
690
  ' loot.decode("LootBox", data), loot.receipts(tx) accounts, and the receipts of a getTransaction result (none when meta.err is set)',
472
691
  'IDL: @xgamesfun/solana/idl.json (Anchor 1.x format).',
473
692
  'A page builds the instruction, puts it in a transaction, and asks the player’s wallet (Wallet Standard: Phantom, Solflare, Backpack) to sign and send it. The app never holds a key.',
474
- 'Reads: the project’s records are plain accounts (loot.decode); every move leaves a receipt in the transaction’s logs (loot.receipts(tx)). A backend delivers only from a finalized transaction whose meta.err is null, once per order or reference: a failed transaction keeps its logs, and they can read as all-success.',
693
+ 'Reads: the project’s records are plain accounts (loot.decode); every move leaves a receipt in the transaction’s logs (loot.receipts(tx)). A backend delivers only from a finalized transaction whose meta.err is null, once per order or reference, and only for a receipt whose data.project is its own (loot.receipts reads every project’s, and anyone can make a project): a failed transaction keeps its logs, and they can read as all-success.',
475
694
  ].join('\n');
476
695
  return { text: text + footer, ...PROGRAM_INFO };
477
696
  },