@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.
Files changed (50) 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 +16 -4
  8. package/js/approve/server.mjs +31 -26
  9. package/js/approve/with-key.mjs +7 -4
  10. package/js/chain/borsh.js +5 -3
  11. package/js/chain/rpc.js +26 -10
  12. package/js/client/accounts.js +6 -6
  13. package/js/client/cluster.js +136 -0
  14. package/js/client/grant.js +2 -2
  15. package/js/client/ix.js +16 -16
  16. package/js/client/pda.js +18 -5
  17. package/js/economy/check.mjs +28 -13
  18. package/js/economy/explain.mjs +2 -1
  19. package/js/economy/guide.mjs +6 -4
  20. package/js/economy/live.mjs +2 -2
  21. package/js/economy/plan.mjs +1 -1
  22. package/js/economy/schema.json +2 -2
  23. package/js/mcp/server.mjs +35 -4
  24. package/js/mcp/tools.mjs +56 -45
  25. package/js/mcp/with-cluster.mjs +25 -0
  26. package/js/play/index.js +42 -37
  27. package/js/play/sponsor.js +4 -4
  28. package/js/results/index.js +7 -6
  29. package/js/results/server.js +4 -3
  30. package/js/results/wire.js +9 -8
  31. package/package.json +6 -1
  32. package/release.json +6 -1
  33. package/skills/xsolana-economy/SKILL.md +13 -9
  34. package/skills/xsolana-economy/references/apps.md +2 -1
  35. package/skills/xsolana-economy/references/telling-the-person.md +1 -1
  36. package/skills/xsolana-economy/references/trust-and-cheating.md +1 -1
  37. package/types/chain/borsh.d.ts +4 -2
  38. package/types/chain/rpc.d.ts +10 -4
  39. package/types/client/cluster.d.ts +61 -0
  40. package/types/client/idl/loot.d.ts +9 -9
  41. package/types/client/pda.d.ts +1 -1
  42. package/types/economy/check.d.mts +1 -2
  43. package/types/economy/plan.d.mts +14 -14
  44. package/types/mcp/server.d.mts +1 -1
  45. package/types/mcp/tools.d.mts +9 -7
  46. package/types/mcp/with-cluster.d.mts +8 -0
  47. package/types/play/index.d.ts +2 -2
  48. package/types/results/index.d.ts +6 -6
  49. package/types/results/server.d.ts +3 -3
  50. package/types/results/wire.d.ts +8 -8
package/js/play/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  withComputeBudget,
41
41
  withSolWrap,
42
42
  } from '../chain/programs.js';
43
- import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
43
+ import { rightCluster, rpcClient } from '../chain/rpc.js';
44
44
  import { extension, mayStartFrozen, mintPowers, resolveTransfers } from '../chain/token2022.js';
