@projectsolo/solo-mission-mcp 0.21.13 → 0.22.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.
Files changed (34) hide show
  1. package/DEVELOPER_README.md +1 -1
  2. package/README.md +96 -12
  3. package/dist/chunk-NUGGTARW.js +196 -0
  4. package/dist/{chunk-TKRT2V2W.js → chunk-OP4QYTL4.js} +5 -0
  5. package/dist/{client-LDWK5HLP.js → client-NS6J6IO2.js} +1 -1
  6. package/dist/deployment-UCUWVIPJ.js +43 -0
  7. package/dist/escrowErrors-4XSAYLGT.js +56 -0
  8. package/dist/index.js +244 -162
  9. package/dist/verify-7EUVRUAS.js +445 -0
  10. package/dist/{wallet-V4T4NTCM.js → wallet-LX7SI5RE.js} +28 -14
  11. package/dist/wire-RLDY4AJR.js +24 -0
  12. package/package.json +1 -1
  13. package/src/config.ts +11 -0
  14. package/src/index.ts +1 -1
  15. package/src/scripts/check-tools-against-spec.ts +3 -3
  16. package/src/solana/deployment.test.ts +61 -0
  17. package/src/solana/deployment.ts +89 -0
  18. package/src/solana/escrowErrors.ts +78 -0
  19. package/src/solana/fixtures/config-account.json +6 -0
  20. package/src/solana/fixtures/funding-transaction-v2-lottery.json +49 -0
  21. package/src/solana/fixtures/funding-transaction-v2-plain.json +46 -0
  22. package/src/solana/fixtures/solo_escrow.v2.idl-excerpt.json +683 -0
  23. package/src/solana/verify.test.ts +605 -78
  24. package/src/solana/verify.ts +545 -84
  25. package/src/solana/wallet.test.ts +85 -2
  26. package/src/solana/wallet.ts +74 -31
  27. package/src/solana/wire.test.ts +129 -0
  28. package/src/solana/wire.ts +264 -0
  29. package/src/tools/missions.ts +56 -119
  30. package/src/tools/solana.test.ts +356 -0
  31. package/src/tools/solana.ts +309 -70
  32. package/src/tools/tracks.ts +1 -1
  33. package/dist/verify-KAETIGV5.js +0 -136
  34. /package/src/solana/fixtures/{funding-transaction.json → funding-transaction-v1.json} +0 -0
