@xgamesfun/solana 0.24.0 → 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.
- package/README.md +94 -51
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +2 -5
- package/examples/duel-arena.json +1 -4
- package/examples/fair-presale.json +1 -3
- package/examples/hero-trials.json +14 -2
- package/examples/holder-den.json +38 -0
- package/examples/holders-drop.json +105 -0
- package/examples/ideas.json +788 -788
- package/examples/match-predictions.json +24 -0
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/partner-vault.json +19 -3
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +3 -7
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +117 -48
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +276 -74
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +15 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +21 -5
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +118 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +40 -14
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +270 -80
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +165 -40
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +104 -16
- package/js/client/ix.js +2121 -457
- package/js/client/pda.js +20 -18
- package/js/client/policy.js +12 -5
- package/js/client/random.js +1 -1
- package/js/client/sizes.js +220 -65
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +3873 -690
- package/js/economy/cost.mjs +380 -56
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +461 -110
- package/js/economy/guide.mjs +282 -59
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +60 -19
- package/js/economy/model.mjs +320 -97
- package/js/economy/plan.mjs +1022 -203
- package/js/economy/schema.json +127 -369
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +771 -178
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/mcp/server.mjs +42 -14
- package/js/mcp/tools.mjs +293 -80
- package/js/play/index.js +1960 -461
- package/js/play/keys.js +35 -9
- package/js/results/index.js +223 -30
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +30 -16
- package/skills/xsolana-economy/references/after-launch.md +30 -20
- package/skills/xsolana-economy/references/apps.md +47 -36
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -129
- package/skills/xsolana-economy/references/telling-the-person.md +9 -6
- package/types/chain/core.d.ts +1 -1
- package/types/chain/lookup.d.ts +1 -1
- package/types/client/accounts.d.ts +24 -3
- package/types/client/grant.d.ts +2 -2
- package/types/client/idl/loot.d.ts +22 -0
- package/types/client/ix.d.ts +54 -45
- package/types/client/pda.d.ts +3 -1
- package/types/economy/guide.d.mts +5 -0
- package/types/economy/model.d.mts +48 -3
- package/types/economy/plan.d.mts +44 -2
- package/types/play/index.d.ts +8 -19
- package/types/results/index.d.ts +7 -6
- package/types/results/server.d.ts +126 -0
- 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 =
|
|
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({
|
|
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 =
|
|
62
|
-
const cwdOf =
|
|
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 =
|
|
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)))
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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: {
|
|
113
|
-
|
|
114
|
-
|
|
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(
|
|
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(
|
|
161
|
-
out[list] = next[list].map(
|
|
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(
|
|
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
|
};
|
|
@@ -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
|
|
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
|
|
195
|
-
|
|
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: {
|
|
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 {
|
|
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 {
|
|
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 }) => ({
|
|
238
|
-
|
|
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
|
|
243
|
-
|
|
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(
|
|
246
|
-
r.ideas.length
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
...r.
|
|
250
|
-
r.
|
|
251
|
-
|
|
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
|
|
266
|
-
|
|
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: {
|
|
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(
|
|
283
|
-
|
|
284
|
-
|
|
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 {
|
|
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:
|
|
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
|
|
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:
|
|
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: {
|
|
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)
|
|
330
|
-
|
|
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))
|
|
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 =
|
|
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)
|
|
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 {
|
|
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 {
|
|
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)
|
|
368
|
-
|
|
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(
|
|
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)
|
|
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
|
-
'
|
|
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 {
|
|
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:
|
|
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)
|
|
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(
|
|
407
|
-
...m.allBoxes.map(
|
|
408
|
-
...m.items.map(
|
|
409
|
-
...m.farms.map(
|
|
410
|
-
...m.lotteries.map(
|
|
411
|
-
...m.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
...m.
|
|
415
|
-
...
|
|
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:'))
|
|
423
|
-
|
|
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 {
|
|
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(
|
|
430
|
-
const text = [
|
|
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:
|
|
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 = [
|
|
@@ -471,6 +683,7 @@ export const tools = [
|
|
|
471
683
|
' loot.openListingIx / takeListingIx / closeListingIx, loot.listingTerms(listing), loot.pda.listing(project, asset), loot.renterOf(listing, now)',
|
|
472
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',
|
|
473
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)',
|
|
474
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',
|
|
475
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',
|
|
476
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)',
|