@xgamesfun/solana 0.28.0 → 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.
Files changed (53) hide show
  1. package/DISCLAIMER.md +3 -1
  2. package/README.md +18 -16
  3. package/bin/xsolana.mjs +27 -1
  4. package/js/approve/details.mjs +3 -3
  5. package/js/approve/live-check.mjs +2 -2
  6. package/js/approve/page.html +5 -5
  7. package/js/approve/page.js +24 -4
  8. package/js/approve/server.mjs +66 -78
  9. package/js/approve/wallet-adds.mjs +180 -0
  10. package/js/approve/with-key.mjs +7 -4
  11. package/js/chain/borsh.js +5 -3
  12. package/js/chain/pda.js +6 -0
  13. package/js/chain/rpc.js +26 -10
  14. package/js/client/accounts.js +6 -6
  15. package/js/client/cluster.js +136 -0
  16. package/js/client/grant.js +2 -2
  17. package/js/client/ix.js +16 -16
  18. package/js/client/pda.js +18 -5
  19. package/js/economy/check.mjs +28 -13
  20. package/js/economy/explain.mjs +2 -1
  21. package/js/economy/guide.mjs +6 -4
  22. package/js/economy/live.mjs +2 -2
  23. package/js/economy/plan.mjs +3 -3
  24. package/js/economy/schema.json +2 -2
  25. package/js/mcp/server.mjs +35 -4
  26. package/js/mcp/tools.mjs +56 -45
  27. package/js/mcp/with-cluster.mjs +25 -0
  28. package/js/play/index.js +42 -37
  29. package/js/play/sponsor.js +4 -4
  30. package/js/results/index.js +7 -6
  31. package/js/results/server.js +4 -3
  32. package/js/results/wire.js +9 -8
  33. package/package.json +6 -1
  34. package/release.json +6 -1
  35. package/skills/xsolana-economy/SKILL.md +13 -9
  36. package/skills/xsolana-economy/references/apps.md +2 -1
  37. package/skills/xsolana-economy/references/telling-the-person.md +1 -1
  38. package/skills/xsolana-economy/references/trust-and-cheating.md +1 -1
  39. package/types/chain/borsh.d.ts +4 -2
  40. package/types/chain/pda.d.ts +6 -0
  41. package/types/chain/rpc.d.ts +10 -4
  42. package/types/client/cluster.d.ts +61 -0
  43. package/types/client/idl/loot.d.ts +9 -9
  44. package/types/client/pda.d.ts +1 -1
  45. package/types/economy/check.d.mts +1 -2
  46. package/types/economy/plan.d.mts +16 -16
  47. package/types/mcp/server.d.mts +1 -1
  48. package/types/mcp/tools.d.mts +9 -7
  49. package/types/mcp/with-cluster.d.mts +8 -0
  50. package/types/play/index.d.ts +2 -2
  51. package/types/results/index.d.ts +6 -6
  52. package/types/results/server.d.ts +3 -3
  53. package/types/results/wire.d.ts +8 -8
package/js/client/pda.js CHANGED
@@ -3,9 +3,22 @@
3
3
  import { addressBytes } from '../chain/base58.js';
4
4
  import { intLE, utf8, uintLE } from '../chain/bytes.js';