@@ -0,0 +1,445 @@
1
+ import {
2
+ base58Decode,
3
+ base58Encode,
4
+ ed25519Verify,
5
+ isEmptySignature,
6
+ isSignerIndex,
7
+ isWritableIndex,
8
+ parseWireTransaction
9
+ } from "./chunk-NUGGTARW.js";
10
+
11
+ // src/solana/verify.ts
12
+ import { createHash } from "crypto";
13
+ var SUPPORTED_ESCROW_INTERFACE = "v2";
14
+ var DISCRIMINATORS = {
15
+ create_task: [194, 80, 6, 180, 232, 127, 48, 171],
16
+ cancel_task: [69, 228, 134, 187, 134, 105, 238, 48],
17
+ emergency_refund: [188, 73, 52, 195, 137, 70, 180, 147],
18
+ claim_refund: [15, 16, 30, 161, 255, 228, 97, 60],
19
+ /** The `Config` ACCOUNT discriminator (first 8 bytes of its data). */
20
+ Config: [155, 12, 170, 224, 30, 250, 204, 130]
21
+ };
22
+ var CREATE_TASK_V2_ACCOUNTS = [
23
+ "sponsor",
24
+ "config",
25
+ "whitelist",
26
+ "mint",
27
+ "task",
28
+ "vault",
29
+ "sponsor_token_account",
30
+ "token_program",
31
+ "system_program",
32
+ "operator",
33
+ "event_authority",
34
+ "program"
35
+ ];
36
+ var CREATE_TASK_V2_DATA_LENGTH = 8 + 8 + 8 + 4 + 8 + 8 + 8 + 32 + 2;
37
+ var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
38
+ var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
39
+ function decodeCreateTaskV2(data) {
40
+ if (!data.subarray(0, 8).equals(Buffer.from(DISCRIMINATORS.create_task))) {
41
+ throw new Error(
42
+ `instruction discriminator is ${data.subarray(0, 8).toString("hex")}, not create_task's ` + Buffer.from(DISCRIMINATORS.create_task).toString("hex")
43
+ );
44
+ }
45
+ if (data.length !== CREATE_TASK_V2_DATA_LENGTH) {
46
+ throw new Error(
47
+ `create_task data is ${data.length} bytes, expected ${CREATE_TASK_V2_DATA_LENGTH} for the v2 interface` + (data.length === CREATE_TASK_V2_DATA_LENGTH - 2 ? " (84 bytes is the v1 layout, without max_fee_bps)" : "")
48
+ );
49
+ }
50
+ return {
51
+ budget: data.readBigUInt64LE(8).toString(),
52
+ base_pool: data.readBigUInt64LE(16).toString(),
53
+ lottery_winner_count: data.readUInt32LE(24),
54
+ lottery_prize_per_winner: data.readBigUInt64LE(28).toString(),
55
+ qualify_deadline: data.readBigInt64LE(36).toString(),
56
+ settlement_deadline: data.readBigInt64LE(44).toString(),
57
+ seed_commit: data.subarray(52, 84).toString("hex"),
58
+ max_fee_bps: data.readUInt16LE(84)
59
+ };
60
+ }
61
+ function decodeEscrowConfig(data, address) {
62
+ if (data.length < 155) throw new Error(`Config account is ${data.length} bytes, too short`);
63
+ if (!data.subarray(0, 8).equals(Buffer.from(DISCRIMINATORS.Config))) {
64
+ throw new Error("account at the Config address does not carry the Config discriminator");
65
+ }
66
+ const key = (o) => base58Encode(data.subarray(o, o + 32));
67
+ return {
68
+ address,
69
+ admin: key(8),
70
+ operator: key(40),
71
+ guardian: key(72),
72
+ treasury: key(104),
73
+ fee_bps: data.readUInt16LE(136),
74
+ min_review_window: Number(data.readBigInt64LE(138)),
75
+ paused: data[146] !== 0,
76
+ task_counter: data.readBigUInt64LE(147).toString()
77
+ };
78
+ }
79
+ async function configAddress(programId) {
80
+ const { PublicKey } = await import("@solana/web3.js");
81
+ return PublicKey.findProgramAddressSync([Buffer.from("config")], new PublicKey(programId))[0].toBase58();
82
+ }
83
+ async function getAccountInfo(rpcUrl, address, { attempts = 3, backoffMs = 500 } = {}) {
84
+ let lastError = "";
85
+ for (let attempt = 1; attempt <= attempts; attempt++) {
86
+ try {
87
+ const res = await fetch(rpcUrl, {
88
+ method: "POST",
89
+ headers: { "Content-Type": "application/json" },
90
+ body: JSON.stringify({
91
+ jsonrpc: "2.0",
92
+ id: 1,
93
+ method: "getAccountInfo",
94
+ params: [address, { encoding: "base64", commitment: "confirmed" }]
95
+ }),
96
+ signal: AbortSignal.timeout(15e3)
97
+ });
98
+ if (res.status === 429 || res.status >= 500) {
99
+ lastError = `HTTP ${res.status}`;
100
+ } else {
101
+ const body = await res.json();
102
+ if (!body.error) return body.result?.value ?? null;
103
+ if (!/rate limit|too many/i.test(body.error.message)) {
104
+ throw new Error(`RPC getAccountInfo(${address}) failed: ${body.error.message}`);
105
+ }
106
+ lastError = body.error.message;
107
+ }
108
+ } catch (e) {
109
+ if (e.message.startsWith("RPC getAccountInfo")) throw e;
110
+ lastError = e.message;
111
+ }
112
+ if (attempt < attempts) await new Promise((r) => setTimeout(r, backoffMs * attempt));
113
+ }
114
+ throw new Error(`RPC getAccountInfo(${address}) failed: ${lastError}`);
115
+ }
116
+ async function readEscrowConfig(rpcUrl, programId, opts = {}) {
117
+ const address = await configAddress(programId);
118
+ const value = await getAccountInfo(rpcUrl, address, opts);
119
+ if (!value) throw new Error(`Config account ${address} does not exist on this cluster`);
120
+ if (value.owner !== programId) {
121
+ throw new Error(`Config account ${address} is owned by ${value.owner}, not the program ${programId}`);
122
+ }
123
+ return decodeEscrowConfig(Buffer.from(value.data[0], "base64"), address);
124
+ }
125
+ async function readMintDecimals(rpcUrl, mint, opts = {}) {
126
+ const value = await getAccountInfo(rpcUrl, mint, opts);
127
+ if (!value) throw new Error(`mint ${mint} does not exist on this cluster`);
128
+ if (value.owner !== TOKEN_PROGRAM_ID) {
129
+ throw new Error(`mint ${mint} is owned by ${value.owner}, not the SPL Token program`);
130
+ }
131
+ const data = Buffer.from(value.data[0], "base64");
132
+ if (data.length !== 82 || data[45] !== 1) {
133
+ throw new Error(`account ${mint} is not an initialized SPL Token mint`);
134
+ }
135
+ return data[44];
136
+ }
137
+ var eqAmount = (a, b) => {
138
+ try {
139
+ return BigInt(a) === BigInt(b);
140
+ } catch {
141
+ return false;
142
+ }
143
+ };
144
+ var hex = (s) => String(s ?? "").replace(/^0x/i, "").toLowerCase();
145
+ async function verifyFundingTransaction(input) {
146
+ const { PublicKey } = await import("@solana/web3.js");
147
+ const problems = [];
148
+ const warnings = [];
149
+ const e = input.expected;
150
+ const d = input.declared;
151
+ const expectLottery = e.lottery_winner_count > 0;
152
+ const summary = {
153
+ escrow_interface: input.escrow_interface,
154
+ program_id: "(none)",
155
+ instruction_count: 0,
156
+ signers: [],
157
+ fee_payer: null,
158
+ budget: d.budget,
159
+ mint: input.accounts.mint,
160
+ task: input.accounts.task,
161
+ lottery: expectLottery,
162
+ max_fee_bps: null,
163
+ operator: null,
164
+ operator_signature: expectLottery ? "missing" : "not_required",
165
+ message_sha256: null
166
+ };
167
+ const done = () => ({ ok: problems.length === 0, problems, warnings, summary });
168
+ if (input.escrow_interface !== SUPPORTED_ESCROW_INTERFACE) {
169
+ problems.push(
170
+ `the funding response reports escrow_interface ${JSON.stringify(input.escrow_interface)}; this verifier decodes only the ${SUPPORTED_ESCROW_INTERFACE} create_task layout`
171
+ );
172
+ }
173
+ let tx;
174
+ try {
175
+ tx = parseWireTransaction(Buffer.from(input.transaction_base64, "base64"));
176
+ } catch (err) {
177
+ problems.push(`the transaction does not decode: ${err.message}`);
178
+ return done();
179
+ }
180
+ summary.instruction_count = tx.instructions.length;
181
+ summary.signers = tx.accountKeys.slice(0, tx.header.numRequiredSignatures);
182
+ summary.fee_payer = tx.accountKeys[0] ?? null;
183
+ summary.message_sha256 = createHash("sha256").update(tx.message).digest("hex");
184
+ if (tx.instructions.length !== 1) {
185
+ problems.push(
186
+ `expected exactly 1 instruction, found ${tx.instructions.length} \u2014 additional instructions would be authorised by the same signature`
187
+ );
188
+ }
189
+ const ix = tx.instructions[0];
190
+ const programId = ix ? tx.accountKeys[ix.programIdIndex] : "(none)";
191
+ summary.program_id = programId;
192
+ if (programId !== input.expected_program_id) {
193
+ problems.push(`program is ${programId}, expected ${input.expected_program_id}`);
194
+ }
195
+ if (programId !== input.accounts.program_id) {
196
+ problems.push(
197
+ `transaction program ${programId} disagrees with the quoted accounts.program_id ${input.accounts.program_id}`
198
+ );
199
+ }
200
+ if (input.message_sha256 !== void 0 && input.message_sha256.toLowerCase() !== summary.message_sha256) {
201
+ problems.push(
202
+ `the response's message_sha256 ${input.message_sha256} is not the hash of this transaction's message (${summary.message_sha256}) \u2014 confirm-funding would not recognise it`
203
+ );
204
+ }
205
+ let encoded = null;
206
+ try {
207
+ encoded = decodeCreateTaskV2(ix?.data ?? Buffer.alloc(0));
208
+ summary.max_fee_bps = encoded.max_fee_bps;
209
+ summary.lottery = encoded.lottery_winner_count > 0;
210
+ } catch (err) {
211
+ problems.push(`${err.message} \u2014 this is not the instruction it claims to be`);
212
+ }
213
+ const program = new PublicKey(input.expected_program_id);
214
+ const pda = (...seeds) => PublicKey.findProgramAddressSync(seeds, program)[0].toBase58();
215
+ let taskId = null;
216
+ try {
217
+ taskId = BigInt(input.task_id);
218
+ if (taskId < 0n || taskId > 0xffffffffffffffffn) throw new Error("out of range");
219
+ } catch {
220
+ problems.push(`task_id ${JSON.stringify(input.task_id)} is not a u64`);
221
+ taskId = null;
222
+ }
223
+ const u64le = (v) => {
224
+ const b = Buffer.alloc(8);
225
+ b.writeBigUInt64LE(v);
226
+ return b;
227
+ };
228
+ let mintKey = null;
229
+ try {
230
+ mintKey = base58Decode(e.mint);
231
+ if (mintKey.length !== 32) mintKey = null;
232
+ } catch {
233
+ mintKey = null;
234
+ }
235
+ if (!mintKey) problems.push(`expected mint ${e.mint} is not a valid address`);
236
+ const operatorSlot = CREATE_TASK_V2_ACCOUNTS.indexOf("operator");
237
+ const want = {
238
+ sponsor: e.sponsor,
239
+ config: pda(Buffer.from("config")),
240
+ whitelist: mintKey ? pda(Buffer.from("whitelist"), mintKey) : null,
241
+ mint: e.mint,
242
+ task: taskId !== null ? pda(Buffer.from("task"), u64le(taskId)) : null,
243
+ vault: taskId !== null ? pda(Buffer.from("vault"), u64le(taskId)) : null,
244
+ sponsor_token_account: e.sponsor_token_account,
245
+ token_program: TOKEN_PROGRAM_ID,
246
+ system_program: SYSTEM_PROGRAM_ID,
247
+ operator: null,
248
+ // checked below: depends on lottery vs not
249
+ event_authority: pda(Buffer.from("__event_authority")),
250
+ program: input.expected_program_id
251
+ };
252
+ const ixKeys = (ix?.accountIndexes ?? []).map((i) => tx.accountKeys[i]);
253
+ if (ix && ixKeys.length !== CREATE_TASK_V2_ACCOUNTS.length) {
254
+ problems.push(
255
+ `create_task lists ${ixKeys.length} accounts, expected ${CREATE_TASK_V2_ACCOUNTS.length} (${CREATE_TASK_V2_ACCOUNTS.join(", ")})`
256
+ );
257
+ } else if (ix) {
258
+ CREATE_TASK_V2_ACCOUNTS.forEach((name, i) => {
259
+ const expectedKey = want[name];
260
+ if (expectedKey !== null && ixKeys[i] !== expectedKey) {
261
+ problems.push(`account #${i} (${name}) is ${ixKeys[i]}, expected ${expectedKey}`);
262
+ }
263
+ });
264
+ if (!isSignerIndex(tx, ix.accountIndexes[0]) || !isWritableIndex(tx, ix.accountIndexes[0])) {
265
+ problems.push("the sponsor account is not a writable signer");
266
+ }
267
+ }
268
+ for (const [label, quoted, derived] of [
269
+ ["task", input.accounts.task, want.task],
270
+ ["vault", input.accounts.vault, want.vault],
271
+ ["config", input.accounts.config, want.config],
272
+ ["whitelist", input.accounts.whitelist, want.whitelist]
273
+ ]) {
274
+ if (derived !== null && quoted !== derived) {
275
+ problems.push(
276
+ `quoted ${label} ${quoted} is not the program-derived address ${derived} \u2014 an address that is not the PDA may be a wallet somebody holds the key to`
277
+ );
278
+ }
279
+ }
280
+ if (input.accounts.mint !== e.mint) {
281
+ problems.push(`quoted mint ${input.accounts.mint}, expected ${e.mint}`);
282
+ }
283
+ if (input.accounts.sponsor !== e.sponsor) {
284
+ problems.push(`quoted sponsor ${input.accounts.sponsor}, expected ${e.sponsor}`);
285
+ }
286
+ if (input.accounts.sponsor_token_account !== e.sponsor_token_account) {
287
+ problems.push(
288
+ `quoted sponsor_token_account ${input.accounts.sponsor_token_account}, expected ${e.sponsor_token_account}`
289
+ );
290
+ }
291
+ if (input.accounts.vault === input.accounts.sponsor_token_account) {
292
+ problems.push("escrow vault equals the sponsor token account \u2014 the budget would not be escrowed");
293
+ }
294
+ const feePayer = tx.accountKeys[0];
295
+ if (feePayer !== e.sponsor) {
296
+ const who = input.onchain_config && feePayer === input.onchain_config.operator ? " (the Operator)" : "";
297
+ problems.push(`fee payer is ${feePayer ?? "(unset)"}${who}, expected the sponsor ${e.sponsor}`);
298
+ }
299
+ const operatorKey = ix && ixKeys.length === CREATE_TASK_V2_ACCOUNTS.length ? ixKeys[operatorSlot] : void 0;
300
+ const operatorIndex = ix?.accountIndexes[operatorSlot];
301
+ const signers = summary.signers;
302
+ if (d.operator_cosigned !== void 0 && d.operator_cosigned !== expectLottery) {
303
+ problems.push(
304
+ `the response says operator_cosigned=${d.operator_cosigned}, but this is ` + (expectLottery ? "a lottery task, which must be co-signed" : "not a lottery task, which must not be")
305
+ );
306
+ }
307
+ if (!expectLottery) {
308
+ if (operatorKey !== void 0 && operatorKey !== input.expected_program_id) {
309
+ problems.push(
310
+ `the operator slot holds ${operatorKey}; a non-lottery create_task must pass the program id ${input.expected_program_id} (no co-signer)`
311
+ );
312
+ }
313
+ if (signers.length !== 1 || signers[0] !== e.sponsor) {
314
+ problems.push(
315
+ `expected the sponsor ${e.sponsor} to be the only signer, found [${signers.join(", ")}]`
316
+ );
317
+ }
318
+ } else {
319
+ summary.operator = operatorKey ?? null;
320
+ const cfg2 = input.onchain_config;
321
+ if (!cfg2) {
322
+ problems.push(
323
+ "cannot check the lottery co-signer: the on-chain Config.operator could not be read" + (input.onchain_config_error ? ` (${input.onchain_config_error})` : "")
324
+ );
325
+ } else if (operatorKey !== cfg2.operator) {
326
+ problems.push(
327
+ `the co-signer in the operator slot is ${operatorKey ?? "(none)"}, but the on-chain Config.operator is ${cfg2.operator}`
328
+ );
329
+ }
330
+ if (operatorKey !== void 0 && operatorKey === e.sponsor) {
331
+ problems.push("the operator slot holds the sponsor itself \u2014 the co-signature would be the agent's own");
332
+ }
333
+ const expectedSigners = operatorKey ? [e.sponsor, operatorKey] : [e.sponsor];
334
+ if (signers.length !== 2 || signers[0] !== expectedSigners[0] || signers[1] !== expectedSigners[1]) {
335
+ problems.push(
336
+ `expected exactly two signers, the sponsor ${e.sponsor} (fee payer) and the Operator ${operatorKey ?? "(none)"}, found [${signers.join(", ")}]`
337
+ );
338
+ }
339
+ if (operatorIndex === void 0 || !isSignerIndex(tx, operatorIndex)) {
340
+ problems.push("the operator slot is not a signer \u2014 a lottery create_task needs the Operator to sign");
341
+ summary.operator_signature = "missing";
342
+ } else {
343
+ const sig = tx.signatures[operatorIndex];
344
+ if (isEmptySignature(sig)) {
345
+ summary.operator_signature = "missing";
346
+ problems.push(
347
+ "the Operator co-signature is missing \u2014 a lottery create_task without it fails on chain (LotteryRequiresOperator), and an agent-added signature cannot fix that"
348
+ );
349
+ } else if (!ed25519Verify(sig, tx.message, operatorKey)) {
350
+ summary.operator_signature = "invalid";
351
+ problems.push(
352
+ "the Operator signature does not verify over this message \u2014 the transaction was altered after it was co-signed, or the signature is not the Operator's"
353
+ );
354
+ } else {
355
+ summary.operator_signature = "valid";
356
+ }
357
+ }
358
+ }
359
+ if (!eqAmount(d.budget, e.budget_raw)) {
360
+ problems.push(`budget is ${d.budget}, expected ${e.budget_raw}`);
361
+ }
362
+ if (!eqAmount(d.base_pool, e.base_pool_raw)) {
363
+ problems.push(`base_pool is ${d.base_pool}, expected ${e.base_pool_raw}`);
364
+ }
365
+ if (Number(d.lottery_winner_count) !== e.lottery_winner_count) {
366
+ problems.push(
367
+ `lottery_winner_count is ${d.lottery_winner_count}, expected ${e.lottery_winner_count}`
368
+ );
369
+ }
370
+ if (!eqAmount(d.lottery_prize_per_winner, e.lottery_prize_per_winner_raw)) {
371
+ problems.push(
372
+ `lottery_prize_per_winner is ${d.lottery_prize_per_winner}, expected ${e.lottery_prize_per_winner_raw}`
373
+ );
374
+ }
375
+ if (!eqAmount(d.qualify_deadline, String(e.qualify_deadline))) {
376
+ problems.push(`qualify_deadline is ${d.qualify_deadline}, expected ${e.qualify_deadline}`);
377
+ }
378
+ if (!eqAmount(d.settlement_deadline, String(e.settlement_deadline))) {
379
+ problems.push(
380
+ `settlement_deadline is ${d.settlement_deadline}, expected ${e.settlement_deadline}`
381
+ );
382
+ }
383
+ if (!/^[0-9a-f]{64}$/i.test(d.seed_commit)) {
384
+ problems.push(`seed_commit is not 32 bytes of hex: ${d.seed_commit}`);
385
+ } else if (e.seed_commit !== void 0 && hex(d.seed_commit) !== hex(e.seed_commit)) {
386
+ problems.push(`seed_commit is ${d.seed_commit}, expected ${hex(e.seed_commit)}`);
387
+ }
388
+ if (typeof e.max_fee_bps !== "number" || !Number.isInteger(e.max_fee_bps)) {
389
+ problems.push("no quoted fee rate to check max_fee_bps against \u2014 refusing to sign an unchecked fee ceiling");
390
+ } else if (d.max_fee_bps === void 0) {
391
+ problems.push("the funding response does not declare max_fee_bps \u2014 not a v2 funding transaction");
392
+ } else if (Number(d.max_fee_bps) !== e.max_fee_bps) {
393
+ problems.push(`max_fee_bps is ${d.max_fee_bps}, expected the quoted fee rate ${e.max_fee_bps}`);
394
+ }
395
+ if (encoded) {
396
+ for (const key of Object.keys(encoded)) {
397
+ const inBytes = String(encoded[key]);
398
+ const quotedRaw = d[key];
399
+ if (quotedRaw === void 0) continue;
400
+ const quoted = String(quotedRaw);
401
+ const same = key === "seed_commit" ? inBytes === hex(quoted) : key === "lottery_winner_count" || key === "max_fee_bps" ? Number(inBytes) === Number(quoted) : eqAmount(inBytes, quoted);
402
+ if (!same) {
403
+ problems.push(
404
+ `the transaction encodes ${key}=${inBytes} but the response quoted ${quoted} \u2014 the backend described one thing and built another`
405
+ );
406
+ }
407
+ }
408
+ if (typeof e.max_fee_bps === "number" && encoded.max_fee_bps !== e.max_fee_bps) {
409
+ problems.push(
410
+ `the transaction encodes max_fee_bps=${encoded.max_fee_bps}, not the quoted fee rate ${e.max_fee_bps}`
411
+ );
412
+ }
413
+ if (encoded.lottery_winner_count > 0 !== expectLottery) {
414
+ problems.push(
415
+ `the transaction encodes lottery_winner_count=${encoded.lottery_winner_count}, expected ${e.lottery_winner_count}`
416
+ );
417
+ }
418
+ }
419
+ const cfg = input.onchain_config;
420
+ if (cfg && encoded) {
421
+ if (cfg.fee_bps > encoded.max_fee_bps) {
422
+ warnings.push(
423
+ `the program's fee is now ${cfg.fee_bps} bps, above this transaction's max_fee_bps ${encoded.max_fee_bps}: create_task will fail with FeeAboveSponsorMax (6132). Cancel the mission and create a new one to get a fresh quote.`
424
+ );
425
+ }
426
+ if (cfg.paused) {
427
+ warnings.push("the escrow program is paused: create_task will fail until it is unpaused.");
428
+ }
429
+ }
430
+ return done();
431
+ }
432
+ export {
433
+ CREATE_TASK_V2_ACCOUNTS,
434
+ CREATE_TASK_V2_DATA_LENGTH,
435
+ DISCRIMINATORS,
436
+ SUPPORTED_ESCROW_INTERFACE,
437
+ SYSTEM_PROGRAM_ID,
438
+ TOKEN_PROGRAM_ID,
439
+ configAddress,
440
+ decodeCreateTaskV2,
441
+ decodeEscrowConfig,
442
+ readEscrowConfig,
443
+ readMintDecimals,
444
+ verifyFundingTransaction
445
+ };
@@ -1,6 +1,8 @@
1
1
  // src/solana/wallet.ts
