@xgamesfun/solana 0.28.1 → 0.28.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLAIMER.md +3 -1
- package/README.md +18 -16
- package/bin/xsolana.mjs +27 -1
- package/js/approve/details.mjs +3 -3
- package/js/approve/live-check.mjs +2 -2
- package/js/approve/page.html +5 -5
- package/js/approve/page.js +16 -4
- package/js/approve/server.mjs +31 -26
- package/js/approve/with-key.mjs +7 -4
- package/js/chain/borsh.js +5 -3
- package/js/chain/rpc.js +26 -10
- package/js/client/accounts.js +6 -6
- package/js/client/cluster.js +136 -0
- package/js/client/grant.js +2 -2
- package/js/client/ix.js +16 -16
- package/js/client/pda.js +18 -5
- package/js/economy/check.mjs +28 -13
- package/js/economy/explain.mjs +2 -1
- package/js/economy/guide.mjs +6 -4
- package/js/economy/live.mjs +2 -2
- package/js/economy/plan.mjs +1 -1
- package/js/economy/schema.json +2 -2
- package/js/mcp/server.mjs +35 -4
- package/js/mcp/tools.mjs +56 -45
- package/js/mcp/with-cluster.mjs +25 -0
- package/js/play/index.js +42 -37
- package/js/play/sponsor.js +4 -4
- package/js/results/index.js +7 -6
- package/js/results/server.js +4 -3
- package/js/results/wire.js +9 -8
- package/package.json +6 -1
- package/release.json +6 -1
- package/skills/xsolana-economy/SKILL.md +13 -9
- package/skills/xsolana-economy/references/apps.md +2 -1
- package/skills/xsolana-economy/references/telling-the-person.md +1 -1
- package/skills/xsolana-economy/references/trust-and-cheating.md +1 -1
- package/types/chain/borsh.d.ts +4 -2
- package/types/chain/rpc.d.ts +10 -4
- package/types/client/cluster.d.ts +61 -0
- package/types/client/idl/loot.d.ts +9 -9
- package/types/client/pda.d.ts +1 -1
- package/types/economy/check.d.mts +1 -2
- package/types/economy/plan.d.mts +14 -14
- package/types/mcp/server.d.mts +1 -1
- package/types/mcp/tools.d.mts +9 -7
- package/types/mcp/with-cluster.d.mts +8 -0
- package/types/play/index.d.ts +2 -2
- package/types/results/index.d.ts +6 -6
- package/types/results/server.d.ts +3 -3
- package/types/results/wire.d.ts +8 -8
package/js/mcp/tools.mjs
CHANGED
|
@@ -9,11 +9,11 @@ import path from 'node:path';
|
|
|
9
9
|
import { addressBytes } from '../chain/base58.js';
|
|
10
10
|
import { metadataAddress } from '../chain/metadata.js';
|
|
11
11
|
import { parseMint, tokenEntry } from '../chain/mint.js';
|
|
12
|
-
import {
|
|
12
|
+
import { rpcClient } from '../chain/rpc.js';
|
|
13
13
|
import { mintState } from '../chain/token2022.js';
|
|
14
14
|
import { decodeAny } from '../client/accounts.js';
|
|
15
15
|
import { IDL } from '../client/ix.js';
|
|
16
|
-
import {
|
|
16
|
+
import { checkOnCluster, cluster, CLUSTERS, clusterRpcUrl, lootProgram, pda, realMoney } from '../client/pda.js';
|
|
17
17
|
import { check, describe } from '../economy/check.mjs';
|
|
18
18
|
import { legalFlags } from '../economy/legal.mjs';
|
|
19
19
|
import { cost, explainCost } from '../economy/cost.mjs';
|
|
@@ -73,17 +73,26 @@ export function keyFileFor(name) {
|
|
|
73
73
|
}
|
|
74
74
|
/** The command that makes a project's key file (it prints the key's address, never the key). */
|
|
75
75
|
export const keygenCommand = keyFile => `npx -y @xgamesfun/solana@${PACKAGE_VERSION} keygen ${keyFile}`;
|
|
76
|
-
export const CLUSTER = 'devnet';
|
|
77
76
|
export const DEVNET_RPC = 'https://api.devnet.solana.com';
|
|
78
|
-
/**
|
|
77
|
+
/** The RPC a tool reads through: the test's own when it gives one, else this process's cluster's (XSOLANA_RPC_URL, or devnet's public RPC). */
|
|
78
|
+
const chainOf = ctx =>
|
|
79
|
+
ctx?.rpc ? ctx.rpc(process.env.XSOLANA_RPC_URL || CLUSTERS[cluster()].rpc) : rpcClient(clusterRpcUrl());
|
|
80
|
+
/**
|
|
81
|
+
* What every answer carries, so an agent never has to remember the program's address. The program and the cluster are
|
|
82
|
+
* read when asked for: this process's cluster (`client/cluster.js`).
|
|
83
|
+
*/
|
|
79
84
|
export const PROGRAM_INFO = Object.freeze({
|
|
80
|
-
program
|
|
81
|
-
|
|
85
|
+
get program() {
|
|
86
|
+
return lootProgram();
|
|
87
|
+
},
|
|
88
|
+
get cluster() {
|
|
89
|
+
return cluster();
|
|
90
|
+
},
|
|
82
91
|
version: VERSION,
|
|
83
92
|
idl: '@xgamesfun/solana/idl.json',
|
|
84
93
|
package: `@xgamesfun/solana@${PACKAGE_VERSION}`,
|
|
85
94
|
});
|
|
86
|
-
const footer = `\n\nloot ${VERSION} · program ${
|
|
95
|
+
const footer = () => `\n\nloot ${VERSION} · program ${lootProgram()} · ${cluster()}`;
|
|
87
96
|
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
88
97
|
const nowOf = ctx => ctx?.now ?? Math.floor(Date.now() / 1000);
|
|
89
98
|
const cwdOf = ctx => ctx?.cwd ?? process.cwd();
|
|
@@ -298,7 +307,7 @@ export const tools = [
|
|
|
298
307
|
},
|
|
299
308
|
async handler({ part } = NO_OPTIONS) {
|
|
300
309
|
return {
|
|
301
|
-
text: guideText(part) + footer,
|
|
310
|
+
text: guideText(part) + footer(),
|
|
302
311
|
parts: catalog(part),
|
|
303
312
|
...(part ? {} : { cannot: CANNOT }),
|
|
304
313
|
...PROGRAM_INFO,
|
|
@@ -327,7 +336,7 @@ export const tools = [
|
|
|
327
336
|
name: ex.name,
|
|
328
337
|
spec: ex.spec,
|
|
329
338
|
checks: verdict.ok ? 'clean' : verdict.refusals.map(r => r.code),
|
|
330
|
-
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}`,
|
|
339
|
+
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()}`,
|
|
331
340
|
};
|
|
332
341
|
}
|
|
333
342
|
if (!query) {
|
|
@@ -345,7 +354,7 @@ export const tools = [
|
|
|
345
354
|
`${all.length} worked examples (xsolana_examples {"name": "<name>"} gives the whole spec):`,
|
|
346
355
|
...all.map(e => `- ${e.name}: ${e.title}. ${e.prompt ?? ''} [${e.parts.join(', ')}]`),
|
|
347
356
|
`\nAnd ${ideas().length} rated economies: pass a query to search them.`,
|
|
348
|
-
].join('\n') + footer,
|
|
357
|
+
].join('\n') + footer(),
|
|
349
358
|
};
|
|
350
359
|
}
|
|
351
360
|
const r = search(query, { limit, fit });
|
|
@@ -370,7 +379,7 @@ export const tools = [
|
|
|
370
379
|
]
|
|
371
380
|
.filter(Boolean)
|
|
372
381
|
.join('\n');
|
|
373
|
-
return { examples: r.examples, ideas: r.ideas, hows: r.hows, limits: r.limits, text: text + footer };
|
|
382
|
+
return { examples: r.examples, ideas: r.ideas, hows: r.hows, limits: r.limits, text: text + footer() };
|
|
374
383
|
},
|
|
375
384
|
},
|
|
376
385
|
{
|
|
@@ -391,7 +400,7 @@ export const tools = [
|
|
|
391
400
|
from,
|
|
392
401
|
refusals: verdict.refusals,
|
|
393
402
|
warnings: verdict.warnings,
|
|
394
|
-
text: `${head}${tail}${footer}`,
|
|
403
|
+
text: `${head}${tail}${footer()}`,
|
|
395
404
|
};
|
|
396
405
|
},
|
|
397
406
|
},
|
|
@@ -430,7 +439,7 @@ export const tools = [
|
|
|
430
439
|
from,
|
|
431
440
|
summary,
|
|
432
441
|
...(input.json ? { simulation: sim } : {}),
|
|
433
|
-
text: explain(sim, economy) + footer,
|
|
442
|
+
text: explain(sim, economy) + footer(),
|
|
434
443
|
};
|
|
435
444
|
},
|
|
436
445
|
},
|
|
@@ -458,7 +467,7 @@ export const tools = [
|
|
|
458
467
|
setup: c.setup,
|
|
459
468
|
actions: c.actions,
|
|
460
469
|
fees: c.fees,
|
|
461
|
-
text: head + explainCost(c) + names + footer,
|
|
470
|
+
text: head + explainCost(c) + names + footer(),
|
|
462
471
|
};
|
|
463
472
|
},
|
|
464
473
|
},
|
|
@@ -471,7 +480,7 @@ export const tools = [
|
|
|
471
480
|
async handler(input, ctx) {
|
|
472
481
|
const { economy, from } = await load(input, ctx);
|
|
473
482
|
const r = explainEconomy(economy, { now: nowOf(ctx) });
|
|
474
|
-
return { ...r, from, text: r.text + footer };
|
|
483
|
+
return { ...r, from, text: r.text + footer() };
|
|
475
484
|
},
|
|
476
485
|
},
|
|
477
486
|
{
|
|
@@ -494,7 +503,7 @@ export const tools = [
|
|
|
494
503
|
routes: r.routes,
|
|
495
504
|
findings: r.findings,
|
|
496
505
|
tradeoffs: r.tradeoffs,
|
|
497
|
-
text: `${describeReview(r, economy.name)}${tail}${footer}`,
|
|
506
|
+
text: `${describeReview(r, economy.name)}${tail}${footer()}`,
|
|
498
507
|
};
|
|
499
508
|
},
|
|
500
509
|
},
|
|
@@ -574,7 +583,7 @@ export const tools = [
|
|
|
574
583
|
ok: true,
|
|
575
584
|
file: rel,
|
|
576
585
|
warnings: verdict.warnings,
|
|
577
|
-
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}`,
|
|
586
|
+
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()}`,
|
|
578
587
|
};
|
|
579
588
|
},
|
|
580
589
|
},
|
|
@@ -592,7 +601,7 @@ export const tools = [
|
|
|
592
601
|
transactions: 0,
|
|
593
602
|
approvals: 0,
|
|
594
603
|
steps: [],
|
|
595
|
-
text: `${from} is live, and its parts are as they are on chain. ${settingsNote(change.settings, economy.live)}${footer}`,
|
|
604
|
+
text: `${from} is live, and its parts are as they are on chain. ${settingsNote(change.settings, economy.live)}${footer()}`,
|
|
596
605
|
};
|
|
597
606
|
if (change.scope.live && !change.changes)
|
|
598
607
|
return {
|
|
@@ -600,7 +609,7 @@ export const tools = [
|
|
|
600
609
|
transactions: 0,
|
|
601
610
|
approvals: 0,
|
|
602
611
|
steps: [],
|
|
603
|
-
text: `${from} is live, and nothing in it has changed since it went live: there is nothing to publish.${change.notes}${footer}`,
|
|
612
|
+
text: `${from} is live, and nothing in it has changed since it went live: there is nothing to publish.${change.notes}${footer()}`,
|
|
604
613
|
};
|
|
605
614
|
const p = plan(economy, { now: nowOf(ctx), ...change.scope });
|
|
606
615
|
if (!p.ok) return refused(p, from);
|
|
@@ -622,7 +631,7 @@ export const tools = [
|
|
|
622
631
|
transactions: 0,
|
|
623
632
|
approvals: 0,
|
|
624
633
|
steps: [],
|
|
625
|
-
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}`,
|
|
634
|
+
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()}`,
|
|
626
635
|
};
|
|
627
636
|
const live = Boolean(change.scope.live);
|
|
628
637
|
const keyFile = keyFileFor(economy.name);
|
|
@@ -632,7 +641,7 @@ export const tools = [
|
|
|
632
641
|
const command = `${approveCommand(target)} --keypair ${keyFile}`;
|
|
633
642
|
const text = [
|
|
634
643
|
...flagsToTell(economy, nowOf(ctx)),
|
|
635
|
-
`${change.scope.live ? 'Publishing this change to the live economy' : 'Going live'}: ${renaming ? 'at most ' : ''}${plural(p.transactions.length, 'transaction')}, about ${c.setup.totalSol} SOL on
|
|
644
|
+
`${change.scope.live ? 'Publishing this change to the live economy' : 'Going live'}: ${renaming ? 'at most ' : ''}${plural(p.transactions.length, 'transaction')}, about ${c.setup.totalSol} SOL on ${cluster()}${realMoney() ? ' (real SOL)' : ''}:`,
|
|
636
645
|
...steps.map((s, i) => `${i + 1}. ${s}`),
|
|
637
646
|
'',
|
|
638
647
|
`${live ? 'To publish it' : 'To put it live'}, in a terminal in the project folder (you run these; nothing opens, and nothing is sent before the person’s yes):`,
|
|
@@ -643,13 +652,13 @@ export const tools = [
|
|
|
643
652
|
: [
|
|
644
653
|
`1. A key file for this project, unless it has one: ${keygen}`,
|
|
645
654
|
' It prints the key’s address, never the key. The key file is the project’s owner: whoever can read it controls the project, and which key goes live is for good (every later change is signed by it; these tools can’t hand a project to another owner). It stays where it is: never in a chat, a spec, a repository or a page, and you never open, read or print it. Tell the person to back the file up themselves: if it is lost, nothing can change the economy or move what its address holds.',
|
|
646
|
-
` Everything the setup takes from the owner must be at that address first: the devnet SOL (about ${c.setup.totalSol} SOL here; https://faucet.solana.com gives some), any seed in a coin that already exists, and every NFT a stash, a raffle or an auction names (a collection they already have needs that address as its update authority). If they would rather not move those, offer the browser wallet.`,
|
|
655
|
+
` Everything the setup takes from the owner must be at that address first: ${realMoney() ? `the SOL (about ${c.setup.totalSol} SOL here: this economy is on mainnet, so it is real SOL)` : `the devnet SOL (about ${c.setup.totalSol} SOL here; https://faucet.solana.com gives some)`}, any seed in a coin that already exists, and every NFT a stash, a raffle or an auction names (a collection they already have needs that address as its update authority). If they would rather not move those, offer the browser wallet.`,
|
|
647
656
|
' The steps above call the owner’s address “your wallet”: with a key file, that is the key’s address. Unless the spec names a treasury, what sales leave lands there, and it signs what only the authority signs. If the economy takes money in, ask for the public address of a wallet the person holds and set "treasury": "wallet:<address>" first: it can’t be changed through the spec once live.',
|
|
648
657
|
]),
|
|
649
658
|
`2. ${command}`,
|
|
650
659
|
' It prints every step with what it pays and trusts, the warnings, the notice (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) and a code. It sends nothing.',
|
|
651
660
|
'3. Show the person that printout and ask. When they say yes, run the same command with --yes <code>: the code stands for exactly what was printed, and anything else is refused. What lands is written back into the file. If it stops part way, run it again without --yes: it prints what is left and a new code. Go on with that code; if it shows a step or a warning they have not seen, ask again.',
|
|
652
|
-
`${live ? 'For a project a browser wallet owns' : 'Only if the person asks to sign in a browser wallet instead'}: give them this to run in their own terminal: ${browserCommand} (it serves a page on their own computer where they connect a wallet set to
|
|
661
|
+
`${live ? 'For a project a browser wallet owns' : 'Only if the person asks to sign in a browser wallet instead'}: give them this to run in their own terminal: ${browserCommand} (it serves a page on their own computer where they connect a wallet set to ${cluster()} and approve, ${plural(p.approvals, 'approval')}, and keeps running until the page says it is live). Don’t run or open it otherwise.${live ? ' A project a key file owns can’t be signed in a browser wallet.' : ''}`,
|
|
653
662
|
...(file ? [] : [`(Save it first with xsolana_save: \`approve\` reads ${DEFAULT_FILE}.)`]),
|
|
654
663
|
...(change.notes ? [change.notes.trim()] : []),
|
|
655
664
|
].join('\n');
|
|
@@ -663,7 +672,7 @@ export const tools = [
|
|
|
663
672
|
approvals: p.approvals,
|
|
664
673
|
setupSol: c.setup.totalSol,
|
|
665
674
|
steps,
|
|
666
|
-
text: text + footer,
|
|
675
|
+
text: text + footer(),
|
|
667
676
|
};
|
|
668
677
|
},
|
|
669
678
|
},
|
|
@@ -671,19 +680,19 @@ export const tools = [
|
|
|
671
680
|
name: 'xsolana_status',
|
|
672
681
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
673
682
|
description:
|
|
674
|
-
'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).',
|
|
683
|
+
'A live economy as it is on chain: the project, every pool’s balance, every record the spec names (found or missing). Reads the economy’s own cluster: devnet through its public RPC (or XSOLANA_RPC_URL), mainnet through XSOLANA_RPC_URL, which must then name a mainnet RPC.',
|
|
675
684
|
inputSchema: { type: 'object', properties: { spec: SOURCE.spec, file: SOURCE.file }, additionalProperties: false },
|
|
676
685
|
async handler(input, ctx) {
|
|
677
686
|
const { economy, from } = await load(input, ctx);
|
|
678
687
|
if (!economy.live?.project)
|
|
679
688
|
return {
|
|
680
689
|
ok: false,
|
|
681
|
-
text: `${from} is not live yet (no "live" section). xsolana_publish says how to put it live.${footer}`,
|
|
690
|
+
text: `${from} is not live yet (no "live" section). xsolana_publish says how to put it live.${footer()}`,
|
|
682
691
|
};
|
|
683
692
|
const m = model(economy);
|
|
684
|
-
//
|
|
685
|
-
const rpc = (ctx
|
|
686
|
-
if (!ctx?.rpc) await
|
|
693
|
+
// This process's cluster only: a URL that names another is refused, and so is a chain whose first block is another's.
|
|
694
|
+
const rpc = chainOf(ctx);
|
|
695
|
+
if (!ctx?.rpc) await checkOnCluster(rpc);
|
|
687
696
|
const project = economy.live.project;
|
|
688
697
|
const wanted = [
|
|
689
698
|
['project', project],
|
|
@@ -720,7 +729,7 @@ export const tools = [
|
|
|
720
729
|
a.data?.length >= 72 && (part.startsWith('pool:') || part.includes(' (pot '))
|
|
721
730
|
? Number(new DataView(a.data.buffer, a.data.byteOffset).getBigUint64(64, true))
|
|
722
731
|
: undefined;
|
|
723
|
-
const decoded = a.owner ===
|
|
732
|
+
const decoded = a.owner === lootProgram() ? decodeAny(a.data) : null;
|
|
724
733
|
// A stash: how many NFTs it still holds to give away.
|
|
725
734
|
const nfts = decoded?.kind === 'Stash' ? decoded.value.nfts.length : undefined;
|
|
726
735
|
return {
|
|
@@ -734,7 +743,7 @@ export const tools = [
|
|
|
734
743
|
});
|
|
735
744
|
const missing = rows.filter(r => !r.found);
|
|
736
745
|
const text = [
|
|
737
|
-
`${economy.name ?? 'The economy'} on ${
|
|
746
|
+
`${economy.name ?? 'The economy'} on ${cluster()}: project ${project}.`,
|
|
738
747
|
...rows.map(
|
|
739
748
|
r =>
|
|
740
749
|
`- ${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})`,
|
|
@@ -745,7 +754,7 @@ export const tools = [
|
|
|
745
754
|
]
|
|
746
755
|
.filter(Boolean)
|
|
747
756
|
.join('\n');
|
|
748
|
-
return { ok: true, rows, text: text + footer };
|
|
757
|
+
return { ok: true, rows, text: text + footer() };
|
|
749
758
|
},
|
|
750
759
|
},
|
|
751
760
|
{
|
|
@@ -778,12 +787,12 @@ export const tools = [
|
|
|
778
787
|
} catch {
|
|
779
788
|
return {
|
|
780
789
|
ok: false,
|
|
781
|
-
text: `${JSON.stringify(mint)} is not a Solana address. Give the token’s mint address.${footer}`,
|
|
790
|
+
text: `${JSON.stringify(mint)} is not a Solana address. Give the token’s mint address.${footer()}`,
|
|
782
791
|
};
|
|
783
792
|
}
|
|
784
|
-
//
|
|
785
|
-
const rpc = (ctx
|
|
786
|
-
if (!ctx?.rpc) await
|
|
793
|
+
// This process's cluster only: a URL that names another is refused, and so is a chain whose first block is another's.
|
|
794
|
+
const rpc = chainOf(ctx);
|
|
795
|
+
if (!ctx?.rpc) await checkOnCluster(rpc);
|
|
787
796
|
let project = null;
|
|
788
797
|
if (input.project !== undefined) {
|
|
789
798
|
project = String(input.project).trim();
|
|
@@ -792,7 +801,7 @@ export const tools = [
|
|
|
792
801
|
} catch {
|
|
793
802
|
return {
|
|
794
803
|
ok: false,
|
|
795
|
-
text: `${JSON.stringify(project)} is not a Solana address. Give the project’s address, or leave it out.${footer}`,
|
|
804
|
+
text: `${JSON.stringify(project)} is not a Solana address. Give the project’s address, or leave it out.${footer()}`,
|
|
796
805
|
};
|
|
797
806
|
}
|
|
798
807
|
}
|
|
@@ -815,15 +824,17 @@ export const tools = [
|
|
|
815
824
|
ok: false,
|
|
816
825
|
text: `${
|
|
817
826
|
account
|
|
818
|
-
? `${mint} is not a token mint on ${
|
|
819
|
-
:
|
|
820
|
-
|
|
827
|
+
? `${mint} is not a token mint on ${cluster()} (a token account or a wallet is not the mint: an explorer’s token page names it)`
|
|
828
|
+
: cluster() === 'devnet'
|
|
829
|
+
? `There is nothing at ${mint} on devnet. A coin launched on mainnet is not on devnet: this reads the cluster of the economy in this folder, devnet unless its file says "network": "mainnet"`
|
|
830
|
+
: `There is nothing at ${mint} on ${cluster()}. A coin made on devnet is not on ${cluster()}`
|
|
831
|
+
}.${footer()}`,
|
|
821
832
|
};
|
|
822
833
|
if (token.source === 'sol')
|
|
823
834
|
return {
|
|
824
835
|
ok: true,
|
|
825
836
|
token,
|
|
826
|
-
text: `${mint} is wrapped SOL: in a spec that is SOL itself.\n\nIn the spec’s "tokens":\n${JSON.stringify(token)}${footer}`,
|
|
837
|
+
text: `${mint} is wrapped SOL: in a spec that is SOL itself.\n\nIn the spec’s "tokens":\n${JSON.stringify(token)}${footer()}`,
|
|
827
838
|
};
|
|
828
839
|
const read = parseMint(mint, account.owner, account.data, record?.data ?? null);
|
|
829
840
|
const supply = Number(BigInt(read.supply)) / 10 ** read.decimals;
|
|
@@ -863,7 +874,7 @@ export const tools = [
|
|
|
863
874
|
`Its mint authority is not the project given (${project}): until it is handed over, nothing the economy mints in it lands.`,
|
|
864
875
|
].filter(Boolean);
|
|
865
876
|
const text = [
|
|
866
|
-
`The token at ${mint} on ${
|
|
877
|
+
`The token at ${mint} on ${cluster()}: ${read.program === 'token-2022' ? 'a Token-2022' : 'a classic SPL'} token, ${plural(read.decimals, 'decimal')}, ${supply.toLocaleString('en-US')} in existence${p.mintAuthority === 'none' ? ', and nobody can mint more' : ''}. It ${called}.`,
|
|
867
878
|
can.length
|
|
868
879
|
? `Whoever controls its mint can:\n${can.map(c => `- ${c}`).join('\n')}\nAn economy in it is only as safe as the token: say so where players pay, stake or are paid.`
|
|
869
880
|
: `Its mint has no key that can freeze it, take it, charge on it or stop a transfer${p.mintAuthority === 'project' ? ' (the project mints it)' : ''}.`,
|
|
@@ -883,7 +894,7 @@ export const tools = [
|
|
|
883
894
|
mintAuthority: read.mintAuthority,
|
|
884
895
|
freezeAuthority: read.freezeAuthority,
|
|
885
896
|
},
|
|
886
|
-
text: text + footer,
|
|
897
|
+
text: text + footer(),
|
|
887
898
|
};
|
|
888
899
|
},
|
|
889
900
|
},
|
|
@@ -895,7 +906,7 @@ export const tools = [
|
|
|
895
906
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
896
907
|
async handler() {
|
|
897
908
|
const text = [
|
|
898
|
-
`loot ${VERSION} on ${
|
|
909
|
+
`loot ${VERSION} on ${cluster()}: program ${lootProgram()}. Never write another address from memory: take it from this package.`,
|
|
899
910
|
'Package: npm install @xgamesfun/solana (plain ES modules, no Solana SDK needed; works in the browser).',
|
|
900
911
|
' What only the project’s owner or authority signs (a contest’s result, a payout, a schedule’s run) is not a page’s: a script on the owner’s machine signs it with the project’s key file, `readKeyFile(file)` from "@xgamesfun/solana/keyfile" (an address and a way to sign, never the key; it is also a `wallet` for solanaApp). The skill’s references/after-launch.md has the script.',
|
|
901
912
|
' import { solanaApp, walletStandardSigner } from "@xgamesfun/solana/play";',
|
|
@@ -941,7 +952,7 @@ export const tools = [
|
|
|
941
952
|
'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.',
|
|
942
953
|
'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.',
|
|
943
954
|
].join('\n');
|
|
944
|
-
return { text: text + footer, ...PROGRAM_INFO };
|
|
955
|
+
return { text: text + footer(), ...PROGRAM_INFO };
|
|
945
956
|
},
|
|
946
957
|
},
|
|
947
958
|
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// For a server that answers for both clusters in one process (Node): `withCluster(cluster, fn)` runs `fn`, and
|
|
2
|
+
// everything it awaits, with the client on that cluster. Every address derived, instruction built and voucher signed
|
|
3
|
+
// inside it is that cluster's, whatever the process's own cluster is, and two pieces of work on two clusters can run
|
|
4
|
+
// at the same time without touching each other. Outside any, the client is on the process's own cluster
|
|
5
|
+
// (js/client/cluster.js, which stays free of Node so a page can load it: this file is the Node half).
|
|
6
|
+
//
|
|
7
|
+
// Importing this file sets it up: import it once, before the first request is answered.
|
|
8
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
9
|
+
import { CLUSTERS, scopeClusters } from '../client/cluster.js';
|
|
10
|
+
|
|
11
|
+
const work = new AsyncLocalStorage();
|
|
12
|
+
scopeClusters(() => work.getStore());
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs `fn` on `cluster` (`devnet` or `mainnet`) and returns what it returns. A cluster loot does not know is refused.
|
|
16
|
+
* @template T
|
|
17
|
+
* @param {'devnet' | 'mainnet'} cluster
|
|
18
|
+
* @param {() => T} fn
|
|
19
|
+
* @returns {T}
|
|
20
|
+
*/
|
|
21
|
+
export function withCluster(cluster, fn) {
|
|
22
|
+
if (!Object.hasOwn(CLUSTERS, cluster))
|
|
23
|
+
throw new Error(`"${cluster}" is not a cluster loot knows: ${Object.keys(CLUSTERS).join(' or ')}.`);
|
|
24
|
+
return work.run(cluster, fn);
|
|
25
|
+
}
|