@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.
- package/DISCLAIMER.md +1 -1
- package/NOTICE +4 -4
- package/README.md +100 -52
- package/bin/xsolana.mjs +32 -11
- 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/ideas.json +788 -788
- package/examples/nft-raffle.json +1 -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 +4 -7
- 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 +2 -2
- 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 +3884 -690
- package/js/economy/cost.mjs +381 -57
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +468 -110
- package/js/economy/guide.mjs +282 -59
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +62 -20
- package/js/economy/model.mjs +320 -97
- package/js/economy/plan.mjs +1022 -203
- package/js/economy/review.mjs +760 -0
- package/js/economy/schema.json +134 -372
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +771 -178
- package/js/economy/trust.mjs +15 -0
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/index.js +1 -0
- package/js/mcp/server.mjs +43 -15
- package/js/mcp/tools.mjs +318 -80
- package/js/play/index.js +1960 -461
- package/js/play/keys.js +35 -9
- package/js/results/index.js +225 -32
- 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 +52 -22
- package/skills/xsolana-economy/references/after-launch.md +32 -21
- package/skills/xsolana-economy/references/apps.md +47 -36
- package/skills/xsolana-economy/references/sentence-to-spec.md +149 -129
- package/skills/xsolana-economy/references/telling-the-person.md +15 -6
- package/skills/xsolana-economy/references/trust-and-cheating.md +126 -0
- 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/cost.d.mts +1 -1
- package/types/economy/explain.d.mts +3 -0
- 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/economy/review.d.mts +45 -0
- package/types/economy/trust.d.mts +8 -0
- package/types/index.d.ts +1 -0
- package/types/mcp/tools.d.mts +62 -0
- package/types/play/index.d.ts +14 -25
- package/types/results/index.d.ts +8 -7
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
package/js/mcp/tools.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { cost, explainCost } from '../economy/cost.mjs';
|
|
|
15
15
|
import { changedParts } from '../economy/live.mjs';
|
|
16
16
|
import { example, examples, ideas, search, partsUsed } from '../economy/examples.mjs';
|
|
17
17
|
import { explainEconomy } from '../economy/explain.mjs';
|
|
18
|
+
import { describeReview, review } from '../economy/review.mjs';
|
|
18
19
|
import { CANNOT, catalog, guideText, PARTS } from '../economy/guide.mjs';
|
|
19
20
|
import { model, rent as rentOf } from '../economy/model.mjs';
|
|
20
21
|
import { plan } from '../economy/plan.mjs';
|
|
@@ -40,7 +41,7 @@ export function plainName(rel, paths = path) {
|
|
|
40
41
|
const name = String(rel).split(paths.sep).join('/');
|
|
41
42
|
return SAVE_NAME.test(name) ? name : null;
|
|
42
43
|
}
|
|
43
|
-
const NOT_PLAIN =
|
|
44
|
+
const NOT_PLAIN = rel => `${JSON.stringify(String(rel))} is not a plain file name`;
|
|
44
45
|
const RENAME = 'use letters, digits and . _ - (and / for a folder), as in xsolana.json';
|
|
45
46
|
/**
|
|
46
47
|
* The command an owner runs to approve going live in their own wallet, pinned to this package's exact version, with
|
|
@@ -55,11 +56,17 @@ export function approveCommand(file = '', paths = path) {
|
|
|
55
56
|
export const CLUSTER = 'devnet';
|
|
56
57
|
export const DEVNET_RPC = 'https://api.devnet.solana.com';
|
|
57
58
|
/** What every answer carries, so an agent never has to remember the program's address. */
|
|
58
|
-
export const PROGRAM_INFO = Object.freeze({
|
|
59
|
+
export const PROGRAM_INFO = Object.freeze({
|
|
60
|
+
program: LOOT_PROGRAM,
|
|
61
|
+
cluster: CLUSTER,
|
|
62
|
+
version: VERSION,
|
|
63
|
+
idl: '@xgamesfun/solana/idl.json',
|
|
64
|
+
package: `@xgamesfun/solana@${PACKAGE_VERSION}`,
|
|
65
|
+
});
|
|
59
66
|
const footer = `\n\nloot ${VERSION} · program ${LOOT_PROGRAM} · ${CLUSTER}`;
|
|
60
67
|
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
61
|
-
const nowOf =
|
|
62
|
-
const cwdOf =
|
|
68
|
+
const nowOf = ctx => ctx?.now ?? Math.floor(Date.now() / 1000);
|
|
69
|
+
const cwdOf = ctx => ctx?.cwd ?? process.cwd();
|
|
63
70
|
export const DEFAULT_FILE = 'xsolana.json';
|
|
64
71
|
|
|
65
72
|
/** The real path of `p`, or of the nearest folder above it that exists, with the rest added back. */
|
|
@@ -79,22 +86,24 @@ function realPath(p) {
|
|
|
79
86
|
export function projectFile(rel, ctx) {
|
|
80
87
|
const root = realPath(path.resolve(cwdOf(ctx)));
|
|
81
88
|
const file = path.resolve(root, String(rel));
|
|
82
|
-
const inside =
|
|
89
|
+
const inside = p => {
|
|
83
90
|
const r = path.relative(root, p);
|
|
84
91
|
return r !== '' && !r.startsWith('..') && !path.isAbsolute(r);
|
|
85
92
|
};
|
|
86
|
-
if (!inside(file) || !inside(realPath(file)))
|
|
93
|
+
if (!inside(file) || !inside(realPath(file)))
|
|
94
|
+
throw new Error(`${rel} is outside the project folder (${root}): a spec file must be inside it.`);
|
|
87
95
|
if (path.extname(file).toLowerCase() !== '.json') throw new Error(`${rel} is not a .json file: a spec file is JSON.`);
|
|
88
96
|
return file;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
/** The spec's own top-level keys (schema.json). */
|
|
92
100
|
const SPEC_KEYS = new Set(Object.keys(SCHEMA.properties));
|
|
93
|
-
const plainObject =
|
|
101
|
+
const plainObject = v => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
94
102
|
/** A file's spec: the JSON as it is, or what it holds under `economy` when that is its only key. */
|
|
95
|
-
const specOf =
|
|
103
|
+
const specOf = value =>
|
|
104
|
+
plainObject(value) && Object.keys(value).length === 1 && plainObject(value.economy) ? value.economy : value;
|
|
96
105
|
/** 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 =
|
|
106
|
+
const economyOf = value => (plainObject(value) && plainObject(value.economy) ? value.economy : value);
|
|
98
107
|
/**
|
|
99
108
|
* 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
109
|
* checks has them), a currency if it names one (a spec of one token checks without: that token is its currency)
|
|
@@ -103,15 +112,30 @@ const economyOf = (value) => (plainObject(value) && plainObject(value.economy) ?
|
|
|
103
112
|
*/
|
|
104
113
|
export function isSpec(value) {
|
|
105
114
|
const s = specOf(value);
|
|
106
|
-
return
|
|
115
|
+
return (
|
|
116
|
+
plainObject(s) &&
|
|
117
|
+
s.version === 2 &&
|
|
118
|
+
(s.currency === undefined || typeof s.currency === 'string') &&
|
|
119
|
+
Array.isArray(s.tokens) &&
|
|
120
|
+
Object.keys(s).every(k => SPEC_KEYS.has(k))
|
|
121
|
+
);
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
// ---------------------------------------------------------------- where a spec comes from
|
|
110
125
|
|
|
111
126
|
const SOURCE = {
|
|
112
|
-
spec: {
|
|
113
|
-
|
|
114
|
-
|
|
127
|
+
spec: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
description: 'An economy spec (version 2) given inline. Takes precedence over file and example.',
|
|
130
|
+
},
|
|
131
|
+
file: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: `A JSON file holding the spec, relative to the project folder (default: ${DEFAULT_FILE} when it exists).`,
|
|
134
|
+
},
|
|
135
|
+
example: {
|
|
136
|
+
type: 'string',
|
|
137
|
+
description: 'A worked example by name, from xsolana_examples (coin-flip-arena, stud-farm, …).',
|
|
138
|
+
},
|
|
115
139
|
};
|
|
116
140
|
|
|
117
141
|
async function load(input, ctx) {
|
|
@@ -121,7 +145,9 @@ async function load(input, ctx) {
|
|
|
121
145
|
const file = projectFile(rel, ctx);
|
|
122
146
|
if (!existsSync(file)) {
|
|
123
147
|
if (input.file) throw new Error(`${rel} does not exist.`);
|
|
124
|
-
throw new Error(
|
|
148
|
+
throw new Error(
|
|
149
|
+
`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.`,
|
|
150
|
+
);
|
|
125
151
|
}
|
|
126
152
|
let value;
|
|
127
153
|
try {
|
|
@@ -157,11 +183,13 @@ export function carryIds(previous, next) {
|
|
|
157
183
|
const out = { ...next, live: previous.live };
|
|
158
184
|
const carry = (list, fields) => {
|
|
159
185
|
if (!next[list]) return;
|
|
160
|
-
const old = new Map((previous[list] ?? []).map(
|
|
161
|
-
out[list] = next[list].map(
|
|
186
|
+
const old = new Map((previous[list] ?? []).map(p => [p.key, p]));
|
|
187
|
+
out[list] = next[list].map(p => {
|
|
162
188
|
const was = old.get(p.key);
|
|
163
189
|
if (!was) return p;
|
|
164
|
-
const ids = Object.fromEntries(
|
|
190
|
+
const ids = Object.fromEntries(
|
|
191
|
+
fields.filter(f => was[f] !== undefined && p[f] === undefined).map(f => [f, was[f]]),
|
|
192
|
+
);
|
|
165
193
|
return { ...p, ...ids };
|
|
166
194
|
});
|
|
167
195
|
};
|
|
@@ -179,7 +207,13 @@ export function carryIds(previous, next) {
|
|
|
179
207
|
/** The legal flags to pass on before the person approves, as lines for an answer: [] when there are none. */
|
|
180
208
|
function flagsToTell(economy, now) {
|
|
181
209
|
const flags = legalFlags(check(economy, { now }));
|
|
182
|
-
return flags.length
|
|
210
|
+
return flags.length
|
|
211
|
+
? [
|
|
212
|
+
"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):",
|
|
213
|
+
...flags.map(f => `- ${f.says}`),
|
|
214
|
+
'',
|
|
215
|
+
]
|
|
216
|
+
: [];
|
|
183
217
|
}
|
|
184
218
|
|
|
185
219
|
/**
|
|
@@ -191,8 +225,12 @@ function liveChange(economy) {
|
|
|
191
225
|
const { only, fixed, removed, settings } = changedParts(economy);
|
|
192
226
|
const notes = [
|
|
193
227
|
settings.length ? `\n${settingsNote(settings, economy.live)}` : '',
|
|
194
|
-
fixed.length
|
|
195
|
-
|
|
228
|
+
fixed.length
|
|
229
|
+
? `\nThese can't change once live, and stay as they were: ${fixed.join(', ')} (a farm, an auction or a pool's token).`
|
|
230
|
+
: '',
|
|
231
|
+
removed.length
|
|
232
|
+
? `\nTaken out of the spec but still on chain: ${removed.join(', ')}. To stop one, keep it and set "active": false.`
|
|
233
|
+
: '',
|
|
196
234
|
].join('');
|
|
197
235
|
return { scope: { only, live: economy.live }, notes, settings };
|
|
198
236
|
}
|
|
@@ -207,16 +245,26 @@ export const tools = [
|
|
|
207
245
|
annotations: READ,
|
|
208
246
|
description:
|
|
209
247
|
'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: {
|
|
248
|
+
inputSchema: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
part: { type: 'string', enum: Object.keys(PARTS), description: 'One part, with its nested shapes.' },
|
|
252
|
+
},
|
|
253
|
+
additionalProperties: false,
|
|
254
|
+
},
|
|
211
255
|
async handler({ part } = NO_OPTIONS) {
|
|
212
|
-
return {
|
|
256
|
+
return {
|
|
257
|
+
text: guideText(part) + footer,
|
|
258
|
+
parts: catalog(part),
|
|
259
|
+
...(part ? {} : { cannot: CANNOT }),
|
|
260
|
+
...PROGRAM_INFO,
|
|
261
|
+
};
|
|
213
262
|
},
|
|
214
263
|
},
|
|
215
264
|
{
|
|
216
265
|
name: 'xsolana_examples',
|
|
217
266
|
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.`,
|
|
267
|
+
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
268
|
inputSchema: {
|
|
221
269
|
type: 'object',
|
|
222
270
|
properties: {
|
|
@@ -231,24 +279,53 @@ export const tools = [
|
|
|
231
279
|
if (name) {
|
|
232
280
|
const ex = example(name);
|
|
233
281
|
const verdict = check(ex.spec, { now: nowOf(ctx) });
|
|
234
|
-
return {
|
|
282
|
+
return {
|
|
283
|
+
name: ex.name,
|
|
284
|
+
spec: ex.spec,
|
|
285
|
+
checks: verdict.ok ? 'clean' : verdict.refusals.map(r => r.code),
|
|
286
|
+
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}`,
|
|
287
|
+
};
|
|
235
288
|
}
|
|
236
289
|
if (!query) {
|
|
237
|
-
const all = examples().map(({ name: n, spec }) => ({
|
|
238
|
-
|
|
290
|
+
const all = examples().map(({ name: n, spec }) => ({
|
|
291
|
+
name: n,
|
|
292
|
+
title: spec.name,
|
|
293
|
+
prompt: spec.prompt,
|
|
294
|
+
parts: partsUsed(spec),
|
|
295
|
+
}));
|
|
296
|
+
return {
|
|
297
|
+
examples: all,
|
|
298
|
+
ideas: { count: ideas().length },
|
|
299
|
+
text:
|
|
300
|
+
[
|
|
301
|
+
`${all.length} worked examples (xsolana_examples {"name": "<name>"} gives the whole spec):`,
|
|
302
|
+
...all.map(e => `- ${e.name}: ${e.title}. ${e.prompt ?? ''} [${e.parts.join(', ')}]`),
|
|
303
|
+
`\nAnd ${ideas().length} rated economies: pass a query to search them.`,
|
|
304
|
+
].join('\n') + footer,
|
|
305
|
+
};
|
|
239
306
|
}
|
|
240
307
|
const r = search(query, { limit, fit });
|
|
241
308
|
const text = [
|
|
242
|
-
r.examples.length
|
|
243
|
-
|
|
309
|
+
r.examples.length
|
|
310
|
+
? `Examples closest to “${query}” (xsolana_examples {"name": …} gives the whole spec):`
|
|
311
|
+
: `No worked example matches “${query}”. Start from the nearest idea below, or from xsolana_guide.`,
|
|
312
|
+
...r.examples.map(e => `- ${e.name}: ${e.title}. ${e.prompt ?? ''} [${e.parts.join(', ')}]`),
|
|
244
313
|
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
|
-
|
|
314
|
+
...r.hows.map(h => `- ${h.what}: ${h.how}.`),
|
|
315
|
+
r.ideas.length
|
|
316
|
+
? '\nRated economies (rated against this version of loot: a rating is a hint, the parts in xsolana_guide decide):'
|
|
317
|
+
: '',
|
|
318
|
+
...r.ideas.map(i => `- #${i.n} ${i.name} (${i.fit}): ${i.how}`),
|
|
319
|
+
r.limits.length
|
|
320
|
+
? '\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”):'
|
|
321
|
+
: '',
|
|
322
|
+
...r.limits.flatMap(l => [`- ${l.what}: ${l.why}.`, ...l.nearest.map((n, i) => ` ${i + 1}. ${n}`)]),
|
|
323
|
+
r.ideas.some(i => i.fit === 'cannot')
|
|
324
|
+
? '\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.'
|
|
325
|
+
: '',
|
|
326
|
+
]
|
|
327
|
+
.filter(Boolean)
|
|
328
|
+
.join('\n');
|
|
252
329
|
return { examples: r.examples, ideas: r.ideas, hows: r.hows, limits: r.limits, text: text + footer };
|
|
253
330
|
},
|
|
254
331
|
},
|
|
@@ -262,8 +339,16 @@ export const tools = [
|
|
|
262
339
|
const { economy, from } = await load(input, ctx);
|
|
263
340
|
const verdict = check(economy, { now: nowOf(ctx) });
|
|
264
341
|
const head = verdict.refusals.length || verdict.warnings.length ? describe(verdict) : 'No problems found.';
|
|
265
|
-
const tail = verdict.ok
|
|
266
|
-
|
|
342
|
+
const tail = verdict.ok
|
|
343
|
+
? `\nReady (${plural(verdict.warnings.length, 'warning')}). Next: xsolana_simulate and xsolana_explain, and tell the person what it means in plain words; before going live, xsolana_review.`
|
|
344
|
+
: `\n${plural(verdict.refusals.length, 'refusal')}: fix each as its fix says, then check again.`;
|
|
345
|
+
return {
|
|
346
|
+
ok: verdict.ok,
|
|
347
|
+
from,
|
|
348
|
+
refusals: verdict.refusals,
|
|
349
|
+
warnings: verdict.warnings,
|
|
350
|
+
text: `${head}${tail}${footer}`,
|
|
351
|
+
};
|
|
267
352
|
},
|
|
268
353
|
},
|
|
269
354
|
{
|
|
@@ -271,7 +356,15 @@ export const tools = [
|
|
|
271
356
|
annotations: READ,
|
|
272
357
|
description:
|
|
273
358
|
'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: {
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: 'object',
|
|
361
|
+
properties: {
|
|
362
|
+
...SOURCE,
|
|
363
|
+
scenario: { type: 'object', description: 'What to assume instead of the spec’s scenario.' },
|
|
364
|
+
json: { type: 'boolean', description: 'Also return every row.' },
|
|
365
|
+
},
|
|
366
|
+
additionalProperties: false,
|
|
367
|
+
},
|
|
275
368
|
async handler(input, ctx) {
|
|
276
369
|
const { economy: e, from } = await load(input, ctx);
|
|
277
370
|
const economy = input.scenario ? { ...e, scenario: { ...e.scenario, ...input.scenario } } : e;
|
|
@@ -279,32 +372,48 @@ export const tools = [
|
|
|
279
372
|
if (!verdict.ok) return refused(verdict, from);
|
|
280
373
|
const sim = simulate(economy, { now: nowOf(ctx) });
|
|
281
374
|
const summary = {
|
|
282
|
-
boxes: sim.boxes.map(
|
|
283
|
-
|
|
284
|
-
|
|
375
|
+
boxes: sim.boxes.map(b => ({
|
|
376
|
+
key: b.key,
|
|
377
|
+
returnPercent: b.returnPercent,
|
|
378
|
+
houseEdgePercent: b.houseEdgePercent,
|
|
379
|
+
})),
|
|
380
|
+
pools: sim.pools.map(p => ({ key: p.key, lasts: p.lasts, lastsDays: p.lastsDays, settlesAt: p.settlesAt })),
|
|
381
|
+
farms: sim.farms.map(f => ({ key: f.key, rewardsPerDay: f.rewardsPerDay, yearlyPercent: f.yearlyPercent })),
|
|
285
382
|
platformSolPerDay: sim.perDay.platform.totalSol,
|
|
286
383
|
};
|
|
287
|
-
return {
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
from,
|
|
387
|
+
summary,
|
|
388
|
+
...(input.json ? { simulation: sim } : {}),
|
|
389
|
+
text: explain(sim, economy) + footer,
|
|
390
|
+
};
|
|
288
391
|
},
|
|
289
392
|
},
|
|
290
393
|
{
|
|
291
394
|
name: 'xsolana_cost',
|
|
292
395
|
annotations: READ,
|
|
293
|
-
description:
|
|
396
|
+
description:
|
|
397
|
+
'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
398
|
inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
|
|
295
399
|
async handler(input, ctx) {
|
|
296
400
|
const { economy, from } = await load(input, ctx);
|
|
297
401
|
const change = liveChange(economy);
|
|
298
402
|
const c = cost(economy, { now: nowOf(ctx), ...change.scope });
|
|
299
403
|
if (!c.ok) return refused(c, from);
|
|
300
|
-
const head = change.scope.live
|
|
404
|
+
const head = change.scope.live
|
|
405
|
+
? change.scope.only.size
|
|
406
|
+
? `This economy is live: this prices only the change (${[...change.scope.only].join(', ')}).\n`
|
|
407
|
+
: 'This economy is live and nothing has changed since it went live: publishing costs nothing.\n'
|
|
408
|
+
: '';
|
|
301
409
|
return { ok: true, from, setup: c.setup, actions: c.actions, fees: c.fees, text: head + explainCost(c) + footer };
|
|
302
410
|
},
|
|
303
411
|
},
|
|
304
412
|
{
|
|
305
413
|
name: 'xsolana_explain',
|
|
306
414
|
annotations: READ,
|
|
307
|
-
description:
|
|
415
|
+
description:
|
|
416
|
+
'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
417
|
inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
|
|
309
418
|
async handler(input, ctx) {
|
|
310
419
|
const { economy, from } = await load(input, ctx);
|
|
@@ -312,11 +421,40 @@ export const tools = [
|
|
|
312
421
|
return { ...r, from, text: r.text + footer };
|
|
313
422
|
},
|
|
314
423
|
},
|
|
424
|
+
{
|
|
425
|
+
name: 'xsolana_review',
|
|
426
|
+
annotations: READ,
|
|
427
|
+
description:
|
|
428
|
+
'Review an economy the way a cheater reads it, before it goes live (not a validity check: xsolana_check is that). For every route that pays or gives value it says who can trigger it, what the program enforces, what it trusts off the chain, and the most one cheater gets; then findings with stable ids and a severity; then the trade-off to show the person: for each grant paid on vouchers, whether its server decides, replays or only bounds the result (its "trust"), what a cheater gets at that level, and what the next level costs to build. Fix every high finding, declare "trust" honestly on every voucher grant, and tell the person in plain words which level each signed route uses and what a cheater gets, so they choose the trade-off. Give the spec inline (`spec`), a file, or an example. Answers { ok (false when a finding is high, or the spec does not check: then `refusals`), routes: [{ kind ("grant", "farm", "item", "box", "payment", "recipe", "lottery", "contest", "auction", "schedule", "hold", "decision"), key, name, trigger, enforced: [words], trusts: [words], trust ("decides" | "replays" | "bounds" | null), ceiling (words), feeds: ["farm:<key>", …: what its prize is staked in, crafted into or buys] }], findings: [{ id (R-…), severity ("high" | "medium" | "low" | "info"), route ("grant:<key>", "farm:<key>", … or "economy"), message, fix }], tradeoffs (markdown) }.',
|
|
429
|
+
inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
|
|
430
|
+
async handler(input, ctx) {
|
|
431
|
+
const { economy, from } = await load(input, ctx);
|
|
432
|
+
const r = review(economy, { now: nowOf(ctx) });
|
|
433
|
+
if (r.refusals) return refused({ refusals: r.refusals, warnings: [] }, from);
|
|
434
|
+
const highs = r.findings.filter(f => f.severity === 'high').length;
|
|
435
|
+
const tail = highs
|
|
436
|
+
? `\n${plural(highs, 'high finding')}: fix ${highs === 1 ? 'it' : 'them'} (or have the person accept ${highs === 1 ? 'it' : 'them'} in plain words) before going live, then review again.`
|
|
437
|
+
: '\nNo high finding. Tell the person, in plain words, which level each signed route uses and what a cheater gets (the trade-offs above).';
|
|
438
|
+
return {
|
|
439
|
+
ok: r.ok,
|
|
440
|
+
from,
|
|
441
|
+
routes: r.routes,
|
|
442
|
+
findings: r.findings,
|
|
443
|
+
tradeoffs: r.tradeoffs,
|
|
444
|
+
text: `${describeReview(r, economy.name)}${tail}${footer}`,
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
},
|
|
315
448
|
{
|
|
316
449
|
name: 'xsolana_save',
|
|
317
450
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
318
451
|
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: {
|
|
452
|
+
inputSchema: {
|
|
453
|
+
type: 'object',
|
|
454
|
+
properties: { spec: SOURCE.spec, file: SOURCE.file },
|
|
455
|
+
required: ['spec'],
|
|
456
|
+
additionalProperties: false,
|
|
457
|
+
},
|
|
320
458
|
async handler(input, ctx) {
|
|
321
459
|
const rel = input.file ?? DEFAULT_FILE;
|
|
322
460
|
let file;
|
|
@@ -326,8 +464,18 @@ export const tools = [
|
|
|
326
464
|
return { ok: false, isError: true, text: `Not saved: ${e.message}` };
|
|
327
465
|
}
|
|
328
466
|
// 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
|
-
|
|
467
|
+
if (plainName(rel) === null)
|
|
468
|
+
return {
|
|
469
|
+
ok: false,
|
|
470
|
+
isError: true,
|
|
471
|
+
text: `Not saved: ${NOT_PLAIN(rel)}. Use letters, digits and . _ - (and / for a folder), as in ${DEFAULT_FILE}.`,
|
|
472
|
+
};
|
|
473
|
+
if (findSecrets(input.spec).length)
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
isError: true,
|
|
477
|
+
text: 'Not saved: the spec holds what looks like a private key or a seed phrase. Remove it (and treat that wallet as exposed).',
|
|
478
|
+
};
|
|
331
479
|
let previous = null;
|
|
332
480
|
if (existsSync(file)) {
|
|
333
481
|
try {
|
|
@@ -337,42 +485,84 @@ export const tools = [
|
|
|
337
485
|
}
|
|
338
486
|
// Only a spec is ever overwritten: never another JSON file that happens to be there (a package.json has a
|
|
339
487
|
// `version` too). A spec is version 2 with only the spec's own keys, as it is or under `economy`.
|
|
340
|
-
if (!isSpec(previous))
|
|
488
|
+
if (!isSpec(previous))
|
|
489
|
+
return {
|
|
490
|
+
ok: false,
|
|
491
|
+
isError: true,
|
|
492
|
+
text: `Not saved: ${rel} is there already and is not an economy spec. Pick another file name.`,
|
|
493
|
+
};
|
|
341
494
|
}
|
|
342
495
|
// (a spec wrapped under `economy`, given or already in the file, is read the same way xsolana_check reads it)
|
|
343
496
|
const given = economyOf(input.spec);
|
|
344
497
|
const spec = carryIds(previous && economyOf(previous), given);
|
|
345
|
-
const dropped =
|
|
498
|
+
const dropped =
|
|
499
|
+
given.live && !previous?.live
|
|
500
|
+
? ' The live section given was not kept: only `xsolana approve` writes one, as what it sends lands.'
|
|
501
|
+
: '';
|
|
346
502
|
const verdict = check(spec, { now: nowOf(ctx) });
|
|
347
|
-
if (!verdict.ok)
|
|
503
|
+
if (!verdict.ok)
|
|
504
|
+
return {
|
|
505
|
+
...refused(verdict, 'the spec given'),
|
|
506
|
+
isError: true,
|
|
507
|
+
text: `Not saved.\n${refused(verdict, 'the spec given').text}`,
|
|
508
|
+
};
|
|
348
509
|
// A folder that isn't there yet is made, inside the project folder (projectFile checked the whole path).
|
|
349
510
|
try {
|
|
350
511
|
await mkdir(path.dirname(file), { recursive: true });
|
|
351
512
|
} catch (e) {
|
|
352
|
-
return {
|
|
513
|
+
return {
|
|
514
|
+
ok: false,
|
|
515
|
+
isError: true,
|
|
516
|
+
text: `Not saved: the folder for ${rel} can’t be made (${e.code ?? 'error'}).`,
|
|
517
|
+
};
|
|
353
518
|
}
|
|
354
519
|
await writeFile(file, `${JSON.stringify(spec, null, 2)}\n`);
|
|
355
|
-
return {
|
|
520
|
+
return {
|
|
521
|
+
ok: true,
|
|
522
|
+
file: rel,
|
|
523
|
+
warnings: verdict.warnings,
|
|
524
|
+
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}`,
|
|
525
|
+
};
|
|
356
526
|
},
|
|
357
527
|
},
|
|
358
528
|
{
|
|
359
529
|
name: 'xsolana_publish',
|
|
360
530
|
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.`,
|
|
531
|
+
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
532
|
inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
|
|
364
533
|
async handler(input, ctx) {
|
|
365
534
|
const { economy, from, file } = await load(input, ctx);
|
|
366
535
|
const change = liveChange(economy);
|
|
367
|
-
if (change.scope.live && !change.scope.only.size && change.settings.length)
|
|
368
|
-
|
|
536
|
+
if (change.scope.live && !change.scope.only.size && change.settings.length)
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
transactions: 0,
|
|
540
|
+
approvals: 0,
|
|
541
|
+
steps: [],
|
|
542
|
+
text: `${from} is live, and its parts are as they are on chain. ${settingsNote(change.settings, economy.live)}${footer}`,
|
|
543
|
+
};
|
|
544
|
+
if (change.scope.live && !change.scope.only.size)
|
|
545
|
+
return {
|
|
546
|
+
ok: true,
|
|
547
|
+
transactions: 0,
|
|
548
|
+
approvals: 0,
|
|
549
|
+
steps: [],
|
|
550
|
+
text: `${from} is live, and nothing in it has changed since it went live: there is nothing to publish.${change.notes}${footer}`,
|
|
551
|
+
};
|
|
369
552
|
const p = plan(economy, { now: nowOf(ctx), ...change.scope });
|
|
370
553
|
if (!p.ok) return refused(p, from);
|
|
371
554
|
const c = cost(economy, { now: nowOf(ctx), ...change.scope });
|
|
372
|
-
const steps = p.transactions.flatMap(
|
|
555
|
+
const steps = p.transactions.flatMap(t => t.steps.map(s => s.says));
|
|
373
556
|
// the file as the approve command names it, relative to the project folder (projectFile's), and plain (plainName)
|
|
374
557
|
const target = file ? path.relative(realPath(path.resolve(cwdOf(ctx))), file) || DEFAULT_FILE : DEFAULT_FILE;
|
|
375
|
-
if (plainName(target) === null)
|
|
558
|
+
if (plainName(target) === null)
|
|
559
|
+
return {
|
|
560
|
+
ok: false,
|
|
561
|
+
transactions: 0,
|
|
562
|
+
approvals: 0,
|
|
563
|
+
steps: [],
|
|
564
|
+
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}`,
|
|
565
|
+
};
|
|
376
566
|
const command = approveCommand(target);
|
|
377
567
|
const text = [
|
|
378
568
|
...flagsToTell(economy, nowOf(ctx)),
|
|
@@ -381,21 +571,34 @@ export const tools = [
|
|
|
381
571
|
'',
|
|
382
572
|
`To put it live, the person runs this in a terminal in the project folder (not you): ${command}`,
|
|
383
573
|
'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
|
-
'
|
|
574
|
+
'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
575
|
...(file ? [] : [`(Save it first with xsolana_save: the approval reads ${DEFAULT_FILE}.)`]),
|
|
386
576
|
...(change.notes ? [change.notes.trim()] : []),
|
|
387
577
|
].join('\n');
|
|
388
|
-
return {
|
|
578
|
+
return {
|
|
579
|
+
ok: true,
|
|
580
|
+
command,
|
|
581
|
+
transactions: p.transactions.length,
|
|
582
|
+
approvals: p.approvals,
|
|
583
|
+
setupSol: c.setup.totalSol,
|
|
584
|
+
steps,
|
|
585
|
+
text: text + footer,
|
|
586
|
+
};
|
|
389
587
|
},
|
|
390
588
|
},
|
|
391
589
|
{
|
|
392
590
|
name: 'xsolana_status',
|
|
393
591
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
394
|
-
description:
|
|
592
|
+
description:
|
|
593
|
+
'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
594
|
inputSchema: { type: 'object', properties: { file: SOURCE.file }, additionalProperties: false },
|
|
396
595
|
async handler(input, ctx) {
|
|
397
596
|
const { economy, from } = await load(input, ctx);
|
|
398
|
-
if (!economy.live?.project)
|
|
597
|
+
if (!economy.live?.project)
|
|
598
|
+
return {
|
|
599
|
+
ok: false,
|
|
600
|
+
text: `${from} is not live yet (no "live" section). xsolana_publish says how to put it live.${footer}`,
|
|
601
|
+
};
|
|
399
602
|
const m = model(economy);
|
|
400
603
|
// Devnet only: a mainnet URL is refused, and so is a chain whose first block is mainnet's or testnet's.
|
|
401
604
|
const rpc = (ctx?.rpc ?? rpcClient)(notMainnet(process.env.XSOLANA_RPC_URL || DEVNET_RPC));
|
|
@@ -403,38 +606,72 @@ export const tools = [
|
|
|
403
606
|
const project = economy.live.project;
|
|
404
607
|
const wanted = [
|
|
405
608
|
['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
|
-
...
|
|
609
|
+
...m.pools.map(p => [`pool:${p.key}`, pda.pool(project, p.index)]),
|
|
610
|
+
...m.allBoxes.map(b => [b.shopOf ? `item:${b.shopOf} (shop)` : `box:${b.key}`, pda.box(project, b.id)]),
|
|
611
|
+
...m.items.map(it => [`item:${it.key}`, pda.item(project, it.id)]),
|
|
612
|
+
...m.farms.map(f => [`farm:${f.key}`, pda.farm(project, f.id)]),
|
|
613
|
+
...m.lotteries.map(l => [`lottery:${l.key}`, pda.lottery(project, l.id)]),
|
|
614
|
+
...m.lotteries
|
|
615
|
+
.filter(l => Number.isInteger(l.pot))
|
|
616
|
+
.map(l => [`lottery:${l.key} (pot ${l.pot})`, pda.lotteryPot(pda.lottery(project, l.id), l.pot)]),
|
|
617
|
+
...m.schedules.map(s => [`schedule:${s.key}`, pda.schedule(project, s.id)]),
|
|
618
|
+
...m.recipes.map(r => [`recipe:${r.key}`, pda.recipe(project, r.id)]),
|
|
619
|
+
...m.grants.map(g => [`grant:${g.key}`, pda.grant(project, g.id)]),
|
|
620
|
+
...m.stashes.map(s => [`stash:${s.key}`, pda.stash(project, s.id)]),
|
|
621
|
+
...(economy.auctions ?? [])
|
|
622
|
+
.filter(a => a.seller)
|
|
623
|
+
.map(a => [`auction:${a.key}`, pda.auction(project, a.seller, a.id)]),
|
|
416
624
|
];
|
|
417
625
|
const accounts = await rpc.accounts(wanted.map(([, a]) => a));
|
|
418
626
|
const rows = wanted.map(([part, address], i) => {
|
|
419
627
|
const a = accounts[i];
|
|
420
628
|
if (!a) return { part, address, found: false };
|
|
421
629
|
// 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
|
-
|
|
630
|
+
if (part.startsWith('grant:'))
|
|
631
|
+
return {
|
|
632
|
+
part,
|
|
633
|
+
address,
|
|
634
|
+
found: true,
|
|
635
|
+
kind: 'Grant',
|
|
636
|
+
sol: Number(BigInt(a.lamports ?? 0) - rentOf(a.data?.length ?? 0)) / 1e9,
|
|
637
|
+
};
|
|
638
|
+
const tokenAmount =
|
|
639
|
+
a.data?.length >= 72 && (part.startsWith('pool:') || part.includes(' (pot '))
|
|
640
|
+
? Number(new DataView(a.data.buffer, a.data.byteOffset).getBigUint64(64, true))
|
|
641
|
+
: undefined;
|
|
424
642
|
const decoded = a.owner === LOOT_PROGRAM ? decodeAny(a.data) : null;
|
|
425
643
|
// A stash: how many NFTs it still holds to give away.
|
|
426
644
|
const nfts = decoded?.kind === 'Stash' ? decoded.value.nfts.length : undefined;
|
|
427
|
-
return {
|
|
645
|
+
return {
|
|
646
|
+
part,
|
|
647
|
+
address,
|
|
648
|
+
found: true,
|
|
649
|
+
...(tokenAmount !== undefined ? { amount: tokenAmount } : {}),
|
|
650
|
+
...(nfts !== undefined ? { nfts } : {}),
|
|
651
|
+
...(decoded ? { kind: decoded.kind } : {}),
|
|
652
|
+
};
|
|
428
653
|
});
|
|
429
|
-
const missing = rows.filter(
|
|
430
|
-
const text = [
|
|
654
|
+
const missing = rows.filter(r => !r.found);
|
|
655
|
+
const text = [
|
|
656
|
+
`${economy.name ?? 'The economy'} on ${CLUSTER}: project ${project}.`,
|
|
657
|
+
...rows.map(
|
|
658
|
+
r =>
|
|
659
|
+
`- ${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})`,
|
|
660
|
+
),
|
|
661
|
+
missing.length
|
|
662
|
+
? `\n${plural(missing.length, 'part')} not on chain: publish again (xsolana_publish) to add them.`
|
|
663
|
+
: '',
|
|
664
|
+
]
|
|
665
|
+
.filter(Boolean)
|
|
666
|
+
.join('\n');
|
|
431
667
|
return { ok: true, rows, text: text + footer };
|
|
432
668
|
},
|
|
433
669
|
},
|
|
434
670
|
{
|
|
435
671
|
name: 'xsolana_client',
|
|
436
672
|
annotations: READ,
|
|
437
|
-
description:
|
|
673
|
+
description:
|
|
674
|
+
'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
675
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
439
676
|
async handler() {
|
|
440
677
|
const text = [
|
|
@@ -471,6 +708,7 @@ export const tools = [
|
|
|
471
708
|
' loot.openListingIx / takeListingIx / closeListingIx, loot.listingTerms(listing), loot.pda.listing(project, asset), loot.renterOf(listing, now)',
|
|
472
709
|
' 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
710
|
' 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)',
|
|
711
|
+
' 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
712
|
' 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
713
|
' 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
714
|
' loot.runsDue(schedule, now, vaultBalance), loot.scheduleCatchesUp(schedule), loot.pda.vest(schedule), loot.pda.prizeVault(contest), loot.pda.lotteryStash(lottery), loot.pda.rewards(farm)',
|