@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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The cluster the client works on, and loot's address there. loot has an address of its own on each cluster, so what
|
|
2
|
+
// is signed for one (a voucher, a verdict) never counts on the other, and every address this client derives, every
|
|
3
|
+
// instruction it builds and everything it signs follows the cluster's address.
|
|
4
|
+
//
|
|
5
|
+
// A process works on one cluster. It is devnet unless the process names another: the environment variable
|
|
6
|
+
// XSOLANA_CLUSTER, or `useCluster` before anything is derived or built. Once an address has been derived the
|
|
7
|
+
// cluster is settled, and naming another is refused: addresses of two clusters never mix by accident.
|
|
8
|
+
//
|
|
9
|
+
// A server that answers for both clusters gives each piece of work its own: `withCluster` (js/mcp/with-cluster.mjs, Node)
|
|
10
|
+
// runs a function, and everything it awaits, on the cluster named; inside it the client is on that cluster whatever
|
|
11
|
+
// the process's own is. Browser-safe.
|
|
12
|
+
import { checkCluster, rightCluster } from '../chain/rpc.js';
|
|
13
|
+
import IDL from './idl/loot.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Each cluster: loot's program there (devnet's is the IDL's own address), the chain as a wallet names it (Wallet
|
|
17
|
+
* Standard), and the RPC used when none is given. Mainnet has none: its public RPC refuses or limits what loot reads
|
|
18
|
+
* (a program's accounts, a compressed NFT's proof), so a mainnet RPC is always named (XSOLANA_RPC_URL, or `rpcUrl`).
|
|
19
|
+
*/
|
|
20
|
+
export const CLUSTERS = Object.freeze({
|
|
21
|
+
devnet: Object.freeze({ program: IDL.address, chain: 'solana:devnet', rpc: 'https://api.devnet.solana.com' }),
|
|
22
|
+
mainnet: Object.freeze({
|
|
23
|
+
program: 'xgames8C758PyweWB9sDw8evvwsTVwyA2fGRUeAXVPy',
|
|
24
|
+
chain: 'solana:mainnet',
|
|
25
|
+
rpc: null,
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const known = name => {
|
|
30
|
+
if (!Object.hasOwn(CLUSTERS, name))
|
|
31
|
+
throw new Error(`"${name}" is not a cluster loot knows: ${Object.keys(CLUSTERS).join(' or ')}.`);
|
|
32
|
+
return name;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const fromEnv = globalThis.process?.env?.XSOLANA_CLUSTER?.trim() || null;
|
|
36
|
+
let name = known(fromEnv ?? 'devnet');
|
|
37
|
+
// Settled: named by the environment or by `useCluster`, or already used to derive an address.
|
|
38
|
+
let settled = Boolean(fromEnv);
|
|
39
|
+
// The cluster of the work being done now, when a server gives each piece of work its own (`scopeClusters`).
|
|
40
|
+
let scoped = null;
|
|
41
|
+
const within = () => {
|
|
42
|
+
const cluster = scoped?.();
|
|
43
|
+
return cluster ? known(cluster) : null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* loot's program on this process's own cluster. A live binding: it follows `useCluster`. It knows nothing of
|
|
48
|
+
* `withCluster`: code that may run for either cluster asks `lootProgram()`, as the client itself does.
|
|
49
|
+
* @type {string}
|
|
50
|
+
*/
|
|
51
|
+
export let LOOT_PROGRAM = CLUSTERS[name].program;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* For a server's own set-up (js/mcp/with-cluster.mjs does it): `current()` answers the cluster of the work being done now,
|
|
55
|
+
* or nothing outside any. While it answers one, the client is on that cluster.
|
|
56
|
+
* @param {(() => string | undefined | null) | null} current
|
|
57
|
+
*/
|
|
58
|
+
export function scopeClusters(current) {
|
|
59
|
+
scoped = current;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Names the cluster this process works on (`devnet` or `mainnet`) and returns it. Call it first, before any address
|
|
64
|
+
* is derived: it throws when the process already works on another cluster. Inside `withCluster` it names nothing: it
|
|
65
|
+
* throws unless the cluster named is the one that work is on.
|
|
66
|
+
* @param {'devnet' | 'mainnet'} cluster
|
|
67
|
+
* @returns {'devnet' | 'mainnet'}
|
|
68
|
+
*/
|
|
69
|
+
export function useCluster(cluster) {
|
|
70
|
+
known(cluster);
|
|
71
|
+
const here = within();
|
|
72
|
+
if (here) {
|
|
73
|
+
if (cluster !== here) throw new Error(`This is being done on ${here}; it can't also be done on ${cluster}.`);
|
|
74
|
+
return here;
|
|
75
|
+
}
|
|
76
|
+
if (cluster !== name) {
|
|
77
|
+
if (settled)
|
|
78
|
+
throw new Error(
|
|
79
|
+
`This process already works on ${name}; it can't also work on ${cluster}. Start one for ${cluster} (XSOLANA_CLUSTER=${cluster}).`,
|
|
80
|
+
);
|
|
81
|
+
name = cluster;
|
|
82
|
+
LOOT_PROGRAM = CLUSTERS[name].program;
|
|
83
|
+
}
|
|
84
|
+
settled = true;
|
|
85
|
+
return name;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The cluster the client works on now: inside `withCluster` that work's, else the process's own (asking settles it).
|
|
90
|
+
* @returns {'devnet' | 'mainnet'}
|
|
91
|
+
*/
|
|
92
|
+
export function cluster() {
|
|
93
|
+
const here = within();
|
|
94
|
+
if (here) return here;
|
|
95
|
+
settled = true;
|
|
96
|
+
return name;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* loot's program on the cluster the client works on now (`cluster()`): what derives an address, builds an
|
|
101
|
+
* instruction or reads who owns an account calls this.
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
export function lootProgram() {
|
|
105
|
+
return CLUSTERS[cluster()].program;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Whether the client works on mainnet now: what is paid and won there is real money. */
|
|
109
|
+
export const realMoney = () => cluster() === 'mainnet';
|
|
110
|
+
|
|
111
|
+
// The tests' own switch, set by js/test/on-mainnet.mjs before a run of the whole suite on mainnet's address and by
|
|
112
|
+
// nothing else: the tests' economies, which say devnet or name no network, are then read as mainnet's.
|
|
113
|
+
const TEST_NETWORK = Symbol.for('xsolana.test.network');
|
|
114
|
+
|
|
115
|
+
/** The cluster an economy is on: the `network` its file names, devnet when it names none. */
|
|
116
|
+
export const networkOf = economy => globalThis[TEST_NETWORK] ?? economy?.network ?? 'devnet';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The RPC URL this process reads and sends through: the one given, else XSOLANA_RPC_URL, else the cluster's own
|
|
120
|
+
* (devnet's public RPC; mainnet has none). A URL that names another cluster is refused.
|
|
121
|
+
*/
|
|
122
|
+
export function clusterRpcUrl(url = globalThis.process?.env?.XSOLANA_RPC_URL || null) {
|
|
123
|
+
const on = cluster();
|
|
124
|
+
const chosen = url || CLUSTERS[on].rpc;
|
|
125
|
+
if (!chosen)
|
|
126
|
+
throw new Error(
|
|
127
|
+
`No RPC for ${on}: name one (XSOLANA_RPC_URL, or rpcUrl). ${on === 'mainnet' ? 'Mainnet’s public RPC does not serve what loot reads, so there is no default.' : ''}`.trim(),
|
|
128
|
+
);
|
|
129
|
+
return rightCluster(chosen, on);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Refuses a chain that is not this process's cluster, by its genesis block (`rpc.js`, `checkCluster`). */
|
|
133
|
+
export const checkOnCluster = rpc => checkCluster(rpc, cluster());
|
|
134
|
+
|
|
135
|
+
/** This process's cluster as a wallet names it (Wallet Standard): `solana:devnet` or `solana:mainnet`. */
|
|
136
|
+
export const walletChain = () => CLUSTERS[cluster()].chain;
|
package/js/client/grant.js
CHANGED
|
@@ -20,7 +20,7 @@ import { meta } from '../chain/programs.js';
|
|
|
20
20
|
import { sha256 } from '../chain/sha256.js';
|
|
21
21
|
import { recordKey16 } from './accounts.js';
|
|
22
22
|
import { ASSOCIATED_TOKEN_PROGRAM, gateAccounts, lootIx, prizeMetas, prizeNonce, shownFor, tailOf } from './ix.js';
|
|
23
|
-
import {
|
|
23
|
+
import { lootProgram, pda } from './pda.js';
|
|
24
24
|
import IDL from './idl/loot.js';
|
|
25
25
|
|
|
26
26
|
export const VOUCHER_DOMAIN = 'loot-grant-v5';
|
|
@@ -74,7 +74,7 @@ const roundsOf = (rounds, round, amount) =>
|
|
|
74
74
|
|
|
75
75
|
/** A voucher's body, which its key signs the sha256 of (`voucherBytes`). */
|
|
76
76
|
export function voucherBody({
|
|
77
|
-
program =
|
|
77
|
+
program = lootProgram(),
|
|
78
78
|
grant,
|
|
79
79
|
incarnation,
|
|
80
80
|
wallet,
|
package/js/client/ix.js
CHANGED
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
ata,
|
|
68
68
|
BUBBLEGUM_PROGRAM,
|
|
69
69
|
COMPRESSION_PROGRAM,
|
|
70
|
-
|
|
70
|
+
lootProgram,
|
|
71
71
|
MPL_CORE_PROGRAM,
|
|
72
72
|
NOOP_PROGRAM,
|
|
73
73
|
pda,
|
|
@@ -204,7 +204,7 @@ export function lootIx(
|
|
|
204
204
|
if (a.pda && !a.pda.program) {
|
|
205
205
|
const seeds = a.pda.seeds.map(s => seedBytes(s, known, args, def));
|
|
206
206
|
if (seeds.every(Boolean)) {
|
|
207
|
-
known[a.name] = findProgramAddress(seeds,
|
|
207
|
+
known[a.name] = findProgramAddress(seeds, lootProgram())[0];
|
|
208
208
|
progress = true;
|
|
209
209
|
}
|
|
210
210
|
}
|
|
@@ -215,13 +215,13 @@ export function lootIx(
|
|
|
215
215
|
const key = known[a.name];
|
|
216
216
|
if (key === null || (key === undefined && a.optional)) {
|
|
217
217
|
if (!a.optional) throw new Error(`${name}: ${a.name} is not optional`);
|
|
218
|
-
return meta(
|
|
218
|
+
return meta(lootProgram(), false, false);
|
|
219
219
|
}
|
|
220
220
|
if (key === undefined) throw new Error(`${name}: missing account ${a.group ? `${a.group}.` : ''}${a.name}`);
|
|
221
221
|
const isWritable = (a.writable === true || writable.includes(a.name)) && !readonly.includes(a.name);
|
|
222
222
|
return meta(key, a.signer === true || signer.includes(a.name), isWritable);
|
|
223
223
|
});
|
|
224
|
-
return { programId:
|
|
224
|
+
return { programId: lootProgram(), keys: [...keys, ...remaining], data: encodeInstruction(IDL, name, args) };
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// ---------------------------------------------------------------- the accounts after an instruction's own
|
|
@@ -548,7 +548,7 @@ const PIECE = leaves(instructionDef('hold').accounts).filter(a => a.group === 'p
|
|
|
548
548
|
function pieceMetas(p) {
|
|
549
549
|
return PIECE.map(a => {
|
|
550
550
|
const key = p.accounts[a.name];
|
|
551
|
-
if (key == null) return meta(
|
|
551
|
+
if (key == null) return meta(lootProgram(), false, false);
|
|
552
552
|
return meta(key, false, a.writable === true || p.writable.includes(a.name));
|
|
553
553
|
});
|
|
554
554
|
}
|
|
@@ -1302,7 +1302,7 @@ export function callbackRollIx({
|
|
|
1302
1302
|
);
|
|
1303
1303
|
keys.push(...tailOf(extra));
|
|
1304
1304
|
return {
|
|
1305
|
-
programId:
|
|
1305
|
+
programId: lootProgram(),
|
|
1306
1306
|
keys,
|
|
1307
1307
|
data: encodeInstruction(IDL, 'callback_roll', { randomness: Uint8Array.from(randomness) }),
|
|
1308
1308
|
};
|
|
@@ -1643,7 +1643,7 @@ function orderPiece(project, orderAddress, order, subject = {}, receiver) {
|
|
|
1643
1643
|
* own address, a record, a stash, a fee box), where nothing could move it again. A settle's fate and an NFT lottery
|
|
1644
1644
|
* ticket's price take it as `handBack`.
|
|
1645
1645
|
*/
|
|
1646
|
-
export const handsBack = account => account?.owner ===
|
|
1646
|
+
export const handsBack = account => account?.owner === lootProgram();
|
|
1647
1647
|
|
|
1648
1648
|
/** Where an NFT that is burned but can't be (any kind but Core) is sent: an address nobody holds the key to. */
|
|
1649
1649
|
export const INCINERATOR = '1nc1nerator11111111111111111111111111111111';
|
|
@@ -2079,7 +2079,7 @@ export const VERDICT_DOMAIN = 'loot-verdict-v4';
|
|
|
2079
2079
|
* the contest it was signed for, never one opened later at the same id. `decide` rebuilds them from its own accounts
|
|
2080
2080
|
* and arguments.
|
|
2081
2081
|
*/
|
|
2082
|
-
export function verdictBytes({ program =
|
|
2082
|
+
export function verdictBytes({ program = lootProgram(), contest, incarnation, side, expires, recipient = null }) {
|
|
2083
2083
|
if (incarnation == null) throw new Error('A verdict signs the contest’s incarnation (the decoded Contest’s).');
|
|
2084
2084
|
const named = recipient == null ? new Uint8Array(0) : addressBytes(recipient);
|
|
2085
2085
|
return concat(
|
|
@@ -2169,7 +2169,7 @@ export const rollIx = ({ authority, project, reference: ref }) =>
|
|
|
2169
2169
|
|
|
2170
2170
|
/** The oracle's answer to a `roll` (for a local simulation). */
|
|
2171
2171
|
export const callbackRandomIx = ({ project, reference: ref, randomness }) => ({
|
|
2172
|
-
programId:
|
|
2172
|
+
programId: lootProgram(),
|
|
2173
2173
|
keys: [
|
|
2174
2174
|
meta(pda.oracleIdentity(), true, false),
|
|
2175
2175
|
meta(project, false, false),
|
|
@@ -2373,7 +2373,7 @@ export function drawLotteryIx({
|
|
|
2373
2373
|
|
|
2374
2374
|
/** The oracle's answer to a round's draw (for a local simulation): the round's record is its only account. */
|
|
2375
2375
|
export const callbackLotteryIx = ({ round, randomness }) => ({
|
|
2376
|
-
programId:
|
|
2376
|
+
programId: lootProgram(),
|
|
2377
2377
|
keys: [meta(pda.oracleIdentity(), true, false), meta(round, false, true)],
|
|
2378
2378
|
data: encodeInstruction(IDL, 'callback_lottery', { randomness: Uint8Array.from(randomness) }),
|
|
2379
2379
|
});
|
|
@@ -3053,7 +3053,7 @@ export function anotherMetas(p) {
|
|
|
3053
3053
|
const writable = new Set([...p.writable, ...PIECE.filter(a => a.writable).map(a => a.name)]);
|
|
3054
3054
|
return [
|
|
3055
3055
|
...ANOTHER[p.kind].map(name =>
|
|
3056
|
-
p.accounts[name] == null ? meta(
|
|
3056
|
+
p.accounts[name] == null ? meta(lootProgram(), false, false) : meta(p.accounts[name], false, writable.has(name)),
|
|
3057
3057
|
),
|
|
3058
3058
|
...p.proof,
|
|
3059
3059
|
];
|
|
@@ -3310,7 +3310,7 @@ export const closeLeafIx = ({ asset, tree, payer, signs = true }) =>
|
|
|
3310
3310
|
|
|
3311
3311
|
const PROVE_LEAF = leaves(instructionDef('prove_leaf').accounts).map(a => a.name);
|
|
3312
3312
|
const CLOSE_LEAF = leaves(instructionDef('close_leaf').accounts).map(a => a.name);
|
|
3313
|
-
const isLoot = (ix, name) => ix.programId ===
|
|
3313
|
+
const isLoot = (ix, name) => ix.programId === lootProgram() && instructionOf(ix.data)?.name === name;
|
|
3314
3314
|
|
|
3315
3315
|
/**
|
|
3316
3316
|
* `instructions` with a `close_leaf` after them for each compressed NFT's record a `prove_leaf` among them makes, its
|
|
@@ -3469,8 +3469,8 @@ function nftSeat({
|
|
|
3469
3469
|
const seatGroup = (seat, p, box = null, order = null) => [
|
|
3470
3470
|
meta(seat, false, true),
|
|
3471
3471
|
...anotherMetas({ ...p, proof: [] }),
|
|
3472
|
-
meta(box ??
|
|
3473
|
-
meta(order ??
|
|
3472
|
+
meta(box ?? lootProgram(), false, false),
|
|
3473
|
+
meta(order ?? lootProgram(), false, Boolean(order)),
|
|
3474
3474
|
...p.proof,
|
|
3475
3475
|
];
|
|
3476
3476
|
|
|
@@ -3653,8 +3653,8 @@ export const harvestIx = ({
|
|
|
3653
3653
|
);
|
|
3654
3654
|
const groups = rest.flatMap(x => [
|
|
3655
3655
|
meta(x.seat, false, true),
|
|
3656
|
-
meta(x.held ??
|
|
3657
|
-
meta(x.tree ??
|
|
3656
|
+
meta(x.held ?? lootProgram(), false, false),
|
|
3657
|
+
meta(x.tree ?? lootProgram(), false, false),
|
|
3658
3658
|
]);
|
|
3659
3659
|
return lootIx(
|
|
3660
3660
|
'harvest',
|
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
|
|
6
|
+
import { lootProgram } from './cluster.js';
|
|
7
7
|
|
|
8
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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). */
|
package/js/economy/check.mjs
CHANGED
|
@@ -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
|
-
|
|
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 “${
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/js/economy/explain.mjs
CHANGED
|
@@ -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
|
-
'
|
|
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,
|
package/js/economy/guide.mjs
CHANGED
|
@@ -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
|
-
'
|
|
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: '
|
|
256
|
-
nearest: [
|
|
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
|
|
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),
|
package/js/economy/live.mjs
CHANGED
|
@@ -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 {
|
|
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:
|
|
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 } : {}),
|
package/js/economy/plan.mjs
CHANGED
package/js/economy/schema.json
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"description": "The spec format: 2 is loot 0.7 and later."
|
|
13
13
|
},
|
|
14
14
|
"network": {
|
|
15
|
-
"
|
|
16
|
-
"description": "
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|