5
5
  import { associatedTokenAddress, findProgramAddress, MPL_CORE_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
6
- import IDL from './idl/loot.js';
6
+ import { lootProgram } from './cluster.js';
7
7
 
8
- export const LOOT_PROGRAM = IDL.address;
8
+ // loot's program on this process's cluster (`cluster.js`): devnet unless the process names mainnet.
9
+ export {
10
+ checkOnCluster,
11
+ CLUSTERS,
12
+ cluster,
13
+ clusterRpcUrl,
14
+ LOOT_PROGRAM,
15
+ lootProgram,
16
+ networkOf,
17
+ realMoney,
18
+ scopeClusters,
19
+ useCluster,
20
+ walletChain,
21
+ } from './cluster.js';
9
22
  /** MagicBlock's VRF program and its default queue, the only queue the program accepts. */
10
23
  export const VRF_PROGRAM = 'Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz';
11
24
  export const VRF_QUEUE = 'Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh';
@@ -25,7 +38,7 @@ const u64 = n => uintLE(n, 8);
25
38
  const i64 = n => intLE(n, 8);
26
39
 
27
40
  /** The PDA of these seeds under the loot program, with its bump. */
28
- export const lootAddress = (seeds, program = LOOT_PROGRAM) => findProgramAddress(seeds, program);
41
+ export const lootAddress = (seeds, program = lootProgram()) => findProgramAddress(seeds, program);
29
42
  const at = (...seeds) => lootAddress(seeds)[0];
30
43
 
31
44
  export const pda = {
@@ -104,9 +117,9 @@ export const pda = {
104
117
  /** The program's identity, which signs its requests to the oracle. */
105
118
  identity: () => at(seed('identity')),
106
119
  /** The identity the oracle signs callbacks with: the VRF program's PDA for this program. */
107
- oracleIdentity: () => findProgramAddress([seed('identity'), key(LOOT_PROGRAM)], VRF_PROGRAM)[0],
120
+ oracleIdentity: () => findProgramAddress([seed('identity'), key(lootProgram())], VRF_PROGRAM)[0],
108
121
  /** The program's ProgramData account (its upgrade authority signs `init_platform`). */
109
- programData: () => findProgramAddress([key(LOOT_PROGRAM)], BPF_LOADER_UPGRADEABLE)[0],
122
+ programData: () => findProgramAddress([key(lootProgram())], BPF_LOADER_UPGRADEABLE)[0],
110
123
  };
111
124
 
112
125
  /** `owner`'s associated token account of `mint` (owner may be a PDA: a project's vault, a pool is not one). */
@@ -8,7 +8,7 @@ import IDL from '../client/idl/loot.js';
8
8
  import { gateList } from '../client/grant.js';
9
9
  import { MAX_BID_NFTS, MAX_SHARES } from '../client/ix.js';
10
10
  import { MAX_POTS } from '../client/accounts.js';
11
- import { vault } from '../client/pda.js';
11
+ import { cluster, CLUSTERS, networkOf, realMoney, useCluster, vault } from '../client/pda.js';
12
12
  import { ASSUMED_TRIES, boxBytes, buyFits, drawsFit, MAX_ORDER_BYTES, tableBudget } from '../client/sizes.js';
13
13
  import { TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
14
14
  import { addressBytes, encodeBase58 } from '../chain/base58.js';
@@ -693,10 +693,7 @@ export function dateWords(seconds) {
693
693
  * (`ASSUMED_TRIES`).
694
694
  * Returns { ok, refusals: [{ code, where, message, fix }], warnings: [{ code, where, message, fix? }] }.
695
695
  */
696
- export function check(
697
- economy,
698
- { network = 'devnet', now = Math.floor(Date.now() / 1000), project = economy?.live?.project ?? null } = {},
699
- ) {
696
+ export function check(economy, { now = Math.floor(Date.now() / 1000), project = economy?.live?.project ?? null } = {}) {
700
697
  const refusals = [];
701
698
  const warnings = [];
702
699
  const refuse = (code, where, message, fix) => refusals.push({ code, where, message, fix });
@@ -717,13 +714,27 @@ export function check(
717
714
  `The economy names ${STAND_IN_OWNER}, the address the setup plan stands in for your wallet before one connects. Nobody holds its key: whatever is sent there is lost.`,
718
715
  'Name a real wallet there.',
719
716
  );
720
- if (network !== 'devnet' || (economy?.network && economy.network !== 'devnet'))
717
+ // The economy's cluster: the network it names, devnet when it names none. This process works on one cluster, and
718
+ // the first economy it checks names it when nothing did before.
719
+ const named = networkOf(economy);
720
+ if (!Object.hasOwn(CLUSTERS, named))
721
721
  refuse(
722
722
  'E-NETWORK',
723
723
  'economy.network',
724
- `The economy names the network “${economy?.network ?? network}”. loot runs on devnet only: mainnet waits for an audit.`,
725
- 'Set the network to "devnet".',
724
+ `The economy names the network “${named}”. loot runs on ${Object.keys(CLUSTERS).join(' and on ')}.`,
725
+ 'Set the network to "devnet", or to "mainnet".',
726
726
  );
727
+ else
728
+ try {
729
+ useCluster(named);
730
+ } catch {
731
+ refuse(
732
+ 'E-NETWORK',
733
+ 'economy.network',
734
+ `The economy is on ${named}, and this process works on ${cluster()}: one process works on one cluster.`,
735
+ `Run it where the cluster is ${named}: the xsolana command takes the cluster from the file, and a server or a script is started with XSOLANA_CLUSTER=${named}.`,
736
+ );
737
+ }
727
738
  const oldVersion = economy?.version === 1;
728
739
  if (oldVersion)
729
740
  refuse(
@@ -4925,7 +4936,9 @@ export function check(
4925
4936
  'W-REVENUE-SHARE',
4926
4937
  where,
4927
4938
  `${label(f)} pays stakers a share of what players spend (${revenue.map(x => x.by).join(', ')}). Paying holders out of a project’s revenue is what regulators look at when they decide whether a token is a security.`,
4928
- 'Fine on devnet. Before mainnet, ask counsel, or reward stakers from emissions (a schedule that mints a game token) rather than from sales.',
4939
+ realMoney()
4940
+ ? 'This economy is on mainnet, where that is real money: ask counsel first, or reward stakers from emissions (a schedule that mints a game token) rather than from sales.'
4941
+ : 'Fine on devnet. Before mainnet, ask counsel, or reward stakers from emissions (a schedule that mints a game token) rather than from sales.',
4929
4942
  );
4930
4943
  }
4931
4944
 
@@ -5030,7 +5043,9 @@ export function check(
5030
5043
  'W-TOKEN-SALE',
5031
5044
  `${where}.prize`,
5032
5045
  `${label(c)} sells ${sold.symbol ?? sold.key} for ${tok.symbol ?? tok.key}: each backer pays in and takes a share of the tokens by what they put in. Selling a token to raise money is what securities law looks at first, and many places limit who may buy.`,
5033
- 'Fine on devnet. Before mainnet, ask counsel.',
5046
+ realMoney()
5047
+ ? 'This economy is on mainnet, where that is real money: ask counsel first.'
5048
+ : 'Fine on devnet. Before mainnet, ask counsel.',
5034
5049
  );
5035
5050
  // One NFT, the last units of an item, a pool's prize: the winning entries share it in the order they are claimed.
5036
5051
  const scarce =
@@ -5119,7 +5134,7 @@ export function check(
5119
5134
  warn(
5120
5135
  'W-CHANCE-PRIZE',
5121
5136
  where,
5122
- `${label(c)} lets players put money on an outcome: that is betting, which is regulated in most places once it is real money (state rules on prediction markets are in flux). loot runs on devnet only.`,
5137
+ `${label(c)} lets players put money on an outcome: that is betting, which is regulated in most places once it is real money (state rules on prediction markets are in flux). ${realMoney() ? 'This economy is on mainnet: it is real money.' : 'This economy is on devnet, which is not real money.'}`,
5123
5138
  'Before mainnet: counsel, an age and region gate, and a name that does not call it an investment.',
5124
5139
  );
5125
5140
  }
@@ -5688,14 +5703,14 @@ export function check(
5688
5703
  warn(
5689
5704
  'W-CHANCE-ITEMS',
5690
5705
  'economy',
5691
- 'Players pay for a random collectible they can resell (a loot box or gacha). Some places treat paid loot boxes of tradeable items as gambling, and many require the odds to be shown. loot runs on devnet only; before mainnet, counsel and an age and region gate.',
5706
+ `Players pay for a random collectible they can resell (a loot box or gacha). Some places treat paid loot boxes of tradeable items as gambling, and many require the odds to be shown. ${realMoney() ? 'This economy is on mainnet, so it is real money: counsel and an age and region gate come first.' : 'This economy is on devnet; before mainnet, counsel and an age and region gate.'}`,
5692
5707
  'Tell the person in plain words: paid random items that can be resold may count as gambling in some places; show the odds on the page, and get a lawyer before real money.',
5693
5708
  );
5694
5709
  if (chance)
5695
5710
  warn(
5696
5711
  'W-CHANCE-PRIZE',
5697
5712
  'economy',
5698
- 'Players pay for a chance at money (a box that pays a prize, a lottery). That is gambling in many places once it is real money, and paid packs with a cash buyback are drawing lawsuits. loot runs on devnet only; before mainnet, counsel and an age and region gate.',
5713
+ `Players pay for a chance at money (a box that pays a prize, a lottery). That is gambling in many places once it is real money, and paid packs with a cash buyback are drawing lawsuits. ${realMoney() ? 'This economy is on mainnet, so it is real money: counsel and an age and region gate come first.' : 'This economy is on devnet; before mainnet, counsel and an age and region gate.'}`,
5699
5714
  'Tell the person this in plain words before they go live: paying for a chance at money is gambling in many places, so before real money they need a lawyer and an age and region gate.',
5700
5715
  );
5701
5716
 
@@ -28,6 +28,7 @@ import {
28
28
  import { farmEnds, playSteps } from './plan.mjs';
29
29
  import { TRUST_WORDS } from './trust.mjs';
30
30
  import { inAll, poolFate, round, simulate, unnamedExceptions, unnamedWords } from './simulate.mjs';
31
+ import { realMoney } from '../client/cluster.js';
31
32
 
32
33
  const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
33
34
  const num = x => (Number.isFinite(x) ? String(round(x, 6)) : String(x));
@@ -613,7 +614,7 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
613
614
  );
614
615
  lines.push(
615
616
  '',
616
- 'Devnet only: nothing here is real money. Going live is signed on your own machine by the project’s key file, which is what “your wallet” means above. The command first prints everything it would send and sends nothing; it sends only when run again with the code it printed, which your agent is told to do only after your yes. No seed phrase or wallet key is ever asked for.',
617
+ `${realMoney() ? 'This economy is on mainnet: what it takes and what it pays is real money, and loot has not been independently audited.' : 'This economy is on devnet: nothing here is real money.'} Going live is signed on your own machine by the project’s key file, which is what “your wallet” means above. The command first prints everything it would send and sends nothing; it sends only when run again with the code it printed, which your agent is told to do only after your yes. No seed phrase or wallet key is ever asked for.`,
617
618
  );
618
619
  return {
619
620
  ok: true,
@@ -123,7 +123,7 @@ export const CONVENTIONS = [
123
123
  'A box, a shop item and a recipe take `opens` and `closes` (times), and `perWallet` (in all) and `perWalletEach` ({ "count": 1, "every": "1d" }) to limit each wallet.',
124
124
  'Leave out `id`, `index`, `shopBox` and `live`: they are written when the economy goes live.',
125
125
  'Public data only. A spec never holds a private key or a seed phrase (the checker refuses one, E-SECRET).',
126
- 'Devnet only: "network": "devnet".',
126
+ '"network" is "devnet" (what a file that names none is on: nothing there is real money) or "mainnet" (real money). loot has an address of its own on each, and an economy is on one: build and try it on devnet first.',
127
127
  ];
128
128
 
129
129
  /**
@@ -252,8 +252,10 @@ export const CANNOT = [
252
252
  {
253
253
  what: 'mainnet',
254
254
  words: 'mainnet real money production',
255
- why: 'the program isn’t audited yet',
256
- nearest: ['devnet now; mainnet after the audit'],
255
+ why: 'loot runs on mainnet, with real money, and has not been independently audited; an economy is on devnet unless its file says "network": "mainnet"',
256
+ nearest: [
257
+ 'try it on devnet first, then set "network": "mainnet" in a copy of the file and go live again with real SOL',
258
+ ],
257
259
  },
258
260
  ];
259
261
 
@@ -431,7 +433,7 @@ export function guideText(only) {
431
433
  return [
432
434
  only
433
435
  ? `The part “${only}” of a loot economy (* = required):`
434
- : `A loot economy is one JSON spec (xsolana.json) on the shared loot program ${IDL.metadata.version}, devnet only. Nobody writes a contract: the spec is checked, simulated, and put live on the owner’s own machine, signed by the project’s key file once they have said yes. Its parts (* = required field):`,
436
+ : `A loot economy is one JSON spec (xsolana.json) on the shared loot program ${IDL.metadata.version}, on devnet or on mainnet (its "network"; devnet when it names none). Nobody writes a contract: the spec is checked, simulated, and put live on the owner’s own machine, signed by the project’s key file once they have said yes. Its parts (* = required field):`,
435
437
  ...parts.flatMap(p => [
436
438
  `\n${p.part} (${p.type}): ${p.says}${p.atPlayTime ? `\n At play time: ${p.atPlayTime}.` : ''}`,
437
439
  ...p.fields.map(fieldLine),
@@ -2,7 +2,7 @@
2
2
  // the approval page (js/approve) has the owner's wallet sign, and the full simulation sends them to a local chain.
3
3
  import { encodeBase58 } from '../chain/base58.js';
4
4
  import { randomBytes } from '../chain/bytes.js';
5
- import { LOOT_PROGRAM } from '../client/pda.js';
5
+ import { lootProgram } from '../client/pda.js';
6
6
  import { compressedTree, listOf, model, tokenLabels } from './model.mjs';
7
7
  import { auctionSeller, plan, setupContext } from './plan.mjs';
8
8
 
@@ -95,7 +95,7 @@ export function recordLanded(economy, { ctx, signatures = [], at = new Date().to
95
95
  };
96
96
  const { live: _, previous: __, ...published } = out;
97
97
  out.live = {
98
- program: LOOT_PROGRAM,
98
+ program: lootProgram(),
99
99
  project: ctx.project,
100
100
  // A project made with no collection (`"collection": false`) has none to write: the schema takes an address only.
101
101
  ...(ctx.collection ? { collection: ctx.collection } : {}),
@@ -1649,7 +1649,7 @@ const TABLE_GROUP = 24;
1649
1649
  */
1650
1650
  export function standingAddresses(m) {
1651
1651
  const fixed = [
1652
- loot.LOOT_PROGRAM,
1652
+ loot.lootProgram(),
1653
1653
  loot.pda.platform(),
1654
1654
  loot.pda.identity(),
1655
1655
  loot.VRF_PROGRAM,
@@ -1892,8 +1892,8 @@ export const SETUP_UNIT_PRICE = 1n;
1892
1892
  /**
1893
1893
  * A setup transaction's instructions as the owner's wallet is handed them: led by a compute unit limit and a compute
1894
1894
  * unit price, each once. A wallet that adds its own priority fee (Phantom does) adds it only to a transaction that
1895
- * sets neither, and the approval page sends a transaction only as it prepared it, so every setup transaction sets both
1896
- * itself. The limit is all a transaction can have (1,400,000 units): nothing here measures what a step takes, a box's
1895
+ * sets neither, and the approval page takes no price a wallet puts on a transaction that has one, so every setup
1896
+ * transaction sets both itself: its fee is the plan's own, whatever the wallet. The limit is all a transaction can have (1,400,000 units): nothing here measures what a step takes, a box's
1897
1897
  * write needs it, and no step may fail for want of compute. A limit or a price the instructions already carry is
1898
1898
  * kept.
1899
1899
  */
@@ -12,8 +12,8 @@
12
12
  "description": "The spec format: 2 is loot 0.7 and later."
13
13
  },
14
14
  "network": {
15
- "const": "devnet",
16
- "description": "Devnet only until the program is audited."
15
+ "enum": ["devnet", "mainnet"],
16
+ "description": "The cluster the economy is on: devnet when it names none. loot has an address of its own on each."
17
17
  },
18
18
  "name": {
19
19
  "type": "string",
package/js/mcp/server.mjs CHANGED
@@ -5,13 +5,20 @@
5
5
  // (`xsolana tool`), with the same words. Answers go out as text and as structuredContent.
6
6
  //
7
7
  // stdout is the protocol: everything else goes to stderr.
8
+ import { readFile } from 'node:fs/promises';
9
+ import path from 'node:path';
8
10
  import { validate } from '../economy/validate.mjs';
9
- import { PACKAGE_VERSION, PROGRAM_INFO, REMOTE_TOOLS, tools, VERSION } from './tools.mjs';
11
+ import { CLUSTERS, cluster, networkOf } from '../client/cluster.js';
12
+ import { PACKAGE_VERSION, REMOTE_TOOLS, tools, VERSION } from './tools.mjs';
13
+ import { withCluster } from './with-cluster.mjs';
14
+
15
+ /** Where loot runs, for every agent that connects. */
16
+ const WHERE = `loot ${VERSION} runs on devnet (program ${CLUSTERS.devnet.program}) and on mainnet (program ${CLUSTERS.mainnet.program}). An economy is on devnet, where nothing is real money, unless its file says "network": "mainnet", where everything is. Build and try an economy on devnet first. loot has not been independently audited.`;
10
17
 
11
18
  export const INSTRUCTIONS = [
12
19
  'loot is one shared Solana program for game and community economies (shops, loot boxes, lotteries, auctions, staking farms, crafting, contests, schedules, perks for holders of any NFT, capped rewards). An economy is a JSON spec, not a new smart contract: never write Anchor or Rust for what loot covers.',
13
20
  'Start with xsolana_guide, then xsolana_examples with the person’s own words; write the spec; xsolana_check after every change; xsolana_simulate and xsolana_explain before telling them what it does; xsolana_review before going live (fix its high findings, say honestly what each voucher server checks, and tell the person what a cheater gets); xsolana_save; xsolana_publish when they say to put it live. Going live is signed by a key file on their own machine (xsolana keygen makes one; never ask for, read or print a key) with xsolana approve --keypair, which prints everything it would send and sends nothing until it is run again with the code it printed: do that only after they have read the printout and said yes. That key owns the project for good: a change to an economy that is already live is signed by the key that owns it, never by a new one. A browser wallet’s signing page (approve --browser) only if they ask for one, and they run it themselves.',
14
- `Program ${PROGRAM_INFO.program} on ${PROGRAM_INFO.cluster} (loot ${VERSION}). Devnet only until audited.`,
21
+ WHERE,
15
22
  ].join(' ');
16
23
 
17
24
  /**
@@ -22,7 +29,7 @@ export const REMOTE_INSTRUCTIONS = [
22
29
  INSTRUCTIONS.split(' Start with')[0],
23
30
  'Start with xsolana_guide, then xsolana_examples with the person’s own words; write the spec and pass it whole as "spec" to every tool (this hosted server reads no files); xsolana_check after every change; xsolana_simulate and xsolana_explain before telling them what it does; xsolana_review before going live (fix its high findings, say honestly what each voucher server checks, and tell the person what a cheater gets).',
24
31
  `To go live, the spec goes in a file named xsolana.json on the person’s own computer and is signed there by a key file. In that folder: npx -y @xgamesfun/solana@${PACKAGE_VERSION} keygen ~/.config/xsolana/<name>.json makes the key file (it prints the address, never the key; the person sends that address devnet SOL), then npx -y @xgamesfun/solana@${PACKAGE_VERSION} approve xsolana.json --keypair <that file> prints everything that would be sent and sends nothing. When the person has read that and said yes, the same command with --yes <the code it printed> sends it. Run them yourself if you have a terminal there; otherwise hand them to the person. Never ask for, read or print a key. Say once what the key file is: the project’s owner for good (whoever can read it controls the project; they back it up themselves; nothing restores a lost one) and, unless the spec names a treasury, where what the economy earns lands. A change to an economy that is already live is signed by the key that owns it: never make a new key for it. A browser wallet’s signing page (the same approve command with --browser in place of --keypair), which the person runs themselves, only if they ask for one or a browser wallet owns the project. (xsolana_save and xsolana_publish, which tool texts may name, run only in the local server.)`,
25
- `Program ${PROGRAM_INFO.program} on ${PROGRAM_INFO.cluster} (loot ${VERSION}). Devnet only until audited.`,
32
+ WHERE,
26
33
  ].join(' ');
27
34
 
28
35
  /** A tool as tools/list shows it; on a hosted server, without the `file` argument it can't take. */
@@ -67,12 +74,36 @@ export async function runTool(name, input = {}, ctx = {}) {
67
74
  const bad = inputProblems(tool.inputSchema, input);
68
75
  if (bad.length) return { isError: true, text: bad.join('; ') };
69
76
  try {
70
- return await tool.handler(input, ctx);
77
+ // Each tool runs on the cluster of the economy it is given, so one server answers for devnet and mainnet alike.
78
+ return await withCluster(await clusterOfInput(input, ctx), () => tool.handler(input, ctx));
71
79
  } catch (e) {
72
80
  return { isError: true, text: e.message };
73
81
  }
74
82
  }
75
83
 
84
+ /**
85
+ * The cluster a tool's call is for: the "network" of the economy it is given (passed whole as `spec`, or read from its
86
+ * `file`, which is xsolana.json when none is named), devnet when the economy names none. With no economy to read (no
87
+ * file there, a file that is not JSON, a tool that takes none) or a network loot does not know, it is the server's
88
+ * own cluster, and the tool says what is wrong in its own words.
89
+ */
90
+ async function clusterOfInput(input, ctx) {
91
+ let economy = input?.spec && typeof input.spec === 'object' ? input.spec : null;
92
+ if (!economy && !ctx.remote) {
93
+ try {
94
+ const file = path.resolve(
95
+ ctx.cwd ?? process.cwd(),
96
+ typeof input?.file === 'string' ? input.file : 'xsolana.json',
97
+ );
98
+ economy = JSON.parse(await readFile(file, 'utf8'));
99
+ } catch {
100
+ economy = null;
101
+ }
102
+ }
103
+ const named = economy && typeof economy === 'object' ? networkOf(economy) : null;
104
+ return named && Object.hasOwn(CLUSTERS, named) ? named : cluster();
105
+ }
106
+
76
107
  const jsonable = value =>
77
108
  JSON.parse(
78
109
  JSON.stringify(value, (_, v) =>