@xgamesfun/solana 0.28.1 → 0.28.3
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 +48 -38
- package/js/play/sponsor.js +4 -4
- package/js/results/index.js +36 -14
- 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 +14 -7
- package/types/results/server.d.ts +3 -3
- package/types/results/wire.d.ts +8 -8
package/DISCLAIMER.md
CHANGED
|
@@ -19,7 +19,9 @@ Read this before you use this package, its tools or the loot program. Using them
|
|
|
19
19
|
wallets, token and NFT programs, npm and any results server. Any of them can fail, change or stop.
|
|
20
20
|
- The public results server (`https://play.xgames.fun/xsolana`) is offered as is, may change or stop at any time, and
|
|
21
21
|
carries no promise of uptime or of what it vouches for.
|
|
22
|
-
- **
|
|
22
|
+
- **Mainnet is real money, and the program has not been independently audited.** loot runs on devnet, where nothing
|
|
23
|
+
has value, and on mainnet, where everything does. An economy is on devnet unless its file says
|
|
24
|
+
`"network": "mainnet"`. What is sent to the program on mainnet can be lost, and you use it there at your own risk.
|
|
23
25
|
|
|
24
26
|
## Use it legally
|
|
25
27
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@xgamesfun/solana)
|
|
6
6
|
[](LICENSE)
|
|
7
|
-
[](#good-to-know)
|
|
7
|
+
[](#good-to-know)
|
|
8
8
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.XGamesFun/solana-kit)
|
|
9
9
|
|
|
10
10
|
You describe your game's or community's economy in a small JSON file: what players buy, open, stake, win or craft.
|
|
@@ -15,8 +15,9 @@ write, deploy or audit yourself.
|
|
|
15
15
|
Most people never touch the JSON: they ask an AI agent (Claude Code, Codex, Cursor, ChatGPT or Claude) for _"a fishing
|
|
16
16
|
game where each cast costs 0.02 SOL, with a rare golden fish and a monthly draw"_ and the agent does the rest.
|
|
17
17
|
|
|
18
|
-
> **
|
|
19
|
-
>
|
|
18
|
+
> **On devnet and on mainnet.** An economy runs on devnet, where nothing is real money, unless its file says
|
|
19
|
+
> `"network": "mainnet"`, where everything is. The program is not independently audited and the tools are provided as
|
|
20
|
+
> is: read [DISCLAIMER.md](DISCLAIMER.md) before you use them, and before real money above all.
|
|
20
21
|
|
|
21
22
|
## What you can build
|
|
22
23
|
|
|
@@ -76,7 +77,7 @@ codex plugin add xsolana@xgamesfun
|
|
|
76
77
|
|
|
77
78
|
**claude.ai, ChatGPT or Claude Desktop:** add `https://xgames.fun/mcp` as a custom connector. Nothing to install.
|
|
78
79
|
|
|
79
|
-
**Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.28.
|
|
80
|
+
**Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.28.3 xsolana mcp`.
|
|
80
81
|
|
|
81
82
|
**Any agent that reads skills** (Copilot, Gemini CLI, Windsurf…): `npx skills add XGamesFun/solana-kit`.
|
|
82
83
|
|
|
@@ -87,26 +88,26 @@ are in [docs/harnesses.md](docs/harnesses.md).
|
|
|
87
88
|
|
|
88
89
|
```bash
|
|
89
90
|
# find a worked example close to your idea
|
|
90
|
-
npx -y @xgamesfun/solana@0.28.
|
|
91
|
+
npx -y @xgamesfun/solana@0.28.3 examples shop with loot boxes
|
|
91
92
|
|
|
92
93
|
# using a coin you already launched? read it from the chain into its "tokens" entry
|
|
93
|
-
npx -y @xgamesfun/solana@0.28.
|
|
94
|
+
npx -y @xgamesfun/solana@0.28.3 token <its mint address>
|
|
94
95
|
|
|
95
96
|
# does it work? where does the money go? what does it cost?
|
|
96
|
-
npx -y @xgamesfun/solana@0.28.
|
|
97
|
-
npx -y @xgamesfun/solana@0.28.
|
|
98
|
-
npx -y @xgamesfun/solana@0.28.
|
|
97
|
+
npx -y @xgamesfun/solana@0.28.3 check xsolana.json
|
|
98
|
+
npx -y @xgamesfun/solana@0.28.3 simulate xsolana.json
|
|
99
|
+
npx -y @xgamesfun/solana@0.28.3 cost xsolana.json
|
|
99
100
|
|
|
100
101
|
# the whole economy in plain words, and what a cheater could get
|
|
101
|
-
npx -y @xgamesfun/solana@0.28.
|
|
102
|
-
npx -y @xgamesfun/solana@0.28.
|
|
102
|
+
npx -y @xgamesfun/solana@0.28.3 explain xsolana.json
|
|
103
|
+
npx -y @xgamesfun/solana@0.28.3 review xsolana.json
|
|
103
104
|
|
|
104
|
-
# go live: a key file for the project (it prints the address to send
|
|
105
|
-
npx -y @xgamesfun/solana@0.28.
|
|
105
|
+
# go live: a key file for the project (it prints the address to send SOL to, never the key)…
|
|
106
|
+
npx -y @xgamesfun/solana@0.28.3 keygen ~/.config/xsolana/my-game.json
|
|
106
107
|
# …then everything going live would send, and a code; nothing is sent
|
|
107
|
-
npx -y @xgamesfun/solana@0.28.
|
|
108
|
+
npx -y @xgamesfun/solana@0.28.3 approve xsolana.json --keypair ~/.config/xsolana/my-game.json
|
|
108
109
|
# …and with that code, exactly what it listed is sent
|
|
109
|
-
npx -y @xgamesfun/solana@0.28.
|
|
110
|
+
npx -y @xgamesfun/solana@0.28.3 approve xsolana.json --keypair ~/.config/xsolana/my-game.json --yes <code>
|
|
110
111
|
```
|
|
111
112
|
|
|
112
113
|
Each command names an exact version, so you always run the release you meant to. `xsolana help` lists them all.
|
|
@@ -149,7 +150,8 @@ Using `@solana/kit`? `loot.toKitInstruction(ix)` turns any builder's instruction
|
|
|
149
150
|
|
|
150
151
|
## Good to know
|
|
151
152
|
|
|
152
|
-
- **Devnet
|
|
153
|
+
- **Devnet and mainnet, not independently audited.** An economy is on devnet unless its file says
|
|
154
|
+
`"network": "mainnet"`. Mainnet is real money: try everything on devnet first.
|
|
153
155
|
- **Upgrades can mean setting up again.** No program version so far has carried accounts over. Pin the package version.
|
|
154
156
|
- **Players trust the project.** Your keys can mint and move your project's pools. The program protects players from
|
|
155
157
|
each other and from mistakes, not from the project they chose.
|
package/bin/xsolana.mjs
CHANGED
|
@@ -141,6 +141,29 @@ async function main(argv) {
|
|
|
141
141
|
|
|
142
142
|
const fileInput = () => (positional[0] ? { file: positional[0] } : {});
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Names the cluster this command works on from the economy it is given (a file, or a spec passed whole): its
|
|
146
|
+
* "network", devnet when it names none. A file that is missing or not JSON, and a network loot does not know, are
|
|
147
|
+
* left to the tool, which says so in its own words.
|
|
148
|
+
*/
|
|
149
|
+
async function clusterOf({ file = 'xsolana.json', spec = null } = {}) {
|
|
150
|
+
const { CLUSTERS, networkOf, useCluster } = await import('../js/client/cluster.js');
|
|
151
|
+
let network;
|
|
152
|
+
try {
|
|
153
|
+
network = networkOf(spec ?? JSON.parse(await readFile(file, 'utf8')));
|
|
154
|
+
} catch {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!Object.hasOwn(CLUSTERS, network)) return;
|
|
158
|
+
try {
|
|
159
|
+
useCluster(network);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new UsageError(
|
|
162
|
+
`${spec ? 'The economy' : file} is on ${network}, and XSOLANA_CLUSTER says ${process.env.XSOLANA_CLUSTER}: unset it, or change the economy’s "network".`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
144
167
|
switch (command) {
|
|
145
168
|
case 'mcp': {
|
|
146
169
|
if (flags.has('--http')) {
|
|
@@ -166,6 +189,7 @@ async function main(argv) {
|
|
|
166
189
|
case 'review':
|
|
167
190
|
case 'publish':
|
|
168
191
|
case 'status':
|
|
192
|
+
await clusterOf(fileInput());
|
|
169
193
|
await tool(`xsolana_${command}`, fileInput());
|
|
170
194
|
break;
|
|
171
195
|
case 'token':
|
|
@@ -193,13 +217,14 @@ async function main(argv) {
|
|
|
193
217
|
`A new key is in ${made.file}${process.platform === 'win32' ? '' : ', readable by you alone'}. Its address:`,
|
|
194
218
|
` ${made.address}`,
|
|
195
219
|
'Whatever is set up with it, it owns, for good: anyone who can read that file controls the project, and every later change is signed by it. Keep the file where it is and never paste it into a chat, a spec, a repository or a page. Back it up yourself (a USB stick, a password manager): if it is lost, nothing can change the project or move what its address holds.',
|
|
196
|
-
`It holds nothing yet. Send it
|
|
220
|
+
`It holds nothing yet. Send it SOL (\`xsolana cost\` says what going live takes): devnet SOL for an economy on devnet (https://faucet.solana.com gives some), real SOL for one whose file says "network": "mainnet". Then: npx -y @xgamesfun/solana@${PACKAGE_VERSION} approve xsolana.json --keypair ${word}`,
|
|
197
221
|
].join('\n'),
|
|
198
222
|
);
|
|
199
223
|
break;
|
|
200
224
|
}
|
|
201
225
|
case 'approve': {
|
|
202
226
|
const file = positional[0] ?? 'xsolana.json';
|
|
227
|
+
await clusterOf({ file });
|
|
203
228
|
const keyFile = values.get('--keypair');
|
|
204
229
|
const page = flags.has('--browser');
|
|
205
230
|
if (keyFile !== undefined && page)
|
|
@@ -252,6 +277,7 @@ async function main(argv) {
|
|
|
252
277
|
{ cause: e },
|
|
253
278
|
);
|
|
254
279
|
}
|
|
280
|
+
if (input && typeof input === 'object' && (input.file !== undefined || input.spec)) await clusterOf(input);
|
|
255
281
|
await tool(positional[0], input);
|
|
256
282
|
break;
|
|
257
283
|
}
|
package/js/approve/details.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { decodeBorsh } from '../chain/borsh.js';
|
|
6
6
|
import { equalBytes, toHex } from '../chain/bytes.js';
|
|
7
7
|
import { accountKind, decode, IDL, instructionOf, keyLeaf, merkleRoot } from '../client/index.js';
|
|
8
|
-
import {
|
|
8
|
+
import { lootProgram, pda } from '../client/pda.js';
|
|
9
9
|
import { dateWords, FAR_SECONDS } from '../economy/check.mjs';
|
|
10
10
|
import { fromUnits } from '../economy/model.mjs';
|
|
11
11
|
import { grantArgs } from '../economy/plan.mjs';
|
|
@@ -38,7 +38,7 @@ const leafNames = list => list.flatMap(a => (a.accounts ? leafNames(a.accounts)
|
|
|
38
38
|
|
|
39
39
|
/** A loot instruction read back: { name, args, accounts: { name: address } }, or null for another program's. */
|
|
40
40
|
function readLootInstruction(ix) {
|
|
41
|
-
if (ix.programId !==
|
|
41
|
+
if (ix.programId !== lootProgram()) return null;
|
|
42
42
|
const def = instructionOf(ix.data);
|
|
43
43
|
if (!def) return null;
|
|
44
44
|
const args = {};
|
|
@@ -104,7 +104,7 @@ export async function liveStepDetails(instructions, names, chain) {
|
|
|
104
104
|
reads.push(null);
|
|
105
105
|
continue;
|
|
106
106
|
}
|
|
107
|
-
if (account.owner !==
|
|
107
|
+
if (account.owner !== lootProgram() || accountKind(account.data) !== RECORD_ACCOUNT[kind])
|
|
108
108
|
throw new Error(`${read.accounts.target} is not the ${kind.toLowerCase()} it should be.`);
|
|
109
109
|
reads.push({
|
|
110
110
|
...read,
|
|
@@ -10,7 +10,7 @@ import { METADATA_LIMITS, metadataAddress, parseRecord } from '../chain/metadata
|
|
|
10
10
|
import { parseMint } from '../chain/mint.js';
|
|
11
11
|
import { mintState, powersOf } from '../chain/token2022.js';
|
|
12
12
|
import { accountKind, decode } from '../client/accounts.js';
|
|
13
|
-
import { BUBBLEGUM_PROGRAM,
|
|
13
|
+
import { BUBBLEGUM_PROGRAM, lootProgram, pda } from '../client/pda.js';
|
|
14
14
|
import { METAPLEX_METADATA_PROGRAM, SYSTEM_PROGRAM } from '../chain/pda.js';
|
|
15
15
|
import { tokenLabels } from '../economy/model.mjs';
|
|
16
16
|
import { madeSoFar } from '../economy/plan.mjs';
|
|
@@ -43,7 +43,7 @@ export async function checkLive({ live, m, wallet, chain }) {
|
|
|
43
43
|
let project = own;
|
|
44
44
|
if (made.project) {
|
|
45
45
|
const account = await read(made.project);
|
|
46
|
-
if (!account || account.owner !==
|
|
46
|
+
if (!account || account.owner !== lootProgram() || accountKind(account.data) !== 'Project')
|
|
47
47
|
throw new LiveMismatch(
|
|
48
48
|
`The file names the project ${made.project}, and there is no loot project at that address.`,
|
|
49
49
|
);
|
package/js/approve/page.html
CHANGED
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
<body>
|
|
162
162
|
<main>
|
|
163
163
|
<header>
|
|
164
|
-
<p class="muted mono flush">loot · devnet · runs on your computer only</p>
|
|
164
|
+
<p class="muted mono flush">loot · <span id="cluster">devnet</span> · runs on your computer only</p>
|
|
165
165
|
<h1 id="title">Approve going live</h1>
|
|
166
166
|
<p class="muted" id="sub">Loading…</p>
|
|
167
167
|
</header>
|
|
@@ -173,8 +173,8 @@
|
|
|
173
173
|
</div>
|
|
174
174
|
<div class="row" id="wallets" hidden></div>
|
|
175
175
|
<p class="muted note">
|
|
176
|
-
Set the wallet to <b>devnet</b>. Each approval shows you the transactions before you sign;
|
|
177
|
-
wallet without it. Nobody else ever sees your key.
|
|
176
|
+
Set the wallet to <b id="walletcluster">devnet</b>. Each approval shows you the transactions before you sign;
|
|
177
|
+
nothing leaves your wallet without it. Nobody else ever sees your key.
|
|
178
178
|
</p>
|
|
179
179
|
</section>
|
|
180
180
|
|
|
@@ -202,8 +202,8 @@
|
|
|
202
202
|
<ul class="warn gap" id="legal" hidden></ul>
|
|
203
203
|
<p class="muted note">The full text is in DISCLAIMER.md, in the package.</p>
|
|
204
204
|
<label class="row gap"
|
|
205
|
-
><input type="checkbox" id="legalok" /> I have read this and accept it.
|
|
206
|
-
needs every point above answered first.</label
|
|
205
|
+
><input type="checkbox" id="legalok" /> I have read this and accept it.
|
|
206
|
+
<span id="money">Devnet is not real money; real money needs every point above answered first.</span></label
|
|
207
207
|
>
|
|
208
208
|
</section>
|
|
209
209
|
|
package/js/approve/page.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// session's token from its <meta name="xsolana-token"> (the page allows no inline script).
|
|
6
6
|
import { fromBase64, toBase64 } from '/js/chain/bytes.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// The cluster of this approval (the economy's own), as the server says it: devnet until the first answer.
|
|
9
|
+
let onCluster = 'devnet';
|
|
9
10
|
const $ = id => document.getElementById(id);
|
|
10
11
|
|
|
11
12
|
function walletRegistry() {
|
|
@@ -65,6 +66,13 @@ export function approvalPage(token) {
|
|
|
65
66
|
// How many of the lines about what the wallet added have been said on this page.
|
|
66
67
|
let addedSaid = 0;
|
|
67
68
|
function render(state) {
|
|
69
|
+
onCluster = state.cluster ?? 'devnet';
|
|
70
|
+
$('cluster').textContent = onCluster;
|
|
71
|
+
$('walletcluster').textContent = onCluster;
|
|
72
|
+
$('money').textContent =
|
|
73
|
+
onCluster === 'mainnet'
|
|
74
|
+
? 'This is mainnet: everything sent here is real money, and every point above is yours to have answered first.'
|
|
75
|
+
: 'Devnet is not real money; real money needs every point above answered first.';
|
|
68
76
|
last = state;
|
|
69
77
|
// What the wallet added to transactions that landed (its own checks): they were sent as it signed them. Said
|
|
70
78
|
// whenever a state arrives, so one that landed in a batch that then stopped is said too.
|
|
@@ -84,7 +92,7 @@ export function approvalPage(token) {
|
|
|
84
92
|
$('sub').textContent = state.done
|
|
85
93
|
? nothingSent
|
|
86
94
|
? `The only change in ${state.file} is a token’s name, and it can’t be written on chain: nothing was sent, and the file is as it was. See below.`
|
|
87
|
-
: `Every step landed on
|
|
95
|
+
: `Every step landed on ${state.cluster}. ${state.file} now holds the project’s addresses: you can close this page.`
|
|
88
96
|
: `${state.mode === 'change' ? 'A change to a live economy' : state.mode === 'resume' ? 'A setup that was cut short goes on' : 'A new economy'} from ${state.file}, on the shared loot program ${state.program.slice(0, 6)}…${state.project ? `, in the project ${state.project}${state.wallet ? `, made from your wallet ${state.wallet}` : ''}` : ''}`;
|
|
89
97
|
$('cost').textContent = state.setupSol !== null && state.mode === 'new' ? `about ${state.setupSol} SOL` : '';
|
|
90
98
|
const steps = $('steps');
|
|
@@ -205,7 +213,7 @@ export function approvalPage(token) {
|
|
|
205
213
|
const found = list();
|
|
206
214
|
if (!found.length) {
|
|
207
215
|
log(
|
|
208
|
-
|
|
216
|
+
`No Solana wallet in this browser. Install one (Phantom, Solflare, Backpack), set it to ${onCluster}, then reload.`,
|
|
209
217
|
'bad',
|
|
210
218
|
);
|
|
211
219
|
return;
|
|
@@ -245,7 +253,11 @@ export function approvalPage(token) {
|
|
|
245
253
|
`Asking your wallet to approve ${batch.transactions.length} transaction${batch.transactions.length === 1 ? '' : 's'} in the project ${batch.project}: ${batch.says.join(' | ')}${details.length ? `. ${details.join('. ')}` : ''}`,
|
|
246
254
|
);
|
|
247
255
|
const sign = signingWallet.features['solana:signTransaction'];
|
|
248
|
-
const inputs = batch.transactions.map(t => ({
|
|
256
|
+
const inputs = batch.transactions.map(t => ({
|
|
257
|
+
account: signer,
|
|
258
|
+
transaction: fromBase64(t),
|
|
259
|
+
chain: `solana:${onCluster}`,
|
|
260
|
+
}));
|
|
249
261
|
const outputs = await sign.signTransaction(...inputs);
|
|
250
262
|
const signed = outputs.map(o => toBase64(o.signedTransaction));
|
|
251
263
|
$('status').textContent = 'Sending and confirming…';
|
package/js/approve/server.mjs
CHANGED
|
@@ -14,14 +14,14 @@ import path from 'node:path';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
16
|
import { equalBytes, fromBase64, toBase64 } from '../chain/bytes.js';
|
|
17
|
-
import { alreadyProcessed,
|
|
17
|
+
import { alreadyProcessed, rpcClient } from '../chain/rpc.js';
|
|
18
18
|
import { addressBytes, encodeBase58 } from '../chain/base58.js';
|
|
19
19
|
import { slotHashes, tableAddress } from '../chain/lookup.js';
|
|
20
20
|
import { SYSVAR_SLOT_HASHES } from '../chain/pda.js';
|
|
21
21
|
import { MINT_BYTES } from '../chain/token.js';
|
|
22
22
|
import { compileMessage, readTransaction, wireTransaction } from '../chain/tx.js';
|
|
23
23
|
import { accountKind, decode } from '../client/accounts.js';
|
|
24
|
-
import {
|
|
24
|
+
import { checkOnCluster, cluster, CLUSTERS, clusterRpcUrl, lootProgram, pda } from '../client/pda.js';
|
|
25
25
|
import { check, describe } from '../economy/check.mjs';
|
|
26
26
|
import { cost } from '../economy/cost.mjs';
|
|
27
27
|
import { LEGAL, legalFlags } from '../economy/legal.mjs';
|
|
@@ -35,7 +35,6 @@ import { walletAdded } from './wallet-adds.mjs';
|
|
|
35
35
|
|
|
36
36
|
const PREPARE_TTL_MS = 90_000;
|
|
37
37
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
38
|
-
const DEVNET = 'https://api.devnet.solana.com';
|
|
39
38
|
|
|
40
39
|
/** A refusal with the HTTP status the page gets. */
|
|
41
40
|
class ApproveError extends Error {
|
|
@@ -46,37 +45,43 @@ class ApproveError extends Error {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
/**
|
|
49
|
-
* The chain the approval sends to:
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* The chain the approval sends to: this process's cluster (`client/cluster.js`), through XSOLANA_RPC_URL or, on
|
|
49
|
+
* devnet, its public RPC (a local validator passes for devnet). A URL that names another cluster is refused at once,
|
|
50
|
+
* and before anything is sent the chain's genesis block is checked: another cluster is refused whatever the URL says.
|
|
52
51
|
*/
|
|
53
|
-
export function
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
export function clusterChain(url) {
|
|
53
|
+
// A URL that names another cluster is refused here. With none to use (mainnet has no RPC of its own), the refusal
|
|
54
|
+
// waits for the first request, so what is wrong with the economy's file is said first.
|
|
55
|
+
let made = null;
|
|
56
|
+
const client = () => (made ??= rpcClient(clusterRpcUrl(url)));
|
|
57
|
+
if (url || process.env.XSOLANA_RPC_URL || CLUSTERS[cluster()].rpc) client();
|
|
58
|
+
let genesis = null;
|
|
56
59
|
const checked = () =>
|
|
57
|
-
(
|
|
58
|
-
|
|
60
|
+
(genesis ??= checkOnCluster(client()).catch(e => {
|
|
61
|
+
genesis = null;
|
|
59
62
|
throw e;
|
|
60
63
|
}));
|
|
61
64
|
return {
|
|
62
|
-
url
|
|
65
|
+
get url() {
|
|
66
|
+
return client().url ?? url;
|
|
67
|
+
},
|
|
63
68
|
latestBlockhash: async () => {
|
|
64
69
|
await checked();
|
|
65
|
-
return
|
|
70
|
+
return client().latestBlockhash();
|
|
66
71
|
},
|
|
67
|
-
rent: async bytes => BigInt(await
|
|
72
|
+
rent: async bytes => BigInt(await client().call('getMinimumBalanceForRentExemption', [bytes])),
|
|
68
73
|
send: async wire => {
|
|
69
74
|
await checked();
|
|
70
|
-
return
|
|
75
|
+
return client().send(wire);
|
|
71
76
|
},
|
|
72
|
-
confirm: (signature, options) =>
|
|
73
|
-
account: address =>
|
|
77
|
+
confirm: (signature, options) => client().confirm(signature, options),
|
|
78
|
+
account: address => client().account(address),
|
|
74
79
|
/** A sent transaction's status from the whole history: { err, confirmationStatus }, or null when unseen. */
|
|
75
80
|
signatureStatus: async signature =>
|
|
76
|
-
(await
|
|
77
|
-
blockHeight: () =>
|
|
81
|
+
(await client().call('getSignatureStatuses', [[signature], { searchTransactionHistory: true }])).value[0],
|
|
82
|
+
blockHeight: () => client().blockHeight(),
|
|
78
83
|
/** The chain's current epoch (a mint's transfer fee changes on one). */
|
|
79
|
-
epoch: async () => Number((await
|
|
84
|
+
epoch: async () => Number((await client().call('getEpochInfo')).epoch),
|
|
80
85
|
};
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -128,7 +133,7 @@ function walletAddress(wallet) {
|
|
|
128
133
|
*/
|
|
129
134
|
export async function createApproval({
|
|
130
135
|
file,
|
|
131
|
-
chain =
|
|
136
|
+
chain = clusterChain(),
|
|
132
137
|
now = () => Math.floor(Date.now() / 1000),
|
|
133
138
|
clock = Date.now,
|
|
134
139
|
write = writeWhole,
|
|
@@ -389,8 +394,8 @@ export async function createApproval({
|
|
|
389
394
|
name: spec.name ?? path.basename(file),
|
|
390
395
|
file: path.basename(file),
|
|
391
396
|
mode,
|
|
392
|
-
program:
|
|
393
|
-
cluster:
|
|
397
|
+
program: lootProgram(),
|
|
398
|
+
cluster: cluster(),
|
|
394
399
|
setupSol: cost(spec, { now: at, live: live(), ...scope() }).setup?.totalSol ?? null,
|
|
395
400
|
warnings: [
|
|
396
401
|
...(extra.refused ? [extra.refused] : []),
|
|
@@ -738,7 +743,7 @@ export async function createApproval({
|
|
|
738
743
|
async function projectOwner(project) {
|
|
739
744
|
if (!project || !chain.account) return null;
|
|
740
745
|
const account = await chain.account(project).catch(() => null);
|
|
741
|
-
return account && account.owner ===
|
|
746
|
+
return account && account.owner === lootProgram() && accountKind(account.data) === 'Project'
|
|
742
747
|
? decode('Project', account.data).owner
|
|
743
748
|
: null;
|
|
744
749
|
}
|
|
@@ -811,7 +816,7 @@ export async function createApproval({
|
|
|
811
816
|
}
|
|
812
817
|
const progress = {
|
|
813
818
|
...was,
|
|
814
|
-
program:
|
|
819
|
+
program: lootProgram(),
|
|
815
820
|
...(made.has('project') ? { project: ctx.project, treasury: ctx.treasury, authority: ctx.authority } : {}),
|
|
816
821
|
...(made.has('collection') && ctx.collection ? { collection: ctx.collection } : {}),
|
|
817
822
|
...(made.has('tree') && ctx.tree ? { tree: ctx.tree } : {}),
|
|
@@ -1076,7 +1081,7 @@ export async function approve({
|
|
|
1076
1081
|
file = 'xsolana.json',
|
|
1077
1082
|
port,
|
|
1078
1083
|
open = true,
|
|
1079
|
-
chain =
|
|
1084
|
+
chain = clusterChain(),
|
|
1080
1085
|
log = (...a) => console.error(...a),
|
|
1081
1086
|
openLink = openBrowser,
|
|
1082
1087
|
} = {}) {
|
package/js/approve/with-key.mjs
CHANGED
|
@@ -11,7 +11,8 @@ import path from 'node:path';
|
|
|
11
11
|
import { fromBase64, toBase64 } from '../chain/bytes.js';
|
|
12
12
|
import { readTransaction, wireTransaction } from '../chain/tx.js';
|
|
13
13
|
import { readKeyFile } from './keyfile.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { clusterChain, createApproval } from './server.mjs';
|
|
15
|
+
import { realMoney } from '../client/cluster.js';
|
|
15
16
|
|
|
16
17
|
/** The notice the page has its owner tick before Approve (js/approve/page.html, "Read this first"), word for word. */
|
|
17
18
|
export const NOTICE = [
|
|
@@ -122,7 +123,7 @@ export function approvalText(state, { keyFile, balance = null }) {
|
|
|
122
123
|
lines.push('', 'Read this first');
|
|
123
124
|
for (const line of [...NOTICE, ...state.legal.map(f => f.says)]) lines.push(`- ${line}`);
|
|
124
125
|
lines.push(
|
|
125
|
-
|
|
126
|
+
`The full text is in DISCLAIMER.md, in the package. ${realMoney() ? 'This is mainnet: everything sent here is real money, and every point above is yours to have answered first.' : 'Devnet is not real money; real money needs every point above answered first.'}`,
|
|
126
127
|
);
|
|
127
128
|
|
|
128
129
|
if (state.warnings.length) {
|
|
@@ -156,7 +157,7 @@ export async function approveWithKey({
|
|
|
156
157
|
keyFile,
|
|
157
158
|
yes = null,
|
|
158
159
|
confirm = null,
|
|
159
|
-
chain =
|
|
160
|
+
chain = clusterChain(),
|
|
160
161
|
out = line => console.log(line),
|
|
161
162
|
now,
|
|
162
163
|
keyReader = readKeyFile,
|
|
@@ -204,7 +205,9 @@ export async function approveWithKey({
|
|
|
204
205
|
out('');
|
|
205
206
|
if (balance === 0n) {
|
|
206
207
|
out(
|
|
207
|
-
|
|
208
|
+
realMoney()
|
|
209
|
+
? `Nothing was sent: the key holds no SOL. This economy is on mainnet: send SOL to ${key.address} first, then run this again.`
|
|
210
|
+
: `Nothing was sent: the key holds no SOL. Send devnet SOL to ${key.address} first (https://faucet.solana.com gives some), then run this again.`,
|
|
208
211
|
);
|
|
209
212
|
return { ok: false, sent: 0, done: false, code };
|
|
210
213
|
}
|
package/js/chain/borsh.js
CHANGED
|
@@ -242,9 +242,11 @@ export function isAccount(idl, name, data) {
|
|
|
242
242
|
* [{ name, data }]. Only lines the program itself printed count: the log names which program each line comes from
|
|
243
243
|
* (`Program <id> invoke [n]` … `success`), and any program can print a line that looks like an event. Logs alone
|
|
244
244
|
* cannot say whether the transaction succeeded (the runtime can fail it after every instruction logged success), so
|
|
245
|
-
* the caller decides that from the transaction's `meta.err` and reads events only when it is null.
|
|
245
|
+
* the caller decides that from the transaction's `meta.err` and reads events only when it is null. `program` is the
|
|
246
|
+
* address the program runs at: the IDL's own unless the caller names another (the program has an address of its own on
|
|
247
|
+
* each cluster, and an IDL holds one).
|
|
246
248
|
*/
|
|
247
|
-
export function decodeEvents(idl, logs) {
|
|
249
|
+
export function decodeEvents(idl, logs, program = idl.address) {
|
|
248
250
|
const out = [];
|
|
249
251
|
const stack = [];
|
|
250
252
|
for (const line of logs ?? []) {
|
|
@@ -259,7 +261,7 @@ export function decodeEvents(idl, logs) {
|
|
|
259
261
|
continue;
|
|
260
262
|
}
|
|
261
263
|
const m = /^Program data: (.+)$/.exec(line);
|
|
262
|
-
if (!m || stack[stack.length - 1] !==
|
|
264
|
+
if (!m || stack[stack.length - 1] !== program) continue;
|
|
263
265
|
let bytes;
|
|
264
266
|
try {
|
|
265
267
|
bytes = fromBase64(m[1]);
|
package/js/chain/rpc.js
CHANGED
|
@@ -47,23 +47,39 @@ export const GENESIS = Object.freeze({
|
|
|
47
47
|
testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
'
|
|
50
|
+
const ON_DEVNET =
|
|
51
|
+
'This works on devnet (an economy that names no network is on devnet). Unset XSOLANA_RPC_URL, or point it at a devnet RPC.';
|
|
52
|
+
const ON_MAINNET = 'This works on mainnet. Point XSOLANA_RPC_URL at a mainnet RPC.';
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Refuses an RPC URL that names another cluster than the one the caller works on (`devnet` or `mainnet`). Returns the
|
|
56
|
+
* URL. A URL names nothing for certain: `checkCluster` reads the chain itself.
|
|
57
|
+
*/
|
|
58
|
+
export function rightCluster(url, cluster = 'devnet') {
|
|
59
|
+
const text = String(url);
|
|
60
|
+
if (cluster === 'mainnet') {
|
|
61
|
+
if (/devnet|testnet/i.test(text)) throw new RpcError(`${url} is not a mainnet RPC. ${ON_MAINNET}`);
|
|
62
|
+
} else if (/mainnet/i.test(text)) throw new RpcError(`${url} is a mainnet RPC. ${ON_DEVNET}`);
|
|
56
63
|
return url;
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
/** Refuses an RPC URL that names mainnet. Returns the URL. */
|
|
67
|
+
export const notMainnet = url => rightCluster(url, 'devnet');
|
|
68
|
+
|
|
59
69
|
/**
|
|
60
|
-
* Refuses a chain that is
|
|
61
|
-
* let through
|
|
70
|
+
* Refuses a chain that is not the cluster the caller works on, by its genesis block, whatever its URL says. On
|
|
71
|
+
* devnet, mainnet and testnet are refused and a local validator is let through; on mainnet, only mainnet itself
|
|
72
|
+
* passes. `rpc` has `call`. Returns the genesis hash.
|
|
62
73
|
*/
|
|
63
|
-
export async function checkCluster(rpc) {
|
|
74
|
+
export async function checkCluster(rpc, cluster = 'devnet') {
|
|
64
75
|
const genesis = await rpc.call('getGenesisHash');
|
|
65
|
-
|
|
66
|
-
if (
|
|
76
|
+
const here = rpc.url ?? 'This RPC';
|
|
77
|
+
if (cluster === 'mainnet') {
|
|
78
|
+
if (genesis !== GENESIS.mainnet) throw new RpcError(`${here} is not on mainnet. ${ON_MAINNET}`);
|
|
79
|
+
return genesis;
|
|
80
|
+
}
|
|
81
|
+
if (genesis === GENESIS.mainnet) throw new RpcError(`${here} is on mainnet. ${ON_DEVNET}`);
|
|
82
|
+
if (genesis === GENESIS.testnet) throw new RpcError(`${here} is on testnet. ${ON_DEVNET}`);
|
|
67
83
|
return genesis;
|
|
68
84
|
}
|
|
69
85
|
const decodeAccount = raw =>
|
package/js/client/accounts.js
CHANGED
|
@@ -7,7 +7,7 @@ import { decodeAccount, decodeBorsh, decodeEvents } from '../chain/borsh.js';
|
|
|
7
7
|
import { compareBytes, concat, readUintLE, toHex, uintLE } from '../chain/bytes.js';
|
|
8
8
|
import { sha256 } from '../chain/sha256.js';
|
|
9
9
|
import { SYSTEM_PROGRAM } from '../chain/pda.js';
|
|
10
|
-
import {
|
|
10
|
+
import { lootProgram, pda } from './pda.js';
|
|
11
11
|
import IDL from './idl/loot.js';
|
|
12
12
|
|
|
13
13
|
/** The bytes of one time in a farm's record of ends (`decodeEnds`): rent its writer pays, not refunded when a seat leaves. */
|
|
@@ -243,7 +243,7 @@ export function readListAccount(account) {
|
|
|
243
243
|
!account.executable
|
|
244
244
|
)
|
|
245
245
|
return null;
|
|
246
|
-
if (account.owner !==
|
|
246
|
+
if (account.owner !== lootProgram() || account.executable) throw new Error('The list has an invalid owner.');
|
|
247
247
|
return decodeList(account.data);
|
|
248
248
|
}
|
|
249
249
|
|
|
@@ -855,14 +855,14 @@ export function receipts(tx, err) {
|
|
|
855
855
|
throw new Error(
|
|
856
856
|
"loot.receipts(logs, err) needs the transaction's meta.err (null when it succeeded): a failed transaction keeps its logs",
|
|
857
857
|
);
|
|
858
|
-
return err === null ? decodeEvents(IDL, tx) : [];
|
|
858
|
+
return err === null ? decodeEvents(IDL, tx, lootProgram()) : [];
|
|
859
859
|
}
|
|
860
860
|
const meta = tx?.meta;
|
|
861
861
|
if (!meta || meta.err === undefined)
|
|
862
862
|
throw new Error(
|
|
863
863
|
'loot.receipts(tx) needs the transaction getTransaction returns, with its meta.err and meta.logMessages',
|
|
864
864
|
);
|
|
865
|
-
return meta.err === null ? decodeEvents(IDL, meta.logMessages ?? []) : [];
|
|
865
|
+
return meta.err === null ? decodeEvents(IDL, meta.logMessages ?? [], lootProgram()) : [];
|
|
866
866
|
}
|
|
867
867
|
|
|
868
868
|
/** The program's errors by code: { code: { name, msg } }. */
|
|
@@ -886,7 +886,7 @@ export function lootError(logs) {
|
|
|
886
886
|
}
|
|
887
887
|
const failed = /^Program (\w+) failed: (.*)$/.exec(line);
|
|
888
888
|
if (failed) {
|
|
889
|
-
if (failed[1] !==
|
|
889
|
+
if (failed[1] !== lootProgram()) return null;
|
|
890
890
|
const m = /custom program error: 0x([0-9a-f]+)/i.exec(failed[2]);
|
|
891
891
|
const code = m ? parseInt(m[1], 16) : null;
|
|
892
892
|
return code !== null && ERRORS[code] ? { code, ...ERRORS[code] } : null;
|
|
@@ -896,7 +896,7 @@ export function lootError(logs) {
|
|
|
896
896
|
continue;
|
|
897
897
|
}
|
|
898
898
|
// A line that names no program (a log line, or an error's own text) is the running program's, when it is known.
|
|
899
|
-
if (running.length && running.at(-1) !==
|
|
899
|
+
if (running.length && running.at(-1) !== lootProgram()) continue;
|
|
900
900
|
const named = /Error Code: (\w+)\. Error Number: (\d+)\. Error Message: (.+?)\.?$/.exec(line);
|
|
901
901
|
if (named)
|
|
902
902
|
return {
|