45
45
  import {
46
46
  compileFitting,
@@ -84,14 +84,13 @@ const LOTTERY_STEPS = new Set(['reveal', 'cancel-draw', 'pull', 'payout']);
84
84
  const BATCHED = new Set(['contest', 'release', 'lookup-table', 'close-leaf', ...LOTTERY_STEPS]);
85
85
  const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
86
86
 
87
- const CHAIN = 'solana:devnet';
88
87
  const sleep = ms => new Promise(r => setTimeout(r, ms));
89
88
  const MPL_CORE = loot.MPL_CORE_PROGRAM;
90
89
  const NOBODY = '11111111111111111111111111111111';
91
90
  /** A table made this many slots ago whose transaction never landed was never sent: its blockhashes ran out long since. */
92
91
  const TABLE_UNSENT_SLOTS = 300n;
93
92
  /** A compressed NFT's `prove_leaf`: its record stays good while the leaf is unchanged, so it may go in a transaction of its own first. */
94
- const isProveLeaf = ix => ix.programId === loot.LOOT_PROGRAM && loot.instructionOf(ix.data)?.name === 'prove_leaf';
93
+ const isProveLeaf = ix => ix.programId === loot.lootProgram() && loot.instructionOf(ix.data)?.name === 'prove_leaf';
95
94
  /** A system account must keep this much (rent for 0 bytes) or be emptied. */
96
95
  const RENT_EMPTY = 890_880n;
97
96
  const SIGNATURE_FEE = 5_000n;
@@ -101,7 +100,7 @@ const SIGNATURE_FEE = 5_000n;
101
100
  * `solana:signMessage`, also `signMessage(bytes)`: the app's settle keys are made from one signature of it a session,
102
101
  * so a page that closes can make them again (`recover`).
103
102
  */
104
- export function walletStandardSigner(wallet, account, chain = CHAIN) {
103
+ export function walletStandardSigner(wallet, account, chain = loot.walletChain()) {
105
104
  const feature = wallet.features?.['solana:signTransaction'];
106
105
  if (!feature) throw new Error(`${wallet.name} cannot sign transactions.`);
107
106
  const messages = wallet.features?.['solana:signMessage'];
@@ -165,18 +164,18 @@ const show = (u, decimals) => {
165
164
  };
166
165
 
167
166
  /**
168
- * `client`, kept to devnet: a URL that names mainnet is refused at once, and before its first request goes out the
169
- * chain's genesis block is checked, once (`checkCluster`), so an RPC whose URL hides mainnet or testnet is refused
167
+ * `client`, kept to this page's cluster: a URL that names another is refused at once, and before its first request goes
168
+ * out the chain's genesis block is checked, once (`checkCluster`), so an RPC whose URL hides another cluster is refused
170
169
  * whatever it says. Every RPC reached over http(s) is checked: the app's own client, and one given whose `url` is
171
170
  * http(s). A client with no such URL (a local chain of the caller's own, like the tests' LiteSVM) is used as it is.
172
171
  */
173
- function devnetOnly(client) {
174
- if (typeof client.url === 'string') notMainnet(client.url);
172
+ function onCluster(client) {
173
+ if (typeof client.url === 'string') rightCluster(client.url, loot.cluster());
175
174
  if (!/^(https?|wss?):/i.test(String(client.url ?? ''))) return client;
176
175
  let checked = null;
177
176
  // A check that could not be made (the network did not answer) is made again on the next request.
178
177
  const ready = () =>
179
- (checked ??= checkCluster(client).catch(e => {
178
+ (checked ??= loot.checkOnCluster(client).catch(e => {
180
179
  checked = null;
181
180
  throw e;
182
181
  }));
@@ -203,7 +202,7 @@ function devnetOnly(client) {
203
202
  */
204
203
  export function solanaApp({
205
204
  spec,
206
- rpcUrl = 'https://api.devnet.solana.com',
205
+ rpcUrl = null,
207
206
  rpc = null,
208
207
  wallet,
209
208
  programs = {},
@@ -217,7 +216,13 @@ export function solanaApp({
217
216
  throw new Error(
218
217
  'The rpc given has no call(method, params): a custom rpc needs it (the devnet check, the slot and compressed NFTs are read through it), as well as accounts, account, latestBlockhash, send, confirm and programAccounts.',
219
218
  );
220
- const client = devnetOnly(rpc ?? rpcClient(notMainnet(rpcUrl)));
219
+ // The economy's file names its cluster (devnet when it names none), and this page works on it and no other.
220
+ loot.useCluster(loot.networkOf(spec));
221
+ if (live.program && live.program !== loot.lootProgram())
222
+ throw new Error(
223
+ `This economy went live on the program ${live.program}, which is not loot’s on ${loot.cluster()} (${loot.lootProgram()}): its "network" and its "live" section name two clusters.`,
224
+ );
225
+ const client = onCluster(rpc ?? rpcClient(loot.clusterRpcUrl(rpcUrl)));
221
226
  const project = live.project;
222
227
  /** The settle keys, each named by its job's account (`keys.js`): made again after a reload (`recover`). */
223
228
  const keys = wallet?.address ? keyring({ wallet, project, store: keyStore }) : null;
@@ -252,13 +257,13 @@ export function solanaApp({
252
257
  const read = async (name, address) => {
253
258
  const a = await client.account(address);
254
259
  if (!a) throw new Error(`Nothing at ${address}: is the economy live on this cluster?`);
255
- if (a.owner !== loot.LOOT_PROGRAM) throw new Error(`${address} is not a loot account.`);
260
+ if (a.owner !== loot.lootProgram()) throw new Error(`${address} is not a loot account.`);
256
261
  return loot.decode(name, a.data);
257
262
  };
258
263
  /** A loot account, or null when there is none yet. */
259
264
  const readMaybe = async (name, address) => {
260
265
  const a = await client.account(address);
261
- return a?.owner === loot.LOOT_PROGRAM ? loot.decode(name, a.data) : null;
266
+ return a?.owner === loot.lootProgram() ? loot.decode(name, a.data) : null;
262
267
  };
263
268
  /** The key of the token a lottery's tickets are paid in: the spec's `ticketCurrency`, or the token of its mint. */
264
269
  const ticketCurrencyOf = (l, lottery) =>
@@ -530,7 +535,7 @@ export function solanaApp({
530
535
  const sheetOf = async nft => {
531
536
  const found = await client.account(loot.pda.sheet(project, nft));
532
537
  return Object.fromEntries(
533
- (loot.decodeSheet(found?.owner === loot.LOOT_PROGRAM ? found.data : null)?.attributes ?? []).map(a => [
538
+ (loot.decodeSheet(found?.owner === loot.lootProgram() ? found.data : null)?.attributes ?? []).map(a => [
534
539
  a.key,
535
540
  a.value,
536
541
  ]),
@@ -653,7 +658,7 @@ export function solanaApp({
653
658
  if (loot.readsProfile(rule)) {
654
659
  const found = await client.account(loot.pda.profile(project, wallet.address));
655
660
  const attributes = Object.fromEntries(
656
- (loot.decodeProfile(found?.owner === loot.LOOT_PROGRAM ? found.data : null)?.attributes ?? []).map(a => [
661
+ (loot.decodeProfile(found?.owner === loot.lootProgram() ? found.data : null)?.attributes ?? []).map(a => [
657
662
  a.key,
658
663
  a.value,
659
664
  ]),
@@ -925,7 +930,7 @@ export function solanaApp({
925
930
  const said = String(e?.message ?? '');
926
931
  const m = /custom program error: 0x([0-9a-f]+)/i.exec(said);
927
932
  const failed = e?.logs?.find(line => / failed: custom program error: 0x/i.test(line));
928
- const known = m && (!failed || failed.includes(loot.LOOT_PROGRAM)) ? loot.ERRORS[parseInt(m[1], 16)] : null;
933
+ const known = m && (!failed || failed.includes(loot.lootProgram())) ? loot.ERRORS[parseInt(m[1], 16)] : null;
929
934
  if (!known) throw e;
930
935
  throw Object.assign(new Error(`${known.msg} (${known.name}).`, { cause: e }), {
931
936
  code: parseInt(m[1], 16),
@@ -1080,14 +1085,14 @@ export function solanaApp({
1080
1085
  const watch = [
1081
1086
  ...new Set(
1082
1087
  instructions
1083
- .filter(ix => ix.programId === loot.LOOT_PROGRAM)
1088
+ .filter(ix => ix.programId === loot.lootProgram())
1084
1089
  .flatMap(ix => ix.keys.filter(k => k.isWritable && !k.isSigner).map(k => k.pubkey)),
1085
1090
  ),
1086
1091
  ];
1087
1092
  const after = (await dryRun(wallet.address, instructions, watch)).accounts;
1088
1093
  const waiting = watch.flatMap((address, i) => {
1089
1094
  const a = after[i];
1090
- if (a?.owner !== loot.LOOT_PROGRAM || loot.accountKind(a.data) !== 'Order') return [];
1095
+ if (a?.owner !== loot.lootProgram() || loot.accountKind(a.data) !== 'Order') return [];
1091
1096
  const order = loot.decode('Order', a.data);
1092
1097
  return order.status === 'Waiting' && order.player === wallet.address ? [[address, order]] : [];
1093
1098
  });
@@ -1191,7 +1196,7 @@ export function solanaApp({
1191
1196
  // `landed`: the slot one's own open landed in; the order is read from a node that has seen it.
1192
1197
  const current = async () => {
1193
1198
  const account = await client.account(orderAddress, 'confirmed', landed != null ? { minContextSlot: landed } : {});
1194
- return account?.owner === loot.LOOT_PROGRAM ? loot.decode('Order', account.data) : null;
1199
+ return account?.owner === loot.lootProgram() ? loot.decode('Order', account.data) : null;
1195
1200
  };
1196
1201
  let order = await current();
1197
1202
  // Right after one's own confirmed open, no order is "not seen yet", never "settled already": a node of the RPC may
@@ -2298,7 +2303,7 @@ export function solanaApp({
2298
2303
  const listing = await readMaybe('Listing', listingAt);
2299
2304
  if (listing) out.push({ kind: 'listing', address: listingAt, record: listing });
2300
2305
  const tag = { memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS.Hold)) } };
2301
- const holds = await client.programAccounts(loot.LOOT_PROGRAM, [
2306
+ const holds = await client.programAccounts(loot.lootProgram(), [
2302
2307
  tag,
2303
2308
  { memcmp: { offset: 1, bytes: project } },
2304
2309
  { memcmp: { offset: 33, bytes: asset } },
@@ -2317,7 +2322,7 @@ export function solanaApp({
2317
2322
  */
2318
2323
  async function stashesOf(asset) {
2319
2324
  const at = (offset, bytes) => ({ memcmp: { offset, bytes } });
2320
- const found = await client.programAccounts(loot.LOOT_PROGRAM, [
2325
+ const found = await client.programAccounts(loot.lootProgram(), [
2321
2326
  at(0, encodeBase58(Uint8Array.of(loot.TAGS.Table))),
2322
2327
  at(1, project),
2323
2328
  at(37, encodeBase58(Uint8Array.of(loot.PAYLOAD.Nft))),
@@ -2532,7 +2537,7 @@ export function solanaApp({
2532
2537
  const found = await client.accounts(keys);
2533
2538
  const rounds = keys.map((key, i) => ({
2534
2539
  address: key,
2535
- round: found[i]?.owner === loot.LOOT_PROGRAM ? loot.decodeRound(found[i].data) : null,
2540
+ round: found[i]?.owner === loot.lootProgram() ? loot.decodeRound(found[i].data) : null,
2536
2541
  }));
2537
2542
  return { address, lottery, current: rounds[0].round, rounds: rounds.filter(r => r.round) };
2538
2543
  }
@@ -2587,7 +2592,7 @@ export function solanaApp({
2587
2592
  first && lottery?.owing && lottery.owing !== NOBODY && loot.potIdOf(terms) != null
2588
2593
  ? await client.account(lottery.owing)
2589
2594
  : null;
2590
- const owing = owingAt?.owner === loot.LOOT_PROGRAM ? loot.decodeRound(owingAt.data) : null;
2595
+ const owing = owingAt?.owner === loot.lootProgram() ? loot.decodeRound(owingAt.data) : null;
2591
2596
  // That round's answer says what it left the pot: until it lands (or the round is cancelled), this draw waits.
2592
2597
  if (owing?.status === 'Drawing')
2593
2598
  throw new Error(
@@ -3000,13 +3005,13 @@ export function solanaApp({
3000
3005
  const tag = t => ({ memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS[t])) } });
3001
3006
  const at = (offset, bytes) => ({ memcmp: { offset, bytes } });
3002
3007
  const orders = new Map();
3003
- for (const { pubkey, account } of await client.programAccounts(loot.LOOT_PROGRAM, [
3008
+ for (const { pubkey, account } of await client.programAccounts(loot.lootProgram(), [
3004
3009
  tag('Order'),
3005
3010
  at(1, project),
3006
3011
  at(33, me),
3007
3012
  ]))
3008
3013
  orders.set(pubkey, loot.decode('Order', account.data));
3009
- const holds = (await client.programAccounts(loot.LOOT_PROGRAM, [tag('Hold'), at(1, project), at(65, me)])).map(
3014
+ const holds = (await client.programAccounts(loot.lootProgram(), [tag('Hold'), at(1, project), at(65, me)])).map(
3010
3015
  h => h.pubkey,
3011
3016
  );
3012
3017
  // Keys are named by an order of the wallet's, a hold (a loan's release), or an auction (the releases after its end).
@@ -3140,7 +3145,7 @@ export function solanaApp({
3140
3145
  async payNft(asset, { to = null, reference, ...others } = NO_OPTIONS) {
3141
3146
  only(others, ['to', 'reference'], 'payNft');
3142
3147
  // An NFT paid goes to a wallet: never the project's address or an account of the program (ToTheProject).
3143
- if (to && (to === project || (await client.account(to))?.owner === loot.LOOT_PROGRAM))
3148
+ if (to && (to === project || (await client.account(to))?.owner === loot.lootProgram()))
3144
3149
  throw new Error('An NFT paid can’t go to the project or an account of the program: name a wallet, or burn it.');
3145
3150
  const { before, ...nft } = await nftOf(asset);
3146
3151
  return send([...before, loot.payNftIx({ player: wallet.address, project, asset, to, ...nft, reference })]);
@@ -3158,7 +3163,7 @@ export function solanaApp({
3158
3163
  const there = await client.account(loot.pda.lottery(project, l.id));
3159
3164
  // The terms on sale are the round's own copy once someone bought into it, the lottery's before.
3160
3165
  const { lottery, current } =
3161
- there?.owner === loot.LOOT_PROGRAM ? await roundOf(l) : { lottery: null, current: null };
3166
+ there?.owner === loot.lootProgram() ? await roundOf(l) : { lottery: null, current: null };
3162
3167
  const copy = current ?? lottery;
3163
3168
  const terms = copy?.terms;
3164
3169
  const currency = lottery ? ticketCurrencyOf(l, lottery) : (l.ticketCurrency ?? currencyKey);
@@ -3718,7 +3723,7 @@ export function solanaApp({
3718
3723
  const nfts = f.stakes === 'collectibles';
3719
3724
  // A seat names its player (offset 1), not its farm: a seat of this farm is the one at its own address here.
3720
3725
  const found = (
3721
- await client.programAccounts(loot.LOOT_PROGRAM, [
3726
+ await client.programAccounts(loot.lootProgram(), [
3722
3727
  { memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS.Seat)) } },
3723
3728
  { memcmp: { offset: 1, bytes: player } },
3724
3729
  ])
@@ -4545,7 +4550,7 @@ export function solanaApp({
4545
4550
  ...(maker ? [{ memcmp: { offset: 33, bytes: maker } }] : []),
4546
4551
  ];
4547
4552
  const time = await chainNow();
4548
- const found = (await client.programAccounts(loot.LOOT_PROGRAM, filters)).map(({ pubkey, account }) => {
4553
+ const found = (await client.programAccounts(loot.lootProgram(), filters)).map(({ pubkey, account }) => {
4549
4554
  const listing = loot.decode('Listing', account.data);
4550
4555
  return { listing: pubkey, ...listing, renter: loot.renterOf(listing, time) };
4551
4556
  });
@@ -5050,7 +5055,7 @@ export function solanaApp({
5050
5055
  const slot = await slotNow();
5051
5056
  const time = BigInt(now());
5052
5057
  const chainTime = await chainNow();
5053
- for (const { pubkey, account } of await client.programAccounts(loot.LOOT_PROGRAM, [
5058
+ for (const { pubkey, account } of await client.programAccounts(loot.lootProgram(), [
5054
5059
  tag('Order'),
5055
5060
  at(1, project),
5056
5061
  at(33, me),
@@ -5068,7 +5073,7 @@ export function solanaApp({
5068
5073
  says: 'A roll you won (a mission’s, a recipe’s, a prize’s): ask for it and see what it gives (one whose box has nothing left to give waits until it has).',
5069
5074
  });
5070
5075
  }
5071
- for (const { pubkey, account } of await client.programAccounts(loot.LOOT_PROGRAM, [
5076
+ for (const { pubkey, account } of await client.programAccounts(loot.lootProgram(), [
5072
5077
  tag('Contest'),
5073
5078
  at(1, project),
5074
5079
  ])) {
@@ -5076,7 +5081,7 @@ export function solanaApp({
5076
5081
  const off = loot.contestOff(contest, time);
5077
5082
  if (!(off || contest.verdict === 'Won')) continue;
5078
5083
  const list = await client.account(loot.contestEntries(pubkey, contest));
5079
- const entries = list?.owner === loot.LOOT_PROGRAM ? loot.decodeEntries(list.data) : [];
5084
+ const entries = list?.owner === loot.lootProgram() ? loot.decodeEntries(list.data) : [];
5080
5085
  const entry = entries.find(e => e.player === me);
5081
5086
  if (!entry) continue;
5082
5087
  const won = !off && Number(contest.winner) === Number(entry.side);
@@ -5134,7 +5139,7 @@ export function solanaApp({
5134
5139
  // wallet made: one search of the project's holds finds them all.
5135
5140
  const g = await projectNow();
5136
5141
  const holdsOf = [me, ...(g.treasury === me ? [project] : [])];
5137
- const holds = (await client.programAccounts(loot.LOOT_PROGRAM, [tag('Hold'), at(1, project)]))
5142
+ const holds = (await client.programAccounts(loot.lootProgram(), [tag('Hold'), at(1, project)]))
5138
5143
  .map(({ pubkey, account }) => ({ pubkey, hold: loot.decode('Hold', account.data) }))
5139
5144
  .filter(({ hold }) => holdsOf.includes(hold.player) || hold.debt?.lender === me);
5140
5145
  // Whether the project still holds each one's NFT: whoever controls its collection can take it back or burn it.
@@ -5157,7 +5162,7 @@ export function solanaApp({
5157
5162
  out.push({ ...claim, says: 'A loan you made is overdue and unpaid: take its NFT.' });
5158
5163
  }
5159
5164
  // A compressed NFT's proof record this wallet paid for and left open: closing it brings its rent back.
5160
- for (const { account } of await client.programAccounts(loot.LOOT_PROGRAM, [tag('Proven')])) {
5165
+ for (const { account } of await client.programAccounts(loot.lootProgram(), [tag('Proven')])) {
5161
5166
  const leaf = (() => {
5162
5167
  try {
5163
5168
  return loot.decodeProven(account.data);
@@ -5335,7 +5340,7 @@ export function solanaApp({
5335
5340
  out.push({ kind: 'vest', key: sc.key, says: `What has vested of “${sc.name ?? sc.key}”: claim it.` });
5336
5341
  }
5337
5342
  // The player's listings whose rental is over: their NFT can go home (or wait for the next renter).
5338
- const mineListed = await client.programAccounts(loot.LOOT_PROGRAM, [tag('Listing'), at(1, project), at(33, me)]);
5343
+ const mineListed = await client.programAccounts(loot.lootProgram(), [tag('Listing'), at(1, project), at(33, me)]);
5339
5344
  for (const { account } of mineListed) {
5340
5345
  const listing = loot.decode('Listing', account.data);
5341
5346
  if (listing.deal.Rent && listing.user !== NOBODY && !loot.renterOf(listing, chainTime))
@@ -5535,7 +5540,7 @@ export function solanaApp({
5535
5540
  : await read('Lottery', loot.pda.lottery(project, l.id));
5536
5541
  const key = c.round ?? loot.currentRound(loot.pda.lottery(project, l.id), lottery);
5537
5542
  const found = await client.account(key);
5538
- const r = found?.owner === loot.LOOT_PROGRAM ? loot.decodeRound(found.data) : null;
5543
+ const r = found?.owner === loot.lootProgram() ? loot.decodeRound(found.data) : null;
5539
5544
  // The ask: the current round's once due, or an older round's again (after the timeout, it cancels the round).
5540
5545
  if (c.kind === 'reveal' || c.kind === 'cancel-draw') {
5541
5546
  const steps = await drawStep(l, lottery, c.round ?? null, r, me);
@@ -5617,7 +5622,7 @@ export function solanaApp({
5617
5622
  const carryOn = async signature => {
5618
5623
  if (build || !partial) return signature;
5619
5624
  const after = await client.account(key);
5620
- const r2 = after?.owner === loot.LOOT_PROGRAM ? loot.decodeRound(after.data) : null;
5625
+ const r2 = after?.owner === loot.lootProgram() ? loot.decodeRound(after.data) : null;
5621
5626
  const moved = r2 && (Number(r2.assigned) > Number(r.assigned) || Number(r2.owed) < Number(r.owed));
5622
5627
  if (!moved) return signature;
5623
5628
  const left =
@@ -36,7 +36,7 @@ import { transferLamports } from '../chain/programs.js';
36
36
  import { encodeBase58 } from '../chain/base58.js';
37
37
  import { fromBase64, toBase64 } from '../chain/bytes.js';
38
38
  import { readMessage, readTransaction, withSignature } from '../chain/tx.js';
39
- import { LOOT_PROGRAM, MPL_CORE_PROGRAM, pda, VRF_PROGRAM } from '../client/index.js';
39
+ import { lootProgram, MPL_CORE_PROGRAM, pda, VRF_PROGRAM } from '../client/index.js';
40
40
 
41
41
  const SYSTEM_PROGRAM = '11111111111111111111111111111111';
42
42
  /** What a signature costs the fee payer, in lamports. */
@@ -82,7 +82,7 @@ export function sponsoredCost({ addresses, pre, post, project, skip = [], progra
82
82
  }
83
83
  if (before) continue;
84
84
  const opened =
85
- after.owner === LOOT_PROGRAM ||
85
+ after.owner === lootProgram() ||
86
86
  (after.owner === MPL_CORE_PROGRAM && !programs.includes(MPL_CORE_PROGRAM)) ||
87
87
  ((after.owner === TOKEN_PROGRAM || after.owner === TOKEN_2022_PROGRAM) &&
88
88
  after.data?.length >= 32 &&
@@ -97,7 +97,7 @@ export function stepShape(instructions) {
97
97
  return {
98
98
  programs: [...new Set(instructions.map(ix => ix.programId))],
99
99
  lootNamed: [
100
- ...new Set(instructions.filter(ix => ix.programId === LOOT_PROGRAM).flatMap(ix => ix.keys.map(k => k.pubkey))),
100
+ ...new Set(instructions.filter(ix => ix.programId === lootProgram()).flatMap(ix => ix.keys.map(k => k.pubkey))),
101
101
  ],
102
102
  };
103
103
  }
@@ -180,7 +180,7 @@ export async function checkSponsored(
180
180
  if (program === COMPUTE_BUDGET_PROGRAM && ix.data[0] === 2 && ix.data.length >= 5)
181
181
  units = BigInt(u32At(ix.data, 1));
182
182
  if (program === COMPUTE_BUDGET_PROGRAM && ix.data[0] === 3 && ix.data.length >= 9) price = u64At(ix.data, 1);
183
- if (program === LOOT_PROGRAM && accounts.includes(project)) ours = true;
183
+ if (program === lootProgram() && accounts.includes(project)) ours = true;
184
184
  }
185
185
  if (!ours) return no('It does nothing in this project.');
186
186
  if (price > BigInt(maxPriorityMicroLamports)) return no('Its priority fee is more than this sponsor pays.');
@@ -22,6 +22,7 @@ import { decodeBase58, encodeBase58 } from '../chain/base58.js';
22
22
  import { toHex } from '../chain/bytes.js';
23
23
  import { pda, voucherBytes } from '../client/index.js';
24
24
  import { canonicalJson, drawText, entryText, lockText, resultBytes, rosterHash } from './wire.js';
25
+ import { cluster as onCluster } from '../client/cluster.js';
25
26
 
26
27
  export {
27
28
  canonicalJson,
@@ -40,7 +41,7 @@ export {
40
41
  rosterHash,
41
42
  } from './wire.js';
42
43
 
43
- /** xgames.fun's results server, on its play host (devnet until the program's audit). */
44
+ /** xgames.fun's results server, on its play host. One host answers for devnet and for mainnet: each request names its cluster. */
44
45
  export const RESULTS_SERVER = 'https://play.xgames.fun/xsolana';
45
46
 
46
47
  const subtle = () => globalThis.crypto.subtle;
@@ -112,7 +113,7 @@ function base64url(text) {
112
113
  }
113
114
 
114
115
  /** One signer's signature of a result: { key, signature }. `signer` is a key from newKey, or anything with sign(). */
115
- export async function signResult(signer, result, cluster = 'devnet') {
116
+ export async function signResult(signer, result, cluster = onCluster()) {
116
117
  return { key: signer.address, signature: encodeBase58(await signer.sign(resultBytes({ ...result, cluster }))) };
117
118
  }
118
119
 
@@ -129,7 +130,7 @@ export async function report({
129
130
  round,
130
131
  endedAt,
131
132
  winners,
132
- cluster = 'devnet',
133
+ cluster = onCluster(),
133
134
  fetchImpl = globalThis.fetch,
134
135
  }) {
135
136
  const result = { game, round, endedAt, winners };
@@ -274,7 +275,7 @@ export const draws = {
274
275
  token = null,
275
276
  wallet = null,
276
277
  signMessage = null,
277
- cluster = 'devnet',
278
+ cluster = onCluster(),
278
279
  fetchImpl = globalThis.fetch,
279
280
  }) => {
280
281
  if (token) return post(server, `/v1/draws/${seg(game)}/${seg(draw)}/entries`, { token }, fetchImpl);
@@ -295,7 +296,7 @@ export const draws = {
295
296
  signMessage,
296
297
  by,
297
298
  cert = null,
298
- cluster = 'devnet',
299
+ cluster = onCluster(),
299
300
  net = 'quicknet',
300
301
  fetchImpl = globalThis.fetch,
301
302
  }) => {
@@ -345,7 +346,7 @@ export const giveaways = {
345
346
  endedAt,
346
347
  winners,
347
348
  grant,
348
- cluster = 'devnet',
349
+ cluster = onCluster(),
349
350
  fetchImpl = globalThis.fetch,
350
351
  }) => {
351
352
  const signed = winners.map(w => ({ wallet: w.wallet, place: w.place, grant }));
@@ -19,6 +19,7 @@ import { MPL_CORE_PROGRAM, ownedAssetFilters, parseCoreAsset } from '../chain/co
19
19
  import { rpcClient } from '../chain/rpc.js';
20
20
  import { sha256 } from '../chain/sha256.js';
21
21
  import { signVoucher } from './index.js';
22
+ import { cluster as onCluster } from '../client/cluster.js';
22
23
 
23
24
  const subtle = () => globalThis.crypto.subtle;
24
25
  const rpcOf = rpc => (typeof rpc === 'string' ? rpcClient(rpc) : rpc);
@@ -70,7 +71,7 @@ const oneLine = (name, value) => {
70
71
  };
71
72
 
72
73
  /** The exact text a wallet signs to prove it is its owner's: fixed lines, so nothing else can be passed off as it. */
73
- export const proofText = ({ cluster = 'devnet', game, wallet, purpose, nonce, expires }) =>
74
+ export const proofText = ({ cluster = onCluster(), game, wallet, purpose, nonce, expires }) =>
74
75
  [
75
76
  PROOF_TITLE,
76
77
  ...PROOF_FIELDS.map(f => `${f}: ${oneLine(f, { cluster, game, wallet, purpose, nonce, expires }[f])}`),
@@ -83,7 +84,7 @@ export const proofText = ({ cluster = 'devnet', game, wallet, purpose, nonce, ex
83
84
  * (unix seconds, `seconds` from now) and takes each nonce once, so a signed proof can't be replayed.
84
85
  */
85
86
  export function proofRequest({
86
- cluster = 'devnet',
87
+ cluster = onCluster(),
87
88
  game,
88
89
  wallet,
89
90
  purpose = 'sign in',
@@ -127,7 +128,7 @@ export async function verifyProof({
127
128
  wallet,
128
129
  game,
129
130
  purpose = null,
130
- cluster = 'devnet',
131
+ cluster = onCluster(),
131
132
  now = Math.floor(Date.now() / 1000),
132
133
  }) {
133
134
  const fields = readProof(text);
@@ -2,6 +2,7 @@
2
2
  // with whoever signs them: a host server, a player's game page, an owner's wallet. Browser-safe: no Node imports.
3
3
  import { addressBytes } from '../chain/base58.js';
4
4
  import { concat, fromHex, uintLE, utf8 } from '../chain/bytes.js';
5
+ import { cluster as onCluster } from '../client/cluster.js';
5
6
 
6
7
  export const RESULT_DOMAIN = 'loot-result-v1';
7
8
  export const NO_GRANT = 0xffff;
@@ -31,10 +32,10 @@ const write = (kind, values) =>
31
32
  [CERT_KINDS[kind].title, ...CERT_KINDS[kind].fields.map(f => `${f}: ${field(f, values[f])}`)].join('\n');
32
33
 
33
34
  /** The text the owner signs to let `host` report `game`'s results until `until` (unix seconds). */
34
- export const hostCertText = ({ cluster = 'devnet', game, host, until }) =>
35
+ export const hostCertText = ({ cluster = onCluster(), game, host, until }) =>
35
36
  write('host', { cluster, game, host, until });
36
37
  /** The text the owner signs to take `host` back. */
37
- export const revokeText = ({ cluster = 'devnet', game, host }) => write('revoke', { cluster, game, host });
38
+ export const revokeText = ({ cluster = onCluster(), game, host }) => write('revoke', { cluster, game, host });
38
39
 
39
40
  /**
40
41
  * The text the organizer (the game's owner or authority, or a host key they certified) signs to open a draw: a round
@@ -45,7 +46,7 @@ export const revokeText = ({ cluster = 'devnet', game, host }) => write('revoke'
45
46
  * JSON), or 'default'.
46
47
  */
47
48
  export const drawText = ({
48
- cluster = 'devnet',
49
+ cluster = onCluster(),
49
50
  game,
50
51
  draw,
51
52
  engine,
@@ -69,19 +70,19 @@ export const drawText = ({
69
70
  rates,
70
71
  });
71
72
  /** The text the organizer signs to lock a draw's players (`roster`: rosterHash) to a beacon round still to come. */
72
- export const lockText = ({ cluster = 'devnet', game, draw, roster, players, beacon, net = 'quicknet' }) =>
73
+ export const lockText = ({ cluster = onCluster(), game, draw, roster, players, beacon, net = 'quicknet' }) =>
73
74
  write('lock', { cluster, game, draw, roster, players, beacon: `${net} ${beacon}` });
74
75
  /** The text a player's wallet signs to enter a draw (identity 'wallet'). */
75
- export const entryText = ({ cluster = 'devnet', game, draw }) => write('entry', { cluster, game, draw });
76
+ export const entryText = ({ cluster = onCluster(), game, draw }) => write('entry', { cluster, game, draw });
76
77
 
77
78
  /**
78
79
  * The text the owner (or authority) signs to appoint a giveaway host: `who` is "x:<X account id>" (the host signs in
79
80
  * with that X account through the play host) or a wallet address; their giveaways pay from grant `grant` alone.
80
81
  */
81
- export const giveawayHostText = ({ cluster = 'devnet', game, who, slug, grant, until }) =>
82
+ export const giveawayHostText = ({ cluster = onCluster(), game, who, slug, grant, until }) =>
82
83
  write('giveaway', { cluster, game, who, slug, grant, until });
83
84
  /** The text the owner (or authority) signs to take a giveaway host back. */
84
- export const revokeGiveawayText = ({ cluster = 'devnet', game, who }) =>
85
+ export const revokeGiveawayText = ({ cluster = onCluster(), game, who }) =>
85
86
  write('revokeGiveaway', { cluster, game, who });
86
87
 
87
88
  /** A draw's players, as one hash: each `id<TAB>wallet`, sorted by id, one a line; sha256, hex. */
@@ -113,7 +114,7 @@ export function canonicalJson(value) {
113
114
  * Every field has one length, so one set of bytes means one result: an unknown cluster, a round that is not 16 bytes, a
114
115
  * place past 255 and more than 255 winners are refused, never written as something else.
115
116
  */
116
- export function resultBytes({ cluster = 'devnet', game, round, endedAt, winners }) {
117
+ export function resultBytes({ cluster = onCluster(), game, round, endedAt, winners }) {
117
118
  if (!Object.hasOwn(CLUSTERS, cluster))
118
119
  throw new RangeError(
119
120
  `Unknown cluster ${JSON.stringify(cluster)}: a result names ${Object.keys(CLUSTERS).join(' or ')}.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xgamesfun/solana",
3
- "version": "0.28.1",
3
+ "version": "0.28.2",
4
4
  "mcpName": "io.github.XGamesFun/solana-kit",
5
5
  "description": "Loot boxes, gacha, shops, lotteries, auctions, NFT staking, crafting and rewards on Solana without writing a smart contract: one shared program, its JS client, a CLI, and an MCP server and skill so Claude Code, Codex or any AI agent can design, check, simulate and launch an economy.",
6
6
  "license": "Apache-2.0",
@@ -80,6 +80,10 @@
80
80
  "./mcp/http": {
81
81
  "types": "./types/mcp/http.d.mts",
82
82
  "default": "./js/mcp/http.mjs"
83
+ },
84
+ "./with-cluster": {
85
+ "types": "./types/mcp/with-cluster.d.mts",
86
+ "default": "./js/mcp/with-cluster.mjs"
83
87
  }
84
88
  },
85
89
  "sideEffects": [
@@ -102,6 +106,7 @@
102
106
  ],
103
107
  "scripts": {
104
108
  "test": "node --test \"js/test/**/*.test.mjs\"",
109
+ "test:mainnet": "node scripts/test-mainnet.mjs",
105
110
  "lint": "eslint . && prettier --check .",
106
111
  "format": "prettier --write .",
107
112
  "types": "tsc -p tsconfig.types.json",
package/release.json CHANGED
@@ -4,5 +4,10 @@
4
4
  "sbfBytes": 1794616,
5
5
  "sbfSha256": "ac8b09ed2c3a3f7daa2dfab55d2f6dea19fa43e4c98b44a82da2a68a7bc8ebbe",
6
6
  "idlSha256": "ef3b8043d62c606e850df75773b4535cdb08206216e17aca653111a77403abd7",
7
- "clientSha256": "3ef11453089e952e50d9d5a7199d9629b3417a091a071ea6dfd90f42746a2421"
7
+ "clientSha256": "164ed8ee25b5a2714f6c8f5de243d6b650ba0741637ee3dd4c2886f15af3a6c5",
8
+ "mainnet": {
9
+ "programId": "xgames8C758PyweWB9sDw8evvwsTVwyA2fGRUeAXVPy",
10
+ "sbfBytes": 1794616,
11
+ "sbfSha256": "f78e4f0ef38b5f1673936044263293e91172b4bac92c741cc7bb9567617bbdf6"
12
+ }
8
13
  }
@@ -8,11 +8,12 @@ description: Build a web3, token or NFT economy on Solana without writing a smar
8
8
  An economy is one JSON spec (`xsolana.json` in the project) on **loot**, a Solana program every project shares. Nobody
9
9
  writes or deploys a contract: the spec is checked, simulated and priced by the `xsolana_*` tools, and it goes live
10
10
  signed by a key file on the owner's own machine, once they have read what will be sent and said yes. It runs on
11
- **devnet** until the program is audited.
11
+ **devnet**, where nothing is real money, and on **mainnet**, where everything is: an economy is on devnet unless its
12
+ file says `"network": "mainnet"`.
12
13
 
13
- The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.28.1 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
14
- `npx -y @xgamesfun/solana@0.28.1 token <mint>` for a coin made elsewhere,
15
- or `npx -y @xgamesfun/solana@0.28.1 tool <name> '<json>'` for any tool (always with the exact version).
14
+ The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.28.2 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
15
+ `npx -y @xgamesfun/solana@0.28.2 token <mint>` for a coin made elsewhere,
16
+ or `npx -y @xgamesfun/solana@0.28.2 tool <name> '<json>'` for any tool (always with the exact version).
16
17
 
17
18
  ## Rules (always)
18
19
 
@@ -33,7 +34,10 @@ or `npx -y @xgamesfun/solana@0.28.1 tool <name> '<json>'` for any tool (always w
33
34
  owner: whoever can read it controls the project. Never open it, copy it or print it, and never put it in a chat, a
34
35
  spec, a repository or a page. Never ask for a seed phrase or the key of a wallet the person already has; if they
35
36
  paste one, tell them to treat it as exposed and move its funds, and write it nowhere.
36
- 3. **Devnet only.** Nothing here is real money yet; say so.
37
+ 3. **Devnet first; mainnet only on the person's own word.** Build, check and try every economy on devnet, and say
38
+ that nothing there is real money. Write `"network": "mainnet"` only when the person asks for mainnet in their own
39
+ words, never to make something feel real. Tell them then, before anything is sent: it is real money, loot has not
40
+ been independently audited, what is sent can be lost, and what the review and the checker's legal warnings say.
37
41
  4. **The checker decides.** Never save or publish a spec with refusals. Fix each refusal the way its `fix` says.
38
42
  5. **Numbers come from the tools.** What a box gives back, how long a pool lasts, what going live costs: quote
39
43
  `xsolana_simulate`, `xsolana_cost` and `xsolana_explain`, never your own arithmetic. What a player pays is the tools'
@@ -137,7 +141,7 @@ or `npx -y @xgamesfun/solana@0.28.1 tool <name> '<json>'` for any tool (always w
137
141
  9. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the commands, which you
138
142
  run in the project folder exactly as given (they name the exact version; never drop it):
139
143
  - **A new economy gets a key file**, unless the project has one:
140
- `npx -y @xgamesfun/solana@0.28.1 keygen ~/.config/xsolana/<name>.json`. Give the person the address it prints. A
144
+ `npx -y @xgamesfun/solana@0.28.2 keygen ~/.config/xsolana/<name>.json`. Give the person the address it prints. A
141
145
  Solana key file they already have and name themselves (`~/.config/solana/id.json`) works the same; never go
142
146
  looking for one.
143
147
  - **Say what the key file is** before anything is sent: the project's owner, and, unless the spec says otherwise,
@@ -148,11 +152,11 @@ or `npx -y @xgamesfun/solana@0.28.1 tool <name> '<json>'` for any tool (always w
148
152
  set `"treasury": "wallet:<address>"` first: it can't be changed through the spec once live. Tell them to back
149
153
  the file up themselves (a USB stick, a password manager): if it is lost, nobody can change the economy, decide
150
154
  a contest or move what its address holds, and nothing restores it. You never open or copy it.
151
- - **Everything the setup takes from the owner must be at the key's address first**: the devnet SOL
152
- (`xsolana_cost`), any seed in a coin that already exists, and every NFT a stash, a raffle or an auction names. A
155
+ - **Everything the setup takes from the owner must be at the key's address first**: the SOL (`xsolana_cost`;
156
+ devnet SOL for an economy on devnet, real SOL for one on mainnet), any seed in a coin that already exists, and every NFT a stash, a raffle or an auction names. A
153
157
  collection they already have needs the key's address as its update authority. If they would rather not move
154
158
  those, offer the browser wallet.
155
- - **Show it**: `npx -y @xgamesfun/solana@0.28.1 approve xsolana.json --keypair <key file>` prints every step with
159
+ - **Show it**: `npx -y @xgamesfun/solana@0.28.2 approve xsolana.json --keypair <key file>` prints every step with
156
160
  what it pays and trusts, the warnings, the notice and a code, and sends nothing. Give the person that printout as
157
161
  it is, not a summary of it, and ask.
158
162
  - **On their yes**, the same command with `--yes <code>`. What lands is written back into `xsolana.json`. If it
@@ -76,7 +76,8 @@ await app.balances(); // what they hold of each of the economy's tokens
76
76
  }
77
77
  </script>
78
78
  ```
79
- Tell the person the one command that starts it, and that it serves on devnet.
79
+ Tell the person the one command that starts it, and which cluster its economy is on (devnet unless its file says
80
+ mainnet).
80
81
  - **What a roll looks like**: a box of several outcomes is rolled by the oracle a few seconds after the open; `openBox`
81
82
  waits for it. Show the player that it is rolling.
82
83
  - **A page that closes before the settle**: the SOL its settle key holds is not lost in the same browser (on another device, with nothing stored, it can't be found yet). Build the wallet with
@@ -29,7 +29,7 @@ and the spec differ (a limit, an amount, a key not set yet), change one of them
29
29
  a score): "Checking that a player really did it is up to you, on your own server: a game runs in the player's
30
30
  browser, and the program trusts the authority's signature."
31
31
  6. **Flags for the law, once.** `W-CHANCE-PRIZE`: "Players pay for a chance at money. That is gambling in many places once
32
- it is real money; this runs on devnet only." `W-CHANCE-ITEMS`: "Players pay for random items they can resell: some
32
+ it is real money; this one is on devnet, which is not." (On mainnet: "this one is on mainnet, so it is.") `W-CHANCE-ITEMS`: "Players pay for random items they can resell: some
33
33
  places treat that as gambling, and many require the odds to be shown." `W-REVENUE-SHARE`: "Stakers are paid from
34
34
  what players spend; regulators look at that when they decide whether a token is a security." `W-TOKEN-SALE`:
35
35
  "People pay for a share of a new token; selling a token to raise money is what securities law looks at first."
@@ -104,7 +104,7 @@ Run `xsolana_review` and go through what it reports; these are the points behind
104
104
  11. **The authority and the treasury.** The owner's wallet alone changes every price, rate, grant, limit and voucher
105
105
  key; the authority mints, moves pool funds, decides contests and releases holds, with no caps. On devnet the
106
106
  owner's key is the key file the setup was signed with: readable by its user alone, outside any repository, never
107
- pasted anywhere. With real money (mainnet is not open) those keys belong in a hardware wallet or a multisig; a
107
+ pasted anywhere. With real money (an economy on mainnet) those keys belong in a hardware wallet or a multisig; a
108
108
  server that signs gets an authority key of its own, never the owner's.
109
109
  12. **The page is untrusted.** A page builds the player's transactions, and a cheater builds their own: the program's
110
110
  checks are what count, never the page's. Anything the page decides (a score, a reward amount, who won) is only as