@xgamesfun/solana 0.28.0 → 0.28.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +24 -4
- package/js/approve/server.mjs +66 -78
- package/js/approve/wallet-adds.mjs +180 -0
- package/js/approve/with-key.mjs +7 -4
- package/js/chain/borsh.js +5 -3
- package/js/chain/pda.js +6 -0
- 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 +3 -3
- 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/pda.d.ts +6 -0
- 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 +16 -16
- 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,180 @@
|
|
|
1
|
+
// What a wallet may add to a transaction it was asked to sign as it stood, and nothing else.
|
|
2
|
+
//
|
|
3
|
+
// The approval page prepares each transaction, shows what it does, and sends what the wallet signed. Some wallets add
|
|
4
|
+
// to a transaction before they sign it: Phantom adds its own checks (instructions of the Lighthouse program, which
|
|
5
|
+
// read accounts and fail the transaction when something is not as the wallet expected). The page takes such a
|
|
6
|
+
// transaction only when everything that was prepared and shown is still in it, untouched, and everything else is of
|
|
7
|
+
// the two programs a wallet adds for its own sake: Compute Budget and Lighthouse. Then what is sent is the wallet's
|
|
8
|
+
// own message with the wallet's own signature. Anything else is refused, as any change was before.
|
|
9
|
+
//
|
|
10
|
+
// The rule, each part of which is needed:
|
|
11
|
+
// - Both messages are legacy, with no lookup table: every account is in the message itself, so every account is
|
|
12
|
+
// compared by its address.
|
|
13
|
+
// - The wallet is the message's one signer and its fee payer, in both. A transaction a fresh key signs too (a new
|
|
14
|
+
// mint, a new collection) can't be changed: that key signed the prepared message.
|
|
15
|
+
// - The same blockhash.
|
|
16
|
+
// - Every prepared instruction is there, in the prepared order, with the same program, the same accounts and the
|
|
17
|
+
// same data.
|
|
18
|
+
// - Every account a prepared instruction names (its program too) is a signer or writable in the wallet's message
|
|
19
|
+
// exactly as it was in the prepared one. Whether an account can be written is the message's word, not one
|
|
20
|
+
// instruction's: an added instruction that asks to write an account of ours would make it writable for ours too.
|
|
21
|
+
// - Every other instruction is the Lighthouse program's, or a Compute Budget instruction that asks for a heap size
|
|
22
|
+
// or a limit on loaded data the prepared transaction does not set. Never a unit limit or a unit price: the fee of a
|
|
23
|
+
// prepared transaction is the one it was shown with.
|
|
24
|
+
// - Nothing added names the loader of upgradeable programs, which would let a program's own account be written.
|
|
25
|
+
import { equalBytes } from '../chain/bytes.js';
|
|
26
|
+
import { COMPUTE_BUDGET_PROGRAM, LIGHTHOUSE_PROGRAM } from '../chain/pda.js';
|
|
27
|
+
import { readMessage } from '../chain/tx.js';
|
|
28
|
+
|
|
29
|
+
/** What a Compute Budget instruction asks for, by its first byte. */
|
|
30
|
+
const BUDGET_ASKS = {
|
|
31
|
+
1: 'a heap size',
|
|
32
|
+
2: 'a compute unit limit',
|
|
33
|
+
3: 'a compute unit price (a priority fee of its own)',
|
|
34
|
+
4: 'a limit on the account data loaded',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The kinds that set a transaction's fee: its unit limit and its unit price. */
|
|
38
|
+
const FEE_KINDS = new Set([2, 3]);
|
|
39
|
+
/** The loader of upgradeable programs: named anywhere in a transaction, it lets a program's own account be written. */
|
|
40
|
+
const UPGRADEABLE_LOADER = 'BPFLoaderUpgradeab1e11111111111111111111111';
|
|
41
|
+
|
|
42
|
+
/** A refusal of a wallet's message: `why`, in words that go after "it …". */
|
|
43
|
+
class Refused extends Error {}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A legacy message's accounts and instructions with nothing left as a number:
|
|
47
|
+
* { blockhash, signers, flags (address → { signer, writable }), instructions: [{ program, accounts: [address], data }] }.
|
|
48
|
+
* Throws `Refused` for a message that is not legacy, that names an account twice, or whose own numbers do not add up.
|
|
49
|
+
*/
|
|
50
|
+
function laidOut(message) {
|
|
51
|
+
let m;
|
|
52
|
+
try {
|
|
53
|
+
m = readMessage(message);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Refused('returned something that is not a transaction');
|
|
56
|
+
}
|
|
57
|
+
if (m.version !== 'legacy' || m.lookups.length)
|
|
58
|
+
throw new Refused('returned a transaction that loads accounts from a lookup table, which can’t be compared');
|
|
59
|
+
const { numRequiredSignatures: signed, numReadonlySigned, numReadonlyUnsigned } = m.header;
|
|
60
|
+
const count = m.keys.length;
|
|
61
|
+
if (signed < 1 || signed > count || numReadonlySigned >= signed || numReadonlyUnsigned > count - signed)
|
|
62
|
+
throw new Refused('returned a transaction whose header does not add up');
|
|
63
|
+
if (new Set(m.keys).size !== count) throw new Refused('returned a transaction that names an account twice');
|
|
64
|
+
const flags = new Map(
|
|
65
|
+
m.keys.map((address, i) => [
|
|
66
|
+
address,
|
|
67
|
+
{
|
|
68
|
+
signer: i < signed,
|
|
69
|
+
writable: i < signed - numReadonlySigned || (i >= signed && i < count - numReadonlyUnsigned),
|
|
70
|
+
},
|
|
71
|
+
]),
|
|
72
|
+
);
|
|
73
|
+
const at = i => {
|
|
74
|
+
if (i >= count) throw new Refused('returned a transaction that names an account it does not list');
|
|
75
|
+
return m.keys[i];
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
blockhash: m.blockhash,
|
|
79
|
+
signers: m.keys.slice(0, signed),
|
|
80
|
+
flags,
|
|
81
|
+
instructions: m.instructions.map(ix => ({
|
|
82
|
+
program: at(ix.programIndex),
|
|
83
|
+
accounts: ix.accountIndexes.map(at),
|
|
84
|
+
data: ix.data,
|
|
85
|
+
})),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sameInstruction = (a, b) =>
|
|
90
|
+
a.program === b.program &&
|
|
91
|
+
a.accounts.length === b.accounts.length &&
|
|
92
|
+
a.accounts.every((address, i) => address === b.accounts[i]) &&
|
|
93
|
+
equalBytes(a.data, b.data);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether `theirs`, the message a wallet signed, is `ours`, the message prepared for it, with nothing but a wallet's
|
|
97
|
+
* own additions. `wallet`: the address the transaction was prepared for.
|
|
98
|
+
* → { ok: true, added: [what it added, in words] } or { ok: false, why: what it did, in words after "it …" }.
|
|
99
|
+
* Two messages that are the same bytes are not this function's to judge (nothing was added).
|
|
100
|
+
*/
|
|
101
|
+
export function walletAdded(ours, theirs, wallet) {
|
|
102
|
+
try {
|
|
103
|
+
// (the prepared message is this package's own: one this can't lay out is one a wallet may not change)
|
|
104
|
+
let a;
|
|
105
|
+
try {
|
|
106
|
+
a = laidOut(ours);
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Refused('changed a transaction that can only be sent as it was prepared');
|
|
109
|
+
}
|
|
110
|
+
const b = laidOut(theirs);
|
|
111
|
+
if (a.signers.length !== 1 || a.signers[0] !== wallet)
|
|
112
|
+
throw new Refused(
|
|
113
|
+
'changed a transaction that another key signs too (a new account’s own key signed it as it was prepared)',
|
|
114
|
+
);
|
|
115
|
+
if (b.signers.length !== 1) throw new Refused('added a signer');
|
|
116
|
+
if (b.signers[0] !== wallet) throw new Refused('put another fee payer on it');
|
|
117
|
+
if (a.blockhash !== b.blockhash) throw new Refused('put another blockhash on it');
|
|
118
|
+
|
|
119
|
+
// Every prepared instruction, in its order; whatever stands between them is what the wallet added.
|
|
120
|
+
const added = [];
|
|
121
|
+
let next = 0;
|
|
122
|
+
for (const ix of b.instructions) {
|
|
123
|
+
if (next < a.instructions.length && sameInstruction(ix, a.instructions[next])) next++;
|
|
124
|
+
else added.push(ix);
|
|
125
|
+
}
|
|
126
|
+
if (next !== a.instructions.length) throw new Refused('changed, dropped or moved one of the prepared instructions');
|
|
127
|
+
if (!added.length) throw new Refused('changed the transaction’s accounts');
|
|
128
|
+
|
|
129
|
+
// No account of ours is more, or less, than it was: a signer, or writable.
|
|
130
|
+
const mine = new Set(a.instructions.flatMap(ix => [ix.program, ...ix.accounts]));
|
|
131
|
+
for (const address of mine) {
|
|
132
|
+
const was = a.flags.get(address);
|
|
133
|
+
const is = b.flags.get(address);
|
|
134
|
+
if (!is || was.signer !== is.signer || was.writable !== is.writable)
|
|
135
|
+
throw new Refused(
|
|
136
|
+
`changed what the account ${address} is in the transaction (${was.writable ? 'it could be written, and no longer can' : 'it could only be read, and can now be written'})`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The network lets a program's own account be written only in a transaction that names the upgradeable loader:
|
|
141
|
+
// without it, an account that is called as a program is read-only whatever the message says. So the loader, named
|
|
142
|
+
// by anything added, would change what a prepared instruction can write. It is never the wallet's to add.
|
|
143
|
+
if (b.flags.has(UPGRADEABLE_LOADER) && !a.flags.has(UPGRADEABLE_LOADER))
|
|
144
|
+
throw new Refused('named the upgradeable loader, which the prepared transaction does not');
|
|
145
|
+
|
|
146
|
+
// What it added is a wallet's own: instructions of the Lighthouse program, or a heap size or a limit on loaded
|
|
147
|
+
// data the transaction did not set. Never a unit limit or a unit price: a prepared transaction's fee is its own
|
|
148
|
+
// (this package's setups set both), and a wallet's price on top of it is not taken.
|
|
149
|
+
const set = new Set(
|
|
150
|
+
a.instructions.filter(ix => ix.program === COMPUTE_BUDGET_PROGRAM).map(ix => (ix.data.length ? ix.data[0] : -1)),
|
|
151
|
+
);
|
|
152
|
+
const words = [];
|
|
153
|
+
let own = 0;
|
|
154
|
+
for (const ix of added) {
|
|
155
|
+
if (ix.program === LIGHTHOUSE_PROGRAM) {
|
|
156
|
+
own++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ix.program !== COMPUTE_BUDGET_PROGRAM) throw new Refused(`added an instruction of the program ${ix.program}`);
|
|
160
|
+
const kind = ix.data.length ? ix.data[0] : -1;
|
|
161
|
+
const what = BUDGET_ASKS[kind];
|
|
162
|
+
if (!what) throw new Refused('added a compute budget instruction of a kind not known here');
|
|
163
|
+
if (ix.accounts.length)
|
|
164
|
+
throw new Refused('added a compute budget instruction that names accounts, which none does');
|
|
165
|
+
if (set.has(kind))
|
|
166
|
+
throw new Refused(
|
|
167
|
+
`added ${what.replace(/^a /, 'a second ')}: the transaction sets its own, and the network refuses one that sets it twice`,
|
|
168
|
+
);
|
|
169
|
+
if (FEE_KINDS.has(kind))
|
|
170
|
+
throw new Refused(`added ${what}: the fee of a transaction prepared here is the one it was shown with`);
|
|
171
|
+
set.add(kind);
|
|
172
|
+
words.push(what);
|
|
173
|
+
}
|
|
174
|
+
if (own) words.push(`${own} instruction${own === 1 ? '' : 's'} of the Lighthouse program (a wallet’s own checks)`);
|
|
175
|
+
return { ok: true, added: words };
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (!(e instanceof Refused)) throw e;
|
|
178
|
+
return { ok: false, why: e.message };
|
|
179
|
+
}
|
|
180
|
+
}
|
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/pda.js
CHANGED
|
@@ -10,6 +10,12 @@ export const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
|
|
|
10
10
|
export const TOKEN_2022_PROGRAM = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
|
|
11
11
|
export const ASSOCIATED_TOKEN_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
|
|
12
12
|
export const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
13
|
+
/**
|
|
14
|
+
* Lighthouse: a program of assertions (it reads accounts and fails the transaction when one is not as expected),
|
|
15
|
+
* which a wallet may add to a transaction before signing it (Phantom does). The same address on mainnet and devnet:
|
|
16
|
+
* its source's `declare_id!` and its README, github.com/Jac0xb/lighthouse.
|
|
17
|
+
*/
|
|
18
|
+
export const LIGHTHOUSE_PROGRAM = 'L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95';
|
|
13
19
|
export const METAPLEX_METADATA_PROGRAM = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s';
|
|
14
20
|
/** Metaplex Core: the program of Core assets and collections. */
|
|
15
21
|
export const MPL_CORE_PROGRAM = 'CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d';
|
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 {
|
|
@@ -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',
|