pog-mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/client.d.ts +319 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +728 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +867 -0
- package/dist/server.js.map +1 -0
- package/dist/wallet.d.ts +88 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +562 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +63 -0
- package/skill/SKILL.md +375 -0
- package/skill/reference/measurements.md +192 -0
package/dist/wallet.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wallet — BIP39 mnemonic ⇄ Solana keypair, plus SIWS message signing.
|
|
3
|
+
*
|
|
4
|
+
* SIWS is the single biggest barrier to an agent playing: the game's API needs an
|
|
5
|
+
* ed25519 signature from a Solana keypair, and most agents have neither. Owning a
|
|
6
|
+
* wallet here is what lets `login` be one tool call instead of a crypto tutorial.
|
|
7
|
+
*
|
|
8
|
+
* DERIVATION MUST MATCH THE BROWSER WALLET.
|
|
9
|
+
* SLIP-0010 ed25519 on m/44'/501'/0'/0' — the path Phantom/Solflare use for
|
|
10
|
+
* account 0 — mirroring `packages/web/src/wallets/solanaMnemonic.ts`. Two copies
|
|
11
|
+
* exist because @sws26/shared is deliberately dependency-free and this needs
|
|
12
|
+
* crypto libraries; `__tests__/derivation.test.ts` pins both against the same
|
|
13
|
+
* fixed vectors so a change to either side fails loudly. Getting this wrong does
|
|
14
|
+
* not throw — it silently yields a DIFFERENT address, stranding whatever the
|
|
15
|
+
* agent owned.
|
|
16
|
+
*
|
|
17
|
+
* Unlike the browser wallet this uses @noble/curves directly rather than
|
|
18
|
+
* @solana/web3.js: the only operations needed are "public key from seed" and
|
|
19
|
+
* "sign bytes", and pulling in web3.js for that would add a large dependency to
|
|
20
|
+
* a stdio process that never touches an RPC.
|
|
21
|
+
*/
|
|
22
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, openSync, writeSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
23
|
+
import { randomBytes } from 'node:crypto';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
26
|
+
import { ed25519 } from '@noble/curves/ed25519';
|
|
27
|
+
import { hmac } from '@noble/hashes/hmac';
|
|
28
|
+
import { sha512 } from '@noble/hashes/sha512';
|
|
29
|
+
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39';
|
|
30
|
+
import { wordlist } from '@scure/bip39/wordlists/english';
|
|
31
|
+
import bs58 from 'bs58';
|
|
32
|
+
/** m/44'/501'/0'/0' — Solana (coin type 501), account 0, all segments hardened. */
|
|
33
|
+
const SOLANA_DERIVATION = [44, 501, 0, 0];
|
|
34
|
+
function hmac512(key, data) {
|
|
35
|
+
return hmac(sha512, key, data);
|
|
36
|
+
}
|
|
37
|
+
function masterNode(seed) {
|
|
38
|
+
const i = hmac512(new TextEncoder().encode('ed25519 seed'), seed);
|
|
39
|
+
return { key: i.slice(0, 32), chainCode: i.slice(32) };
|
|
40
|
+
}
|
|
41
|
+
/** ed25519 SLIP-0010 supports hardened derivation only. */
|
|
42
|
+
function deriveHardened(node, index) {
|
|
43
|
+
const hardened = (index | 0x80000000) >>> 0;
|
|
44
|
+
const data = new Uint8Array(37);
|
|
45
|
+
data[0] = 0x00;
|
|
46
|
+
data.set(node.key, 1);
|
|
47
|
+
data[33] = (hardened >>> 24) & 0xff;
|
|
48
|
+
data[34] = (hardened >>> 16) & 0xff;
|
|
49
|
+
data[35] = (hardened >>> 8) & 0xff;
|
|
50
|
+
data[36] = hardened & 0xff;
|
|
51
|
+
const i = hmac512(node.chainCode, data);
|
|
52
|
+
return { key: i.slice(0, 32), chainCode: i.slice(32) };
|
|
53
|
+
}
|
|
54
|
+
/** The 32-byte ed25519 private seed for a mnemonic. */
|
|
55
|
+
export function seedFromMnemonic(mnemonic) {
|
|
56
|
+
let node = masterNode(mnemonicToSeedSync(mnemonic, ''));
|
|
57
|
+
for (const index of SOLANA_DERIVATION)
|
|
58
|
+
node = deriveHardened(node, index);
|
|
59
|
+
return node.key;
|
|
60
|
+
}
|
|
61
|
+
/** Base58 Solana address for a mnemonic. */
|
|
62
|
+
export function addressFromMnemonic(mnemonic) {
|
|
63
|
+
return bs58.encode(ed25519.getPublicKey(seedFromMnemonic(mnemonic)));
|
|
64
|
+
}
|
|
65
|
+
/** Sign raw UTF-8 message bytes; returns a base58 signature, as the API expects. */
|
|
66
|
+
export function signMessage(mnemonic, message) {
|
|
67
|
+
const bytes = new TextEncoder().encode(message);
|
|
68
|
+
return bs58.encode(ed25519.sign(bytes, seedFromMnemonic(mnemonic)));
|
|
69
|
+
}
|
|
70
|
+
/** Generate a fresh 12-word (128-bit) English mnemonic. */
|
|
71
|
+
export function newMnemonic() {
|
|
72
|
+
return generateMnemonic(wordlist, 128);
|
|
73
|
+
}
|
|
74
|
+
/** True when `mnemonic` is a valid BIP39 English phrase. */
|
|
75
|
+
export function isValidMnemonic(mnemonic) {
|
|
76
|
+
return validateMnemonic(mnemonic, wordlist);
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Persistence
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
/**
|
|
82
|
+
* Where the recovery phrase lives.
|
|
83
|
+
*
|
|
84
|
+
* A file, not an env var: the agent must come back as the SAME manager across
|
|
85
|
+
* restarts or it loses its squad, its record, and anything it owns on-chain.
|
|
86
|
+
* `POG_MCP_WALLET_FILE` overrides it so one machine can run several agents.
|
|
87
|
+
*/
|
|
88
|
+
export function walletFilePath(env = process.env) {
|
|
89
|
+
return env['POG_MCP_WALLET_FILE'] ?? join(homedir(), '.pog-mcp', 'wallet.json');
|
|
90
|
+
}
|
|
91
|
+
/** Read the stored wallet, or null when this machine has none yet. */
|
|
92
|
+
/**
|
|
93
|
+
* Read the stored wallet, or null if the file is missing or not a usable record.
|
|
94
|
+
*
|
|
95
|
+
* "Usable" includes the address AGREEING with the mnemonic. A file with a valid
|
|
96
|
+
* phrase but a missing or wrong address used to be accepted, and then `login`
|
|
97
|
+
* requested a nonce for `undefined` — or for an address this key cannot sign
|
|
98
|
+
* for — so the sign-in failed with no hint that the file was the problem, and
|
|
99
|
+
* the corrupt-file refusal never fired.
|
|
100
|
+
*/
|
|
101
|
+
export function readWallet(path = walletFilePath()) {
|
|
102
|
+
try {
|
|
103
|
+
// What KIND of thing is there, before opening it.
|
|
104
|
+
//
|
|
105
|
+
// readFileSync on a FIFO blocks until somebody writes — forever, if nobody
|
|
106
|
+
// does — and this process speaks MCP over stdio, so a hung read is a hung
|
|
107
|
+
// agent with no error to catch. Another local user who can create the
|
|
108
|
+
// configured path can leave one there. A symlink is refused for the reason
|
|
109
|
+
// the directory walk refuses them: its owner chooses where the read lands.
|
|
110
|
+
// The directory checks cannot help here; they run after a wallet has
|
|
111
|
+
// already been parsed.
|
|
112
|
+
// Unconditional: lstat and isFile mean the same thing on Windows, and a
|
|
113
|
+
// junction or named pipe there is no more readable than a symlink or FIFO
|
|
114
|
+
// here. The reason for the check is portable even though FIFOs are not.
|
|
115
|
+
if (!lstatSync(path).isFile())
|
|
116
|
+
return null;
|
|
117
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
118
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
119
|
+
return null;
|
|
120
|
+
const { mnemonic, address } = parsed;
|
|
121
|
+
if (typeof mnemonic !== 'string' || !isValidMnemonic(mnemonic))
|
|
122
|
+
return null;
|
|
123
|
+
if (typeof address !== 'string' || address !== addressFromMnemonic(mnemonic))
|
|
124
|
+
return null;
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The one way an EXISTING wallet gets returned.
|
|
133
|
+
*
|
|
134
|
+
* There are three doors onto the same file — the import that matches what is
|
|
135
|
+
* already stored, the ordinary start with no import, and losing the creation
|
|
136
|
+
* race to another process — and securing one of them secured only one of them.
|
|
137
|
+
* A phrase readable by every local user is just as exposed whichever door the
|
|
138
|
+
* caller came through, so the checks live here instead of at the returns.
|
|
139
|
+
*/
|
|
140
|
+
function acceptExisting(wallet, path) {
|
|
141
|
+
assertDirectoryIsSafe(dirname(path));
|
|
142
|
+
ensurePrivate(path, 0o600);
|
|
143
|
+
return { wallet, created: false };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* What we can and cannot check on Windows.
|
|
147
|
+
*
|
|
148
|
+
* Every guard below is POSIX: mode bits, uid, sticky. Windows has none of them —
|
|
149
|
+
* chmod is close to a no-op there and privacy is an ACL, which Node cannot read.
|
|
150
|
+
* So they all returned early, and a wallet in a shared directory got no
|
|
151
|
+
* protection at all while the code looked like it was protecting one.
|
|
152
|
+
*
|
|
153
|
+
* The one thing that IS knowable without ACL APIs is WHERE the file is. A path
|
|
154
|
+
* under the user's profile inherits that profile's ACL, which Windows makes
|
|
155
|
+
* private by default — that covers the default location and anybody who kept it.
|
|
156
|
+
* Outside the profile — C:\\Temp, a network share, a machine-wide folder — we
|
|
157
|
+
* genuinely cannot tell, so it is refused rather than silently trusted. A real
|
|
158
|
+
* key deserves the fail-closed answer, not the convenient one.
|
|
159
|
+
*/
|
|
160
|
+
function assertWindowsPathIsPrivate(target) {
|
|
161
|
+
const home = resolve(homedir());
|
|
162
|
+
const path = resolve(target);
|
|
163
|
+
if (path === home || path.startsWith(home + sep))
|
|
164
|
+
return;
|
|
165
|
+
throw new Error(`${target} is outside your user profile (${home}), and on Windows this cannot be checked ` +
|
|
166
|
+
'for privacy — file permissions there are ACLs, which nothing here can read, so a ' +
|
|
167
|
+
'wallet placed in a shared folder would be readable by other users with no sign of it. ' +
|
|
168
|
+
'Leave POG_MCP_WALLET_FILE unset to use the default under your profile, or point it ' +
|
|
169
|
+
'somewhere inside it.');
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A directory is checked, never changed.
|
|
173
|
+
*
|
|
174
|
+
* Tightening it was a worse bug than the one it fixed: POG_MCP_WALLET_FILE can
|
|
175
|
+
* point straight into a shared directory — /tmp/pog-wallet.json in a container,
|
|
176
|
+
* ./wallet.json in a checkout — and chmod 0700 there locks every other user and
|
|
177
|
+
* service out of /tmp or the workspace. That directory is not ours to change.
|
|
178
|
+
*
|
|
179
|
+
* What actually endangers the wallet is somebody else being able to REPLACE it,
|
|
180
|
+
* which needs write permission on the directory. Readable is fine: the file
|
|
181
|
+
* itself is 0600, so listing the name gives nothing. And a world-writable
|
|
182
|
+
* directory with the sticky bit — /tmp is 1777 — cannot be used to unlink
|
|
183
|
+
* another user's file, which is exactly what the sticky bit is for. So the test
|
|
184
|
+
* is write-without-sticky, and the answer to it is to refuse and say what to do,
|
|
185
|
+
* not to reach into a directory somebody else is using.
|
|
186
|
+
*/
|
|
187
|
+
function assertDirectoryIsSafe(dir) {
|
|
188
|
+
if (process.platform === 'win32')
|
|
189
|
+
return assertWindowsPathIsPrivate(dir);
|
|
190
|
+
if (typeof process.getuid !== 'function')
|
|
191
|
+
return;
|
|
192
|
+
const me = process.getuid();
|
|
193
|
+
// EVERY component, not just the last one.
|
|
194
|
+
//
|
|
195
|
+
// A path is only as trustworthy as the way in. `/tmp/mine/wallet.json` can
|
|
196
|
+
// have a perfectly good `mine` while `/tmp/mine` is a symlink somebody else
|
|
197
|
+
// owns, or while an ancestor is world-writable — either lets another user
|
|
198
|
+
// rename or retarget a link and send the next start somewhere new. It creates
|
|
199
|
+
// a wallet there, and the squad and assets stay with a phrase nothing points
|
|
200
|
+
// at any more. statSync also FOLLOWS links, so checking the target said
|
|
201
|
+
// nothing about who controls the pointer.
|
|
202
|
+
for (const component of ancestors(dir)) {
|
|
203
|
+
let link;
|
|
204
|
+
try {
|
|
205
|
+
link = lstatSync(component);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
continue; // Not there yet — mkdir is about to create it, at 0700.
|
|
209
|
+
}
|
|
210
|
+
const ownerIsTrusted = link.uid === me || link.uid === 0;
|
|
211
|
+
if (link.isSymbolicLink()) {
|
|
212
|
+
// Root-owned links are how the system is built: /tmp -> /private/tmp on
|
|
213
|
+
// macOS is one, and refusing those would refuse the documented setup.
|
|
214
|
+
if (!ownerIsTrusted) {
|
|
215
|
+
throw new Error(`${component} is a symlink owned by uid ${String(link.uid)}, not by this process ` +
|
|
216
|
+
`(uid ${String(me)}). Whoever owns it can point it somewhere else at any time, and ` +
|
|
217
|
+
'the next start would make a new wallet there while the squad and assets stay with ' +
|
|
218
|
+
'the phrase this one holds. Point POG_MCP_WALLET_FILE somewhere no other user is in ' +
|
|
219
|
+
'the path.');
|
|
220
|
+
}
|
|
221
|
+
continue; // Its target is checked on the next iteration via realpath below.
|
|
222
|
+
}
|
|
223
|
+
if (!ownerIsTrusted && (link.mode & 0o200) !== 0) {
|
|
224
|
+
throw new Error(`${component} is owned by uid ${String(link.uid)}, not by this process ` +
|
|
225
|
+
`(uid ${String(me)}), and that owner can delete or move what is inside it whatever ` +
|
|
226
|
+
'the permissions say. The next start would then generate a different key while the ' +
|
|
227
|
+
'squad and assets stay with the phrase that vanished. Point POG_MCP_WALLET_FILE at a ' +
|
|
228
|
+
'path you own the whole way down.');
|
|
229
|
+
}
|
|
230
|
+
// Sticky is not a blanket pass. POSIX lets the DIRECTORY's owner unlink any
|
|
231
|
+
// entry in it, sticky or not — so a sticky directory belonging to another
|
|
232
|
+
// ordinary user still lets that user delete the only copy of this phrase.
|
|
233
|
+
// The exception exists for /tmp, which root owns; keep it to that shape.
|
|
234
|
+
const groupOrOtherWritable = (link.mode & 0o022) !== 0;
|
|
235
|
+
const sticky = (link.mode & 0o1000) !== 0;
|
|
236
|
+
if (groupOrOtherWritable && !(sticky && ownerIsTrusted)) {
|
|
237
|
+
throw new Error(`${component} is writable by other users, so someone there can replace or delete the ` +
|
|
238
|
+
'wallet below it — and the phrase you have would stop being the one this agent plays ' +
|
|
239
|
+
'as, with nothing left to recover from. ' +
|
|
240
|
+
'Refusing rather than changing a directory that is not ours to change: point ' +
|
|
241
|
+
'POG_MCP_WALLET_FILE at a private directory (the default ~/.pog-mcp is created 0700), ' +
|
|
242
|
+
`or tighten ${component} yourself if it really is only yours.`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Every path component from the root down to `dir`, links resolved.
|
|
248
|
+
*
|
|
249
|
+
* Both spellings matter: the literal path is where the SYMLINKS live (and their
|
|
250
|
+
* owners are the ones who can retarget them), and the resolved path is where
|
|
251
|
+
* the real directories are. Checking one and not the other leaves the other as
|
|
252
|
+
* the way in.
|
|
253
|
+
*/
|
|
254
|
+
function ancestors(dir) {
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
const out = [];
|
|
257
|
+
for (const start of [dir, safeRealpath(dir)]) {
|
|
258
|
+
let current = resolve(start);
|
|
259
|
+
const chain = [];
|
|
260
|
+
for (;;) {
|
|
261
|
+
if (!seen.has(current)) {
|
|
262
|
+
seen.add(current);
|
|
263
|
+
chain.push(current);
|
|
264
|
+
}
|
|
265
|
+
const parent = dirname(current);
|
|
266
|
+
if (parent === current)
|
|
267
|
+
break;
|
|
268
|
+
current = parent;
|
|
269
|
+
}
|
|
270
|
+
// Root first, so a failure names the outermost thing that is wrong.
|
|
271
|
+
out.push(...chain.reverse());
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
function safeRealpath(dir) {
|
|
276
|
+
try {
|
|
277
|
+
return realpathSync(dir);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return dir; // Not fully created yet; the literal chain is all there is.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Make sure a FILE we are about to trust is ours alone, tightening it if not.
|
|
285
|
+
*
|
|
286
|
+
* The creation paths write 0600/0700, which protects a wallet this process
|
|
287
|
+
* made — and nothing else. A phrase restored from a backup, copied off another
|
|
288
|
+
* machine, or written before a umask was fixed arrives with whatever mode it
|
|
289
|
+
* had, and every local user can read it. Same for the DIRECTORY: mkdirSync's
|
|
290
|
+
* `mode` applies only when it creates one, so a wallet installed into an
|
|
291
|
+
* existing group-writable directory can be unlinked and replaced by anyone —
|
|
292
|
+
* and the agent adopts a key someone else chose, or loses its only copy.
|
|
293
|
+
*
|
|
294
|
+
* Tightening rather than refusing, because a wide mode is almost always an
|
|
295
|
+
* innocent umask and refusing would strand an agent on its own wallet. But a
|
|
296
|
+
* file owned by ANOTHER user is not a permissions accident, and chmod would
|
|
297
|
+
* fail on it anyway — that one is refused.
|
|
298
|
+
*
|
|
299
|
+
* A no-op where the concept does not apply: Windows has no POSIX mode bits, and
|
|
300
|
+
* process.getuid is not defined there.
|
|
301
|
+
*/
|
|
302
|
+
function ensurePrivate(target, mode) {
|
|
303
|
+
if (process.platform === 'win32')
|
|
304
|
+
return assertWindowsPathIsPrivate(target);
|
|
305
|
+
if (typeof process.getuid !== 'function')
|
|
306
|
+
return;
|
|
307
|
+
let info;
|
|
308
|
+
try {
|
|
309
|
+
info = statSync(target);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return; // Not there yet; the caller is about to create it at the right mode.
|
|
313
|
+
}
|
|
314
|
+
const me = process.getuid();
|
|
315
|
+
if (info.uid !== me) {
|
|
316
|
+
throw new Error(`${target} is owned by uid ${String(info.uid)}, not by this process (uid ${String(me)}). ` +
|
|
317
|
+
'Refusing to use it: a wallet another user controls can be replaced under you, and the ' +
|
|
318
|
+
'phrase in it may already be theirs. Move it aside, or point POG_MCP_WALLET_FILE ' +
|
|
319
|
+
'somewhere you own.');
|
|
320
|
+
}
|
|
321
|
+
// Any group or other bit at all — read included. The phrase is the key.
|
|
322
|
+
if ((info.mode & 0o077) !== 0)
|
|
323
|
+
chmodSync(target, mode);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Persist a recovery phrase at 0600.
|
|
327
|
+
*
|
|
328
|
+
* This is a real Solana key on a real network. The mode is set explicitly rather
|
|
329
|
+
* than left to umask, because a phrase readable by every process on the box is a
|
|
330
|
+
* different thing from a config file.
|
|
331
|
+
*/
|
|
332
|
+
export function writeWallet(mnemonic, path = walletFilePath(),
|
|
333
|
+
/**
|
|
334
|
+
* When true the write fails with EEXIST instead of clobbering an existing
|
|
335
|
+
* file. Used for first-time creation, where losing the race must NOT mean
|
|
336
|
+
* overwriting the phrase another process already handed out.
|
|
337
|
+
*/
|
|
338
|
+
exclusive = false) {
|
|
339
|
+
const wallet = {
|
|
340
|
+
mnemonic,
|
|
341
|
+
address: addressFromMnemonic(mnemonic),
|
|
342
|
+
createdAt: new Date().toISOString(),
|
|
343
|
+
};
|
|
344
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
345
|
+
writeFileSync(path, `${JSON.stringify(wallet, null, 2)}\n`, {
|
|
346
|
+
mode: 0o600,
|
|
347
|
+
...(exclusive ? { flag: 'wx' } : {}),
|
|
348
|
+
});
|
|
349
|
+
chmodSync(path, 0o600);
|
|
350
|
+
return wallet;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The wallet this server plays as: an existing one, `POG_MCP_MNEMONIC`, or a new
|
|
354
|
+
* one. Importing writes the phrase so an operator can move an agent between
|
|
355
|
+
* machines without hand-editing JSON — but only onto a machine that does not
|
|
356
|
+
* already have one.
|
|
357
|
+
*
|
|
358
|
+
* A different phrase already on disk is a REFUSAL, not an overwrite. That file
|
|
359
|
+
* is a real Solana key: it is very likely the only copy, and the manager it
|
|
360
|
+
* belongs to owns a squad and on-chain assets. Replacing it because someone
|
|
361
|
+
* exported a variable to try another account — or left one exported from an
|
|
362
|
+
* earlier shell — destroys that access with no error and nothing to recover
|
|
363
|
+
* from. The concurrent-creation path already refuses to clobber for exactly this
|
|
364
|
+
* reason; there is no version of "wins over the file" worth that.
|
|
365
|
+
*/
|
|
366
|
+
export function loadOrCreateWallet(env = process.env, path = walletFilePath(env)) {
|
|
367
|
+
const imported = env['POG_MCP_MNEMONIC']?.trim();
|
|
368
|
+
if (imported) {
|
|
369
|
+
if (!isValidMnemonic(imported)) {
|
|
370
|
+
throw new Error('POG_MCP_MNEMONIC is not a valid BIP39 English phrase');
|
|
371
|
+
}
|
|
372
|
+
const existing = readWallet(path);
|
|
373
|
+
if (existing?.mnemonic === imported)
|
|
374
|
+
return acceptExisting(existing, path);
|
|
375
|
+
// existsSync, not `existing !== null`: readWallet also returns null for a
|
|
376
|
+
// file it cannot parse, and an unreadable wallet file is the LAST thing to
|
|
377
|
+
// overwrite — a phrase truncated mid-write is still most of a phrase.
|
|
378
|
+
if (existsSync(path))
|
|
379
|
+
throw importConflict(path);
|
|
380
|
+
// Install through the SAME atomic path as a generated wallet. A plain write
|
|
381
|
+
// here reopened the race from the other side: two processes start with no
|
|
382
|
+
// file, one atomically links a generated phrase, and this one truncates it a
|
|
383
|
+
// moment later — leaving the first process live, caching a key that is no
|
|
384
|
+
// longer on disk, free to build a squad nothing can recover after a restart.
|
|
385
|
+
// The existsSync above cannot prevent that; it is a check, and the write was
|
|
386
|
+
// a separate step.
|
|
387
|
+
const installed = createWalletAtomically(imported, path);
|
|
388
|
+
if (installed.wallet.mnemonic !== imported) {
|
|
389
|
+
// Lost the race to a different phrase. Adopting it silently is what
|
|
390
|
+
// createWalletAtomically does for a GENERATED wallet, where any valid
|
|
391
|
+
// phrase will do — but an operator who named one did not ask for another.
|
|
392
|
+
throw importConflict(path);
|
|
393
|
+
}
|
|
394
|
+
return { wallet: installed.wallet, created: false };
|
|
395
|
+
}
|
|
396
|
+
const existing = readWallet(path);
|
|
397
|
+
if (existing && isValidMnemonic(existing.mnemonic)) {
|
|
398
|
+
// A wallet is only as private as the file holding it, and this one may have
|
|
399
|
+
// arrived from anywhere.
|
|
400
|
+
return acceptExisting(existing, path);
|
|
401
|
+
}
|
|
402
|
+
// Two MCP processes starting at once both see no file and both generate a
|
|
403
|
+
// phrase. With a plain write both would save, each caching its own — and the
|
|
404
|
+
// loser's phrase can create a squad and hold assets that nothing on disk can
|
|
405
|
+
// recover, because the winner's file is what survives a restart. No error is
|
|
406
|
+
// raised anywhere; the account simply becomes unreachable.
|
|
407
|
+
//
|
|
408
|
+
// Write the content to a temp file first, then hard-link it into place.
|
|
409
|
+
// `link()` fails with EEXIST if the target exists, so exactly one process
|
|
410
|
+
// wins — and because the temp file is already complete, the file is valid the
|
|
411
|
+
// instant it appears. An `wx` open would also be atomic, but only for
|
|
412
|
+
// CREATION: the loser could then read the winner's still-empty file, call it
|
|
413
|
+
// corrupt, and overwrite it. Linking leaves no such window.
|
|
414
|
+
return createWalletAtomically(newMnemonic(), path);
|
|
415
|
+
}
|
|
416
|
+
/** The refusal shared by both ways an import can meet a wallet it did not name. */
|
|
417
|
+
function importConflict(path) {
|
|
418
|
+
return new Error(`A different wallet already exists at ${path}, and POG_MCP_MNEMONIC is not its phrase. ` +
|
|
419
|
+
'Refusing to replace it: that file is probably the only copy of a key that owns a ' +
|
|
420
|
+
'squad and on-chain assets, and nothing here can bring it back. Move or delete it if ' +
|
|
421
|
+
'you really mean to switch, or set POG_MCP_WALLET_FILE to another path to run this ' +
|
|
422
|
+
'phrase alongside it.');
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Write a file and make sure it is actually on the disk before returning.
|
|
426
|
+
*
|
|
427
|
+
* writeFileSync returns when the kernel has the data, not when the platter or
|
|
428
|
+
* the flash does. For most files that is the right trade; for the only copy of
|
|
429
|
+
* a key it is not.
|
|
430
|
+
*/
|
|
431
|
+
function writeFileSyncDurable(target, contents) {
|
|
432
|
+
const bytes = Buffer.from(contents, 'utf8');
|
|
433
|
+
const fd = openSync(target, 'wx', 0o600);
|
|
434
|
+
let complete = false;
|
|
435
|
+
try {
|
|
436
|
+
// writeSync reports how many bytes the kernel TOOK, which is not always how
|
|
437
|
+
// many we handed it. A full disk or an exhausted quota can accept the first
|
|
438
|
+
// half of the JSON and stop — and half a wallet file is an unreadable one,
|
|
439
|
+
// which the caller would otherwise fsync and link into place as if it were
|
|
440
|
+
// the real thing. Keep going until every byte is down.
|
|
441
|
+
let written = 0;
|
|
442
|
+
while (written < bytes.length) {
|
|
443
|
+
const n = writeSync(fd, bytes, written, bytes.length - written);
|
|
444
|
+
if (n <= 0) {
|
|
445
|
+
throw new Error(`Could not write ${target}: the filesystem accepted ${String(written)} of ` +
|
|
446
|
+
`${String(bytes.length)} bytes and then stopped. Check free space and quota. ` +
|
|
447
|
+
'No wallet was created, so nothing is lost — run this again once there is room.');
|
|
448
|
+
}
|
|
449
|
+
written += n;
|
|
450
|
+
}
|
|
451
|
+
fsyncSync(fd);
|
|
452
|
+
complete = true;
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
closeSync(fd);
|
|
456
|
+
if (!complete) {
|
|
457
|
+
// A partial file must never survive to be linked into place. We created
|
|
458
|
+
// this name exclusively, so removing it can only remove our own failure.
|
|
459
|
+
try {
|
|
460
|
+
unlinkSync(target);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Already gone. Nothing downstream depends on the removal succeeding.
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* fsync a directory, so a newly linked name survives a power cut too.
|
|
470
|
+
*
|
|
471
|
+
* Not portable: Windows cannot open a directory as a file, and some filesystems
|
|
472
|
+
* refuse the sync. Neither failure means the write is lost — the file itself is
|
|
473
|
+
* already synced — so this is best effort by design.
|
|
474
|
+
*/
|
|
475
|
+
function syncDirectory(dir) {
|
|
476
|
+
if (process.platform === 'win32')
|
|
477
|
+
return;
|
|
478
|
+
let fd;
|
|
479
|
+
try {
|
|
480
|
+
fd = openSync(dir, 'r');
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
fsyncSync(fd);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
// Some filesystems (and some containers) refuse this. The file is synced.
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
closeSync(fd);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Create the wallet file, or adopt whichever concurrent process created it
|
|
497
|
+
* first. Never overwrites an existing valid wallet.
|
|
498
|
+
*/
|
|
499
|
+
function createWalletAtomically(mnemonic, path) {
|
|
500
|
+
const wallet = {
|
|
501
|
+
mnemonic,
|
|
502
|
+
address: addressFromMnemonic(mnemonic),
|
|
503
|
+
createdAt: new Date().toISOString(),
|
|
504
|
+
};
|
|
505
|
+
const dir = dirname(path);
|
|
506
|
+
// 0700 for one we create; umask can only remove bits, never add them.
|
|
507
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
508
|
+
assertDirectoryIsSafe(dir);
|
|
509
|
+
// Unique per process AND per attempt, so two racers never share a temp name.
|
|
510
|
+
const temp = join(dir, `.wallet.${String(process.pid)}.${randomBytes(6).toString('hex')}.tmp`);
|
|
511
|
+
// Written THROUGH a descriptor and fsync'd before it is linked into place.
|
|
512
|
+
//
|
|
513
|
+
// writeFileSync returns once the data is in the page cache, not once it is on
|
|
514
|
+
// the disk. The link then publishes a name whose contents may not survive a
|
|
515
|
+
// power cut — and the caller can create a squad in that window, so after the
|
|
516
|
+
// reboot the account exists and the phrase that owns it does not. This is the
|
|
517
|
+
// one file where that trade is not worth making.
|
|
518
|
+
writeFileSyncDurable(temp, `${JSON.stringify(wallet, null, 2)}\n`);
|
|
519
|
+
try {
|
|
520
|
+
try {
|
|
521
|
+
linkSync(temp, path);
|
|
522
|
+
chmodSync(path, 0o600);
|
|
523
|
+
// And the DIRECTORY entry itself, so the name survives too. A synced file
|
|
524
|
+
// under an unsynced directory is a file with no way back to it.
|
|
525
|
+
syncDirectory(dir);
|
|
526
|
+
return { wallet, created: true };
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
if (err.code !== 'EEXIST')
|
|
530
|
+
throw err;
|
|
531
|
+
}
|
|
532
|
+
const winner = readWallet(path);
|
|
533
|
+
if (winner && isValidMnemonic(winner.mnemonic)) {
|
|
534
|
+
return acceptExisting(winner, path);
|
|
535
|
+
}
|
|
536
|
+
// The file exists but is unusable. REFUSE rather than replace it.
|
|
537
|
+
//
|
|
538
|
+
// Replacing looks helpful and is not. A truncated or hand-edited file may
|
|
539
|
+
// still contain the phrase in plain text, and overwriting destroys the only
|
|
540
|
+
// copy of an account that holds real assets — irreversibly, to save the user
|
|
541
|
+
// one . Automatic recovery is not worth that.
|
|
542
|
+
//
|
|
543
|
+
// Refusing also removes the last race here. Any unlink-then-retry scheme has
|
|
544
|
+
// a window where one process reads the corrupt file, a second replaces it
|
|
545
|
+
// with a valid one, and the first then unlinks a good file it never saw —
|
|
546
|
+
// putting the two processes back on different phrases, which is exactly the
|
|
547
|
+
// failure the atomic link exists to prevent.
|
|
548
|
+
throw new Error(`${path} exists but is not a readable wallet. It may still contain your ` +
|
|
549
|
+
'recovery phrase — open it before doing anything else. To start over, delete it. ' +
|
|
550
|
+
'POG_MCP_MNEMONIC will not help while the file is there: importing refuses to ' +
|
|
551
|
+
'overwrite it too, for the same reason.');
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
try {
|
|
555
|
+
unlinkSync(temp);
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// Already gone, or never created. Nothing depends on the temp surviving.
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
//# sourceMappingURL=wallet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet.js","sourceRoot":"","sources":["../src/wallet.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pog-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server that lets an AI agent play Proof of Goal — wallet, sign-in, squad building, and matches as typed tools.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://pog.soccer/agents",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mcp",
|
|
10
|
+
"model-context-protocol",
|
|
11
|
+
"agent",
|
|
12
|
+
"game",
|
|
13
|
+
"solana",
|
|
14
|
+
"football"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"skill/SKILL.md",
|
|
25
|
+
"skill/reference/measurements.md",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"bin": {
|
|
31
|
+
"pog-mcp": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"dev": "tsc --watch",
|
|
42
|
+
"type-check": "tsc --noEmit",
|
|
43
|
+
"lint": "tsc --noEmit",
|
|
44
|
+
"start": "node dist/index.js",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"prepublishOnly": "pnpm build"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
51
|
+
"@noble/curves": "^1.8.2",
|
|
52
|
+
"@noble/hashes": "^1.8.0",
|
|
53
|
+
"@scure/bip39": "^1.6.0",
|
|
54
|
+
"bs58": "^6.0.0",
|
|
55
|
+
"zod": "^3.24.2"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^22.15.30",
|
|
59
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
60
|
+
"typescript": "*",
|
|
61
|
+
"vitest": "^3.2.4"
|
|
62
|
+
}
|
|
63
|
+
}
|