2
2
  import { readFileSync } from "fs";
3
3
  import { createDecipheriv, pbkdf2Sync } from "crypto";
4
+ var KEY_STORAGE_GUIDANCE = "Keep the keyfile and the passphrase in your secret manager and have it write the files when the MCP server starts: for example 1Password (`op inject`, `op read --out-file`), a HashiCorp Vault Agent template, GCP Secret Manager (`gcloud secrets versions access`), or the macOS Keychain (`security find-generic-password -w`). Never paste the key or the passphrase into chat, a command line or shell history.";
5
+ var REMOVED_INLINE_KEY_VAR = "SOLO_SOLANA_KEYPAIR";
4
6
  var SolanaWalletUnavailable = class extends Error {
5
7
  constructor(reason) {
6
8
  super(
@@ -8,10 +10,13 @@ var SolanaWalletUnavailable = class extends Error {
8
10
 
9
11
  Set one of:
10
12
  SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH + SOLO_SOLANA_KEYPAIR_PASSWORD_FILE
11
- - recommended: an openssl-aes-256-cbc-pbkdf2-encrypted
12
- keyfile plus a separate passphrase file (chmod 600 both)
13
- SOLO_SOLANA_KEYPAIR - JSON byte array, as \`solana-keygen\` writes it (plaintext)
14
- SOLO_SOLANA_KEYPAIR_PATH - path to that file (e.g. ~/.config/solana/id.json), plaintext
13
+ - recommended: a keyfile encrypted with
14
+ \`openssl enc -aes-256-cbc -pbkdf2 -iter 10000 -md sha256\`
15
+ plus a separate passphrase file (chmod 600 both)
16
+ SOLO_SOLANA_KEYPAIR_PATH - path to a plaintext \`solana-keygen\` keyfile
17
+ (e.g. ~/.config/solana/id.json, chmod 600)
18
+
19
+ ${KEY_STORAGE_GUIDANCE}
15
20
 
16
21
  The wallet needs SOL for rent and fees, and USDC for the mission budget. Rent is a
17
22
  refundable deposit, not a fee: most of it returns when the task is closed.`
@@ -19,6 +24,13 @@ refundable deposit, not a fee: most of it returns when the task is closed.`
19
24
  this.name = "SolanaWalletUnavailable";
20
25
  }
21
26
  };
27
+ function assertNoInlineKeyVar() {
28
+ if ((process.env[REMOVED_INLINE_KEY_VAR] ?? "").trim() !== "") {
29
+ throw new SolanaWalletUnavailable(
30
+ `${REMOVED_INLINE_KEY_VAR} (the raw key in an environment variable) is no longer supported. Unset it, and move the key into a file referenced by SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH + SOLO_SOLANA_KEYPAIR_PASSWORD_FILE (recommended) or SOLO_SOLANA_KEYPAIR_PATH.`
31
+ );
32
+ }
33
+ }
22
34
  function decryptOpenSslAes256CbcPbkdf2(encrypted, password) {
23
35
  const MAGIC = "Salted__";
24
36
  if (encrypted.length < 16 || encrypted.subarray(0, 8).toString("utf8") !== MAGIC) {
@@ -68,9 +80,9 @@ function expandHome(path) {
68
80
  var cachedWallet;
69
81
  async function loadSolanaWallet() {
70
82
  if (cachedWallet) return cachedWallet;
83
+ assertNoInlineKeyVar();
71
84
  const encryptedPath = process.env.SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH;
72
85
  const passwordFile = process.env.SOLO_SOLANA_KEYPAIR_PASSWORD_FILE;
73
- const inline = process.env.SOLO_SOLANA_KEYPAIR;
74
86
  const path = process.env.SOLO_SOLANA_KEYPAIR_PATH;
75
87
  let raw;
76
88
  if (encryptedPath && encryptedPath.trim() !== "" || passwordFile && passwordFile.trim() !== "") {
@@ -92,8 +104,6 @@ async function loadSolanaWallet() {
92
104
  throw new SolanaWalletUnavailable(`cannot read ${passwordFile}: ${e.message}`);
93
105
  }
94
106
  raw = decryptOpenSslAes256CbcPbkdf2(encrypted, password).toString("utf8");
95
- } else if (inline && inline.trim() !== "") {
96
- raw = inline;
97
107
  } else if (path && path.trim() !== "") {
98
108
  try {
99
109
  raw = readFileSync(expandHome(path), "utf8");
@@ -102,7 +112,7 @@ async function loadSolanaWallet() {
102
112
  }
103
113
  } else {
104
114
  throw new SolanaWalletUnavailable(
105
- "no wallet source configured (checked SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH, SOLO_SOLANA_KEYPAIR, SOLO_SOLANA_KEYPAIR_PATH)"
115
+ "no wallet source configured (checked SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH, SOLO_SOLANA_KEYPAIR_PATH)"
106
116
  );
107
117
  }
108
118
  const secretKey = parseKeypairBytes(raw);
@@ -113,7 +123,7 @@ async function loadSolanaWallet() {
113
123
  }
114
124
  function hasSolanaWallet() {
115
125
  return Boolean(
116
- cachedWallet || (process.env.SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH ?? "").trim() && (process.env.SOLO_SOLANA_KEYPAIR_PASSWORD_FILE ?? "").trim() || (process.env.SOLO_SOLANA_KEYPAIR ?? "").trim() || (process.env.SOLO_SOLANA_KEYPAIR_PATH ?? "").trim()
126
+ cachedWallet || (process.env[REMOVED_INLINE_KEY_VAR] ?? "").trim() || (process.env.SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH ?? "").trim() && (process.env.SOLO_SOLANA_KEYPAIR_PASSWORD_FILE ?? "").trim() || (process.env.SOLO_SOLANA_KEYPAIR_PATH ?? "").trim()
117
127
  );
118
128
  }
119
129
  async function associatedTokenAddress(mint, owner) {
@@ -127,14 +137,18 @@ async function associatedTokenAddress(mint, owner) {
127
137
  return address.toBase58();
128
138
  }
129
139
  async function signTransaction(transactionBase64, wallet) {
130
- const { Keypair, Transaction } = await import("@solana/web3.js");
131
- const kp = Keypair.fromSecretKey(wallet.secretKey);
132
- const tx = Transaction.from(Buffer.from(transactionBase64, "base64"));
133
- tx.partialSign(kp);
134
- return tx.serialize().toString("base64");
140
+ const { addOwnSignature } = await import("./wire-RLDY4AJR.js");
141
+ return addOwnSignature(
142
+ Buffer.from(transactionBase64, "base64"),
143
+ wallet.secretKey,
144
+ wallet.publicKey
145
+ ).toString("base64");
135
146
  }
136
147
  export {
148
+ KEY_STORAGE_GUIDANCE,
149
+ REMOVED_INLINE_KEY_VAR,
137
150
  SolanaWalletUnavailable,
151
+ assertNoInlineKeyVar,
138
152
  associatedTokenAddress,
139
153
  decryptOpenSslAes256CbcPbkdf2,
140
154
  hasSolanaWallet,
@@ -0,0 +1,24 @@
1
+ import {
2
+ addOwnSignature,
3
+ base58Decode,
4
+ base58Encode,
5
+ ed25519Sign,
6
+ ed25519Verify,
7
+ isEmptySignature,
8
+ isSignerIndex,
9
+ isWritableIndex,
10
+ parseWireTransaction,
11
+ publicKeyOfSecret
12
+ } from "./chunk-NUGGTARW.js";
13
+ export {
14
+ addOwnSignature,
15
+ base58Decode,
16
+ base58Encode,
17
+ ed25519Sign,
18
+ ed25519Verify,
19
+ isEmptySignature,
20
+ isSignerIndex,
21
+ isWritableIndex,
22
+ parseWireTransaction,
23
+ publicKeyOfSecret
24
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectsolo/solo-mission-mcp",
3
- "version": "0.21.13",
3
+ "version": "0.22.0",
4
4
  "description": "MCP server for Solo Mission Platform — lets AI agents create missions, browse humans, and chat.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/config.ts CHANGED
@@ -8,3 +8,14 @@ export const config = {
8
8
  if (!config.agentKey) {
9
9
  console.warn('Warning: SOLO_AGENT_KEY is not set. Only register_agent will work until a key is configured.');
10
10
  }
11
+
12
+ // Solana tools load the wallet lazily and fail there with the full explanation; this startup line
13
+ // makes sure an operator running only off-chain tools still learns the raw key is sitting in env.
14
+ if ((process.env.SOLO_SOLANA_KEYPAIR ?? '').trim() !== '') {
15
+ console.warn(
16
+ 'Warning: SOLO_SOLANA_KEYPAIR (the raw key in an environment variable) is no longer supported, ' +
17
+ 'and Solana tools will refuse to run while it is set. Unset it and use ' +
18
+ 'SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH + SOLO_SOLANA_KEYPAIR_PASSWORD_FILE (recommended) or ' +
19
+ 'SOLO_SOLANA_KEYPAIR_PATH.',
20
+ );
21
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Solo Mission MCP Server
4
- * Exposes 15 tools for AI agents to interact with the Solo Mission Platform.
4
+ * Exposes MCP tools for AI agents to interact with the Solo Mission Platform.
5
5
  */
6
6
 
7
7
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -132,9 +132,9 @@ try {
132
132
  * The check's original premise, "every route the spec documents must be called by something here",
133
133
  * held only while the spec happened to annotate agent routes exclusively. It broke the moment
134
134
  * solo-firebase documented `GET /human/solana/rewards` and `POST /profile/wallet/solana/bind` —
135
- * correctly, since a published API should describe its human surface too. The Base equivalents
136
- * (`/human/rewards`, `/profile/wallet/bind`) are simply not annotated yet, which is why this never
137
- * fired before rather than the rule ever having been right.
135
+ * correctly, since a published API should describe its human surface too. The older non-Solana
136
+ * routes (`/human/rewards`, `/profile/wallet/bind`) are simply not annotated yet, which is why this
137
+ * never fired before rather than the rule ever having been right.
138
138
  *
139
139
  * Scoped by path prefix rather than an allowlist of specific routes, so the next human endpoint
140
140
  * does not fail a release for the same reason.