@xgamesfun/solana 0.28.0 → 0.28.1
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/README.md +11 -11
- package/js/approve/page.js +8 -0
- package/js/approve/server.mjs +35 -52
- package/js/approve/wallet-adds.mjs +180 -0
- package/js/chain/pda.js +6 -0
- package/js/economy/plan.mjs +2 -2
- package/package.json +1 -1
- package/release.json +1 -1
- package/skills/xsolana-economy/SKILL.md +5 -5
- package/types/chain/pda.d.ts +6 -0
- package/types/economy/plan.d.mts +2 -2
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ codex plugin add xsolana@xgamesfun
|
|
|
76
76
|
|
|
77
77
|
**claude.ai, ChatGPT or Claude Desktop:** add `https://xgames.fun/mcp` as a custom connector. Nothing to install.
|
|
78
78
|
|
|
79
|
-
**Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.28.
|
|
79
|
+
**Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.28.1 xsolana mcp`.
|
|
80
80
|
|
|
81
81
|
**Any agent that reads skills** (Copilot, Gemini CLI, Windsurf…): `npx skills add XGamesFun/solana-kit`.
|
|
82
82
|
|
|
@@ -87,26 +87,26 @@ are in [docs/harnesses.md](docs/harnesses.md).
|
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
89
|
# find a worked example close to your idea
|
|
90
|
-
npx -y @xgamesfun/solana@0.28.
|
|
90
|
+
npx -y @xgamesfun/solana@0.28.1 examples shop with loot boxes
|
|
91
91
|
|
|
92
92
|
# using a coin you already launched? read it from the chain into its "tokens" entry
|
|
93
|
-
npx -y @xgamesfun/solana@0.28.
|
|
93
|
+
npx -y @xgamesfun/solana@0.28.1 token <its mint address>
|
|
94
94
|
|
|
95
95
|
# 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.
|
|
96
|
+
npx -y @xgamesfun/solana@0.28.1 check xsolana.json
|
|
97
|
+
npx -y @xgamesfun/solana@0.28.1 simulate xsolana.json
|
|
98
|
+
npx -y @xgamesfun/solana@0.28.1 cost xsolana.json
|
|
99
99
|
|
|
100
100
|
# 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.
|
|
101
|
+
npx -y @xgamesfun/solana@0.28.1 explain xsolana.json
|
|
102
|
+
npx -y @xgamesfun/solana@0.28.1 review xsolana.json
|
|
103
103
|
|
|
104
104
|
# go live: a key file for the project (it prints the address to send devnet SOL to, never the key)…
|
|
105
|
-
npx -y @xgamesfun/solana@0.28.
|
|
105
|
+
npx -y @xgamesfun/solana@0.28.1 keygen ~/.config/xsolana/my-game.json
|
|
106
106
|
# …then everything going live would send, and a code; nothing is sent
|
|
107
|
-
npx -y @xgamesfun/solana@0.28.
|
|
107
|
+
npx -y @xgamesfun/solana@0.28.1 approve xsolana.json --keypair ~/.config/xsolana/my-game.json
|
|
108
108
|
# …and with that code, exactly what it listed is sent
|
|
109
|
-
npx -y @xgamesfun/solana@0.28.
|
|
109
|
+
npx -y @xgamesfun/solana@0.28.1 approve xsolana.json --keypair ~/.config/xsolana/my-game.json --yes <code>
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
Each command names an exact version, so you always run the release you meant to. `xsolana help` lists them all.
|
package/js/approve/page.js
CHANGED
|
@@ -62,8 +62,16 @@ export function approvalPage(token) {
|
|
|
62
62
|
let last = null;
|
|
63
63
|
// The legal lines the box was last shown with.
|
|
64
64
|
let legalShown = new Set();
|
|
65
|
+
// How many of the lines about what the wallet added have been said on this page.
|
|
66
|
+
let addedSaid = 0;
|
|
65
67
|
function render(state) {
|
|
66
68
|
last = state;
|
|
69
|
+
// What the wallet added to transactions that landed (its own checks): they were sent as it signed them. Said
|
|
70
|
+
// whenever a state arrives, so one that landed in a batch that then stopped is said too.
|
|
71
|
+
const addedLines = state.walletAdded ?? [];
|
|
72
|
+
for (const line of addedLines.slice(addedSaid))
|
|
73
|
+
log(`${line} It was sent as your wallet signed it: everything listed here is in it, untouched.`);
|
|
74
|
+
addedSaid = Math.max(addedSaid, addedLines.length);
|
|
67
75
|
// A token's name the spec changes and the chain can't take (`unwritten`): said whether or not anything else was
|
|
68
76
|
// sent. With nothing else to send, the page never calls that a landing.
|
|
69
77
|
const unwritten = state.unwritten ?? [];
|
package/js/approve/server.mjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// wallet, served only when asked for (`xsolana approve --keypair`, js/approve/with-key.mjs, drives this same approval
|
|
3
3
|
// with a key file and no page). The
|
|
4
4
|
// server builds each transaction (js/economy/plan.mjs), has the fresh keys a setup makes sign their part, and sends
|
|
5
|
-
// only what it prepared: a wallet's answer must carry
|
|
6
|
-
//
|
|
5
|
+
// only what it prepared and showed: a wallet's answer must carry that wallet's valid signature of the exact message
|
|
6
|
+
// prepared for it, or of that message with nothing but a wallet's own additions (js/approve/wallet-adds.mjs: its
|
|
7
|
+
// Lighthouse checks, a compute budget). Each prepared batch answers once and expires after 90 seconds. What lands is written back into the spec
|
|
7
8
|
// file as it lands, so a setup cut short goes on from where it stopped. The server never sees the owner's key.
|
|
8
9
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
9
10
|
import { chmod, mkdtemp, open as openFile, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
@@ -16,9 +17,9 @@ import { equalBytes, fromBase64, toBase64 } from '../chain/bytes.js';
|
|
|
16
17
|
import { alreadyProcessed, checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
|
|
17
18
|
import { addressBytes, encodeBase58 } from '../chain/base58.js';
|
|
18
19
|
import { slotHashes, tableAddress } from '../chain/lookup.js';
|
|
19
|
-
import {
|
|
20
|
+
import { SYSVAR_SLOT_HASHES } from '../chain/pda.js';
|
|
20
21
|
import { MINT_BYTES } from '../chain/token.js';
|
|
21
|
-
import { compileMessage,
|
|
22
|
+
import { compileMessage, readTransaction, wireTransaction } from '../chain/tx.js';
|
|
22
23
|
import { accountKind, decode } from '../client/accounts.js';
|
|
23
24
|
import { LOOT_PROGRAM, pda } from '../client/pda.js';
|
|
24
25
|
import { check, describe } from '../economy/check.mjs';
|
|
@@ -30,6 +31,7 @@ import { madeSoFar, plan, PER_APPROVAL } from '../economy/plan.mjs';
|
|
|
30
31
|
import { compareDetails, liveStepDetails, namesOf, outsideAddresses, stepDetails } from './details.mjs';
|
|
31
32
|
import { freshKeypair, verifySignature } from './ed25519.mjs';
|
|
32
33
|
import { checkLive, checkMints, LiveMismatch, MintMismatch, readRenames } from './live-check.mjs';
|
|
34
|
+
import { walletAdded } from './wallet-adds.mjs';
|
|
33
35
|
|
|
34
36
|
const PREPARE_TTL_MS = 90_000;
|
|
35
37
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -150,6 +152,8 @@ export async function createApproval({
|
|
|
150
152
|
// Goes up each time the live section changes: a batch prepared before a change is never sent.
|
|
151
153
|
let generation = 0;
|
|
152
154
|
const signatures = [];
|
|
155
|
+
// What a wallet added to transactions this session sent (its own checks, a compute budget): said on the page.
|
|
156
|
+
const walletAdds = [];
|
|
153
157
|
const at = now();
|
|
154
158
|
const mode = !spec.live ? 'new' : spec.live.partial ? 'resume' : 'change';
|
|
155
159
|
/**
|
|
@@ -413,6 +417,8 @@ export async function createApproval({
|
|
|
413
417
|
steps,
|
|
414
418
|
landed: [...done].map(id => ({ id, says: said.get(id)?.says ?? id, details: said.get(id)?.details ?? [] })),
|
|
415
419
|
signatures: [...signatures],
|
|
420
|
+
// (what a wallet added to transactions it signed, in words: they were sent as it signed them)
|
|
421
|
+
walletAdded: [...walletAdds],
|
|
416
422
|
// The project the setup sends to, once a wallet is connected: always that wallet's own (or the one it is the
|
|
417
423
|
// authority of).
|
|
418
424
|
project: extra.project ?? null,
|
|
@@ -597,7 +603,7 @@ export async function createApproval({
|
|
|
597
603
|
prepared = null;
|
|
598
604
|
if (batch.generation !== generation)
|
|
599
605
|
throw new ApproveError(409, 'Something landed after that batch was built: ask for the next one.');
|
|
600
|
-
const wires = checkSigned(batch.wires, signed, wallet);
|
|
606
|
+
const { wires, added } = checkSigned(batch.wires, signed, wallet);
|
|
601
607
|
busy = true;
|
|
602
608
|
try {
|
|
603
609
|
for (const [i, wire] of wires.entries()) {
|
|
@@ -654,6 +660,8 @@ export async function createApproval({
|
|
|
654
660
|
}
|
|
655
661
|
}
|
|
656
662
|
signatures.push(signature);
|
|
663
|
+
// (what the wallet added to it is said once it has landed, never of one that did not)
|
|
664
|
+
if (added[i]) walletAdds.push(added[i]);
|
|
657
665
|
await landed(sent, batch.ctx, signature, { more: i < wires.length - 1 });
|
|
658
666
|
}
|
|
659
667
|
} finally {
|
|
@@ -928,53 +936,18 @@ const sameText = (a, b) => {
|
|
|
928
936
|
return x.length === y.length && timingSafeEqual(x, y);
|
|
929
937
|
};
|
|
930
938
|
|
|
931
|
-
/** What a Compute Budget instruction asks for, by its first byte. */
|
|
932
|
-
const BUDGET_ASKS = {
|
|
933
|
-
1: 'a heap size',
|
|
934
|
-
2: 'a compute unit limit',
|
|
935
|
-
3: 'a compute unit price (a priority fee of its own)',
|
|
936
|
-
4: 'a limit on the account data loaded',
|
|
937
|
-
};
|
|
938
|
-
|
|
939
939
|
/**
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
940
|
+
* The wallet's signed copies, as they are sent: { wires, added }. Each is the exact prepared message with the wallet's
|
|
941
|
+
* valid signature, or, from a wallet that adds to what it signs, the wallet's own message with its signature, taken
|
|
942
|
+
* only when everything prepared is in it untouched and what it added is a wallet's own (`walletAdded`,
|
|
943
|
+
* js/approve/wallet-adds.mjs: instructions of the Lighthouse program, a heap size). `added`: for each transaction, what
|
|
944
|
+
* was added to it in words, or null. Anything else is refused, and nothing is sent.
|
|
943
945
|
*/
|
|
944
|
-
export function walletChange(ours, theirs) {
|
|
945
|
-
try {
|
|
946
|
-
const [a, b] = [readMessage(ours), readMessage(theirs)];
|
|
947
|
-
const told = m =>
|
|
948
|
-
m.instructions.map(ix => ({
|
|
949
|
-
program: m.keys[ix.programIndex],
|
|
950
|
-
kind: ix.data[0],
|
|
951
|
-
text: `${m.keys[ix.programIndex]} ${ix.accountIndexes.map(i => m.keys[i] ?? `#${i}`).join(',')} ${toBase64(ix.data)}`,
|
|
952
|
-
}));
|
|
953
|
-
const mine = told(a);
|
|
954
|
-
const added = [];
|
|
955
|
-
let at = 0;
|
|
956
|
-
for (const ix of told(b)) {
|
|
957
|
-
if (at < mine.length && ix.text === mine[at].text) at++;
|
|
958
|
-
else added.push(ix);
|
|
959
|
-
}
|
|
960
|
-
if (at !== mine.length || !added.length)
|
|
961
|
-
return a.blockhash !== b.blockhash ? ' (it put another blockhash on it)' : '';
|
|
962
|
-
const what = added.map(ix =>
|
|
963
|
-
ix.program === COMPUTE_BUDGET_PROGRAM
|
|
964
|
-
? (BUDGET_ASKS[ix.kind] ?? 'a compute budget instruction')
|
|
965
|
-
: `an instruction of the program ${ix.program}`,
|
|
966
|
-
);
|
|
967
|
-
return ` (it added ${[...new Set(what)].join(', ')})`;
|
|
968
|
-
} catch {
|
|
969
|
-
return '';
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
/** The wallet's signed copies: each the exact prepared message, each with the wallet's valid signature. */
|
|
974
946
|
function checkSigned(prepared, signed, wallet) {
|
|
975
947
|
if (!Array.isArray(signed) || signed.length !== prepared.length)
|
|
976
948
|
throw new ApproveError(400, 'The wallet’s answer does not match the request.');
|
|
977
|
-
|
|
949
|
+
const added = prepared.map(() => null);
|
|
950
|
+
const wires = prepared.map((wire, i) => {
|
|
978
951
|
let mine;
|
|
979
952
|
let theirs;
|
|
980
953
|
try {
|
|
@@ -983,11 +956,20 @@ function checkSigned(prepared, signed, wallet) {
|
|
|
983
956
|
} catch {
|
|
984
957
|
throw new ApproveError(400, 'The wallet returned something that is not a transaction.');
|
|
985
958
|
}
|
|
986
|
-
if (!equalBytes(mine.message, theirs.message))
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
959
|
+
if (!equalBytes(mine.message, theirs.message)) {
|
|
960
|
+
const taken = walletAdded(mine.message, theirs.message, wallet);
|
|
961
|
+
if (!taken.ok)
|
|
962
|
+
throw new ApproveError(
|
|
963
|
+
400,
|
|
964
|
+
`The wallet changed the transaction before signing it (it ${taken.why}), so nothing was sent. A wallet may add its own checks (instructions of the Lighthouse program) to a transaction it alone signs; everything that was prepared and shown here must be in it untouched, and nothing else may be. (A setup that has not started can be signed by a key file instead: xsolana approve --keypair. One that has started is finished by the key that started it.)`,
|
|
965
|
+
);
|
|
966
|
+
// The wallet's own message, with the wallet's own signature: it is the transaction's one signer.
|
|
967
|
+
const signature = theirs.signatures[0];
|
|
968
|
+
if (theirs.signers[0] !== wallet || !verifySignature(wallet, theirs.message, signature))
|
|
969
|
+
throw new ApproveError(400, 'The wallet’s signature is missing or does not match your wallet.');
|
|
970
|
+
added[i] = `Transaction ${i + 1} of its batch: your wallet added ${taken.added.join(', ')}.`;
|
|
971
|
+
return wireTransaction(theirs.message, [signature]);
|
|
972
|
+
}
|
|
991
973
|
const slot = mine.signers.indexOf(wallet);
|
|
992
974
|
if (slot < 0) throw new ApproveError(400, 'This transaction was not prepared for that wallet.');
|
|
993
975
|
const signature = theirs.signatures[slot];
|
|
@@ -997,6 +979,7 @@ function checkSigned(prepared, signed, wallet) {
|
|
|
997
979
|
signatures[slot] = signature;
|
|
998
980
|
return wireTransaction(mine.message, signatures);
|
|
999
981
|
});
|
|
982
|
+
return { wires, added };
|
|
1000
983
|
}
|
|
1001
984
|
|
|
1002
985
|
// ---------------------------------------------------------------- the page
|
|
@@ -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/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/economy/plan.mjs
CHANGED
|
@@ -1892,8 +1892,8 @@ export const SETUP_UNIT_PRICE = 1n;
|
|
|
1892
1892
|
/**
|
|
1893
1893
|
* A setup transaction's instructions as the owner's wallet is handed them: led by a compute unit limit and a compute
|
|
1894
1894
|
* unit price, each once. A wallet that adds its own priority fee (Phantom does) adds it only to a transaction that
|
|
1895
|
-
* sets neither, and the approval page
|
|
1896
|
-
* itself. The limit is all a transaction can have (1,400,000 units): nothing here measures what a step takes, a box's
|
|
1895
|
+
* sets neither, and the approval page takes no price a wallet puts on a transaction that has one, so every setup
|
|
1896
|
+
* transaction sets both itself: its fee is the plan's own, whatever the wallet. The limit is all a transaction can have (1,400,000 units): nothing here measures what a step takes, a box's
|
|
1897
1897
|
* write needs it, and no step may fail for want of compute. A limit or a price the instructions already carry is
|
|
1898
1898
|
* kept.
|
|
1899
1899
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgamesfun/solana",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"mcpName": "io.github.XGamesFun/solana-kit",
|
|
5
5
|
"description": "Loot boxes, gacha, shops, lotteries, auctions, NFT staking, crafting and rewards on Solana without writing a smart contract: one shared program, its JS client, a CLI, and an MCP server and skill so Claude Code, Codex or any AI agent can design, check, simulate and launch an economy.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/release.json
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
"sbfBytes": 1794616,
|
|
5
5
|
"sbfSha256": "ac8b09ed2c3a3f7daa2dfab55d2f6dea19fa43e4c98b44a82da2a68a7bc8ebbe",
|
|
6
6
|
"idlSha256": "ef3b8043d62c606e850df75773b4535cdb08206216e17aca653111a77403abd7",
|
|
7
|
-
"clientSha256": "
|
|
7
|
+
"clientSha256": "3ef11453089e952e50d9d5a7199d9629b3417a091a071ea6dfd90f42746a2421"
|
|
8
8
|
}
|
|
@@ -10,9 +10,9 @@ writes or deploys a contract: the spec is checked, simulated and priced by the `
|
|
|
10
10
|
signed by a key file on the owner's own machine, once they have read what will be sent and said yes. It runs on
|
|
11
11
|
**devnet** until the program is audited.
|
|
12
12
|
|
|
13
|
-
The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.28.
|
|
14
|
-
`npx -y @xgamesfun/solana@0.28.
|
|
15
|
-
or `npx -y @xgamesfun/solana@0.28.
|
|
13
|
+
The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.28.1 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
|
|
14
|
+
`npx -y @xgamesfun/solana@0.28.1 token <mint>` for a coin made elsewhere,
|
|
15
|
+
or `npx -y @xgamesfun/solana@0.28.1 tool <name> '<json>'` for any tool (always with the exact version).
|
|
16
16
|
|
|
17
17
|
## Rules (always)
|
|
18
18
|
|
|
@@ -137,7 +137,7 @@ or `npx -y @xgamesfun/solana@0.28.0 tool <name> '<json>'` for any tool (always w
|
|
|
137
137
|
9. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the commands, which you
|
|
138
138
|
run in the project folder exactly as given (they name the exact version; never drop it):
|
|
139
139
|
- **A new economy gets a key file**, unless the project has one:
|
|
140
|
-
`npx -y @xgamesfun/solana@0.28.
|
|
140
|
+
`npx -y @xgamesfun/solana@0.28.1 keygen ~/.config/xsolana/<name>.json`. Give the person the address it prints. A
|
|
141
141
|
Solana key file they already have and name themselves (`~/.config/solana/id.json`) works the same; never go
|
|
142
142
|
looking for one.
|
|
143
143
|
- **Say what the key file is** before anything is sent: the project's owner, and, unless the spec says otherwise,
|
|
@@ -152,7 +152,7 @@ or `npx -y @xgamesfun/solana@0.28.0 tool <name> '<json>'` for any tool (always w
|
|
|
152
152
|
(`xsolana_cost`), any seed in a coin that already exists, and every NFT a stash, a raffle or an auction names. A
|
|
153
153
|
collection they already have needs the key's address as its update authority. If they would rather not move
|
|
154
154
|
those, offer the browser wallet.
|
|
155
|
-
- **Show it**: `npx -y @xgamesfun/solana@0.28.
|
|
155
|
+
- **Show it**: `npx -y @xgamesfun/solana@0.28.1 approve xsolana.json --keypair <key file>` prints every step with
|
|
156
156
|
what it pays and trusts, the warnings, the notice and a code, and sends nothing. Give the person that printout as
|
|
157
157
|
it is, not a summary of it, and ask.
|
|
158
158
|
- **On their yes**, the same command with `--yes <code>`. What lands is written back into `xsolana.json`. If it
|
package/types/chain/pda.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ export declare const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5D
|
|
|
3
3
|
export declare const TOKEN_2022_PROGRAM = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
|
4
4
|
export declare const ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
5
5
|
export declare const COMPUTE_BUDGET_PROGRAM = "ComputeBudget111111111111111111111111111111";
|
|
6
|
+
/**
|
|
7
|
+
* Lighthouse: a program of assertions (it reads accounts and fails the transaction when one is not as expected),
|
|
8
|
+
* which a wallet may add to a transaction before signing it (Phantom does). The same address on mainnet and devnet:
|
|
9
|
+
* its source's `declare_id!` and its README, github.com/Jac0xb/lighthouse.
|
|
10
|
+
*/
|
|
11
|
+
export declare const LIGHTHOUSE_PROGRAM = "L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95";
|
|
6
12
|
export declare const METAPLEX_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
|
|
7
13
|
/** Metaplex Core: the program of Core assets and collections. */
|
|
8
14
|
export declare const MPL_CORE_PROGRAM = "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d";
|
package/types/economy/plan.d.mts
CHANGED
|
@@ -1051,8 +1051,8 @@ export declare const SETUP_UNIT_PRICE = 1n;
|
|
|
1051
1051
|
/**
|
|
1052
1052
|
* A setup transaction's instructions as the owner's wallet is handed them: led by a compute unit limit and a compute
|
|
1053
1053
|
* unit price, each once. A wallet that adds its own priority fee (Phantom does) adds it only to a transaction that
|
|
1054
|
-
* sets neither, and the approval page
|
|
1055
|
-
* itself. The limit is all a transaction can have (1,400,000 units): nothing here measures what a step takes, a box's
|
|
1054
|
+
* sets neither, and the approval page takes no price a wallet puts on a transaction that has one, so every setup
|
|
1055
|
+
* transaction sets both itself: its fee is the plan's own, whatever the wallet. The limit is all a transaction can have (1,400,000 units): nothing here measures what a step takes, a box's
|
|
1056
1056
|
* write needs it, and no step may fail for want of compute. A limit or a price the instructions already carry is
|
|
1057
1057
|
* kept.
|
|
1058
1058
|
*/
|