@scriptmasterlabs/mcp-x402 2.0.2 → 2.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.
Files changed (93) hide show
  1. package/.well-known/agentcard.json +34 -34
  2. package/.well-known/ai.txt +32 -0
  3. package/CONTRIBUTING.md +76 -76
  4. package/LICENSE +21 -21
  5. package/README.md +304 -304
  6. package/agents.json +81 -67
  7. package/ai/faq.json +74 -0
  8. package/ai/summary.json +157 -0
  9. package/dist/lib/chains/base.d.ts.map +1 -1
  10. package/dist/lib/chains/base.js +2 -0
  11. package/dist/lib/chains/base.js.map +1 -1
  12. package/dist/lib/credit/bureau.d.ts +7 -1
  13. package/dist/lib/credit/bureau.d.ts.map +1 -1
  14. package/dist/lib/credit/bureau.js +40 -10
  15. package/dist/lib/credit/bureau.js.map +1 -1
  16. package/dist/server/index.js +128 -5
  17. package/dist/server/index.js.map +1 -1
  18. package/llms.txt +170 -70
  19. package/package.json +78 -78
  20. package/server.json +52 -48
  21. package/.env.example +0 -35
  22. package/.github/workflows/ci.yml +0 -59
  23. package/.github/workflows/keepalive.yml +0 -31
  24. package/Dockerfile +0 -19
  25. package/docker-compose.yml +0 -50
  26. package/mcp-publisher.exe +0 -0
  27. package/render.yaml +0 -39
  28. package/sdk/mcp-x402-sdk/package.json +0 -18
  29. package/sdk/mcp-x402-sdk/src/index.ts +0 -118
  30. package/sdk/mcp-x402-sdk/tsconfig.json +0 -14
  31. package/services/backtest_service.py +0 -176
  32. package/src/lib/chains/base.ts +0 -77
  33. package/src/lib/chains/solana.ts +0 -59
  34. package/src/lib/chains/xrpl.ts +0 -63
  35. package/src/lib/credit/bureau.ts +0 -65
  36. package/src/lib/sml-api/agentcard.ts +0 -40
  37. package/src/lib/sml-api/backtest.ts +0 -47
  38. package/src/lib/sml-api/brokers.ts +0 -160
  39. package/src/lib/sml-api/copytrader.ts +0 -33
  40. package/src/lib/sml-api/crawl.ts +0 -44
  41. package/src/lib/sml-api/echo.ts +0 -28
  42. package/src/lib/sml-api/forge.ts +0 -33
  43. package/src/lib/sml-api/ftd.ts +0 -53
  44. package/src/lib/sml-api/ghost.ts +0 -35
  45. package/src/lib/sml-api/launchpad.ts +0 -43
  46. package/src/lib/sml-api/leviathan.ts +0 -49
  47. package/src/lib/sml-api/nexus.ts +0 -50
  48. package/src/lib/sml-api/proof402.ts +0 -27
  49. package/src/lib/sml-api/rails.ts +0 -34
  50. package/src/lib/sml-api/shadow.ts +0 -35
  51. package/src/lib/sml-api/squeezeos.ts +0 -95
  52. package/src/lib/sml-api/xdeo.ts +0 -40
  53. package/src/lib/sml-api/xmit.ts +0 -40
  54. package/src/server/health.ts +0 -52
  55. package/src/server/index.ts +0 -213
  56. package/src/server/payments/ap2.ts +0 -101
  57. package/src/server/payments/receipt.ts +0 -85
  58. package/src/server/payments/router.ts +0 -110
  59. package/src/server/payments/wallet.ts +0 -123
  60. package/src/server/payments/x402.ts +0 -177
  61. package/src/server/registry/catalog.ts +0 -61
  62. package/src/server/registry/discovery.ts +0 -39
  63. package/src/server/registry/pricing.ts +0 -133
  64. package/src/server/security/acl.ts +0 -42
  65. package/src/server/security/audit.ts +0 -94
  66. package/src/server/security/rate-limit.ts +0 -84
  67. package/src/server/security/sandbox.ts +0 -40
  68. package/src/server/tools/agentcard.ts +0 -134
  69. package/src/server/tools/backtest.ts +0 -119
  70. package/src/server/tools/brokers.ts +0 -250
  71. package/src/server/tools/copytrader.ts +0 -104
  72. package/src/server/tools/crawl.ts +0 -70
  73. package/src/server/tools/discovery.ts +0 -202
  74. package/src/server/tools/echo.ts +0 -58
  75. package/src/server/tools/forge.ts +0 -87
  76. package/src/server/tools/ftd.ts +0 -88
  77. package/src/server/tools/ghost.ts +0 -93
  78. package/src/server/tools/index.ts +0 -42
  79. package/src/server/tools/launchpad.ts +0 -173
  80. package/src/server/tools/leviathan.ts +0 -81
  81. package/src/server/tools/nexus.ts +0 -76
  82. package/src/server/tools/proof402.ts +0 -87
  83. package/src/server/tools/rails.ts +0 -92
  84. package/src/server/tools/shadow.ts +0 -128
  85. package/src/server/tools/squeezeos.ts +0 -312
  86. package/src/server/tools/xdeo.ts +0 -67
  87. package/src/server/tools/xmit.ts +0 -68
  88. package/tests/integration/e2e.test.ts +0 -51
  89. package/tests/unit/payments.test.ts +0 -49
  90. package/tests/unit/security.test.ts +0 -92
  91. package/tests/unit/tools.test.ts +0 -42
  92. package/tsconfig.json +0 -21
  93. package/vitest.config.ts +0 -20
@@ -1,213 +0,0 @@
1
- #!/usr/bin/env node
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
5
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
- import express from 'express';
7
- import { randomUUID } from 'crypto';
8
- import cors from 'cors';
9
- import { registerTools } from './tools/index.js';
10
- import { AuditLogger } from './security/audit.js';
11
- import { RateLimiter } from './security/rate-limit.js';
12
- import { healthHandler } from './health.js';
13
-
14
- const VERSION = '1.0.0';
15
-
16
- async function createServer(): Promise<McpServer> {
17
- const server = new McpServer(
18
- { name: 'mcp-x402', version: VERSION },
19
- { capabilities: { tools: {} } },
20
- );
21
- await registerTools(server);
22
- return server;
23
- }
24
-
25
- async function runStdio(): Promise<void> {
26
- const server = await createServer();
27
- const transport = new StdioServerTransport();
28
- await server.connect(transport);
29
- AuditLogger.getInstance().info('server_start', { transport: 'stdio', version: VERSION });
30
-
31
- const shutdown = async () => {
32
- AuditLogger.getInstance().info('server_stop', { transport: 'stdio' });
33
- await server.close();
34
- process.exit(0);
35
- };
36
- process.on('SIGINT', shutdown);
37
- process.on('SIGTERM', shutdown);
38
-
39
- // Keep stdio process alive
40
- process.stdin.on('end', () => {
41
- AuditLogger.getInstance().warn('stdio_stdin_end', {});
42
- process.exit(0);
43
- });
44
- }
45
-
46
- async function runSSE(): Promise<void> {
47
- const app = express();
48
- const port = parseInt(process.env['MCP_SSE_PORT'] ?? '3402', 10);
49
-
50
- app.use(cors({ origin: process.env['CORS_ORIGIN'] ?? '*' }));
51
- app.use(express.json({ limit: '1mb' }));
52
-
53
- // Health endpoint
54
- app.get('/health', healthHandler);
55
-
56
- // Wallet info — shows the server's derived wallet address (safe to expose, no private key)
57
- app.get('/wallet', async (_req, res) => {
58
- const { WalletManager } = await import('./payments/wallet.js');
59
- const wallet = await WalletManager.getInstance().getOrCreateWallet();
60
- res.json({ address: wallet.address, chain: wallet.chain, note: 'Fund this address with USDC on Base to enable outbound payments.' });
61
- });
62
-
63
- app.get('/agents.json', (_req, res) => {
64
- res.sendFile('agents.json', { root: process.cwd() });
65
- });
66
- app.get('/llms.txt', (_req, res) => {
67
- res.sendFile('llms.txt', { root: process.cwd() });
68
- });
69
- app.get('/.well-known/agentcard.json', (_req, res) => {
70
- res.sendFile('.well-known/agentcard.json', { root: process.cwd() });
71
- });
72
-
73
- // FIX: Root handler — was 404, now returns service discovery
74
- app.get('/', (_req, res) => {
75
- res.json({
76
- name: 'mcp-x402',
77
- version: VERSION,
78
- description: 'The x402 Amazon — 43+ tools, pay-per-call via XRPL. scriptmasterlabs.com',
79
- status: 'online',
80
- transport: 'streamable-http + sse',
81
- endpoints: {
82
- mcp_streamable: 'POST /mcp',
83
- sse_connect: 'GET /sse',
84
- sse_messages: 'POST /messages',
85
- health: 'GET /health',
86
- agentCard: 'GET /.well-known/agentcard.json',
87
- llms: 'GET /llms.txt',
88
- },
89
- links: {
90
- github: 'https://github.com/Timwal78/SML_Portfolio/tree/main/mcp-x402',
91
- homepage: 'https://scriptmasterlabs.com',
92
- },
93
- });
94
- });
95
-
96
- // Streamable HTTP transport — claude.ai web connectors
97
- const streamableTransports = new Map<string, StreamableHTTPServerTransport>();
98
-
99
- app.post('/mcp', async (req, res) => {
100
- const sessionId = req.headers['mcp-session-id'] as string | undefined;
101
- let transport = sessionId ? streamableTransports.get(sessionId) : undefined;
102
-
103
- if (!transport) {
104
- const newSessionId = randomUUID();
105
- transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => newSessionId });
106
- streamableTransports.set(newSessionId, transport);
107
- transport.onclose = () => streamableTransports.delete(newSessionId);
108
- const server = await createServer();
109
- await server.connect(transport);
110
- AuditLogger.getInstance().info('mcp_connect', { sessionId: newSessionId });
111
- }
112
-
113
- await transport.handleRequest(req, res, req.body);
114
- });
115
-
116
- // FIX: GET /mcp with no session was 404, now returns service info
117
- app.get('/mcp', async (req, res) => {
118
- const sessionId = req.headers['mcp-session-id'] as string | undefined;
119
- const transport = sessionId ? streamableTransports.get(sessionId) : undefined;
120
- if (!transport) {
121
- res.json({
122
- name: 'mcp-x402',
123
- version: VERSION,
124
- protocol: 'MCP/streamable-http',
125
- status: 'ready',
126
- tools: '43+ tools available',
127
- how_to_connect: 'POST /mcp with a JSON-RPC initialize request',
128
- sse_alternative: 'GET /sse for legacy SSE transport',
129
- health: '/health',
130
- homepage: 'https://scriptmasterlabs.com',
131
- });
132
- return;
133
- }
134
- await transport.handleRequest(req, res);
135
- });
136
-
137
- app.delete('/mcp', async (req, res) => {
138
- const sessionId = req.headers['mcp-session-id'] as string | undefined;
139
- const transport = sessionId ? streamableTransports.get(sessionId) : undefined;
140
- if (!transport) { res.status(404).json({ error: 'session_not_found' }); return; }
141
- await transport.handleRequest(req, res);
142
- });
143
-
144
- const transports = new Map<string, SSEServerTransport>();
145
- const rateLimiter = RateLimiter.getInstance();
146
-
147
- app.get('/sse', async (req, res) => {
148
- const clientIp = req.ip ?? 'unknown';
149
- if (!rateLimiter.checkIp(clientIp)) {
150
- res.status(429).json({ error: 'rate_limit_exceeded', retry_after: 60 });
151
- return;
152
- }
153
- const transport = new SSEServerTransport('/messages', res);
154
- const sessionId = transport.sessionId;
155
- transports.set(sessionId, transport);
156
- const server = await createServer();
157
- await server.connect(transport);
158
- AuditLogger.getInstance().info('sse_connect', { sessionId, clientIp });
159
- res.on('close', async () => {
160
- transports.delete(sessionId);
161
- AuditLogger.getInstance().info('sse_disconnect', { sessionId });
162
- await server.close();
163
- });
164
- });
165
-
166
- app.post('/messages', async (req, res) => {
167
- const sessionId = req.query['sessionId'] as string | undefined;
168
- if (!sessionId) { res.status(400).json({ error: 'missing_session_id' }); return; }
169
- const transport = transports.get(sessionId);
170
- if (!transport) { res.status(404).json({ error: 'session_not_found' }); return; }
171
- await transport.handlePostMessage(req, res);
172
- });
173
-
174
- const httpServer = await new Promise<ReturnType<typeof app.listen>>(
175
- (resolve) => {
176
- const s = app.listen(port, () => resolve(s));
177
- },
178
- );
179
-
180
- AuditLogger.getInstance().info('server_start', { transport: 'sse', port, version: VERSION });
181
- console.error(`[mcp-x402] listening on :${port} — health: http://localhost:${port}/health`);
182
-
183
- const shutdown = async () => {
184
- AuditLogger.getInstance().info('server_stop', { transport: 'sse' });
185
- for (const [id] of transports) {
186
- AuditLogger.getInstance().info('sse_force_close', { sessionId: id });
187
- }
188
- httpServer.close(() => process.exit(0));
189
- setTimeout(() => process.exit(1), 10_000).unref();
190
- };
191
- process.on('SIGINT', shutdown);
192
- process.on('SIGTERM', shutdown);
193
-
194
- process.on('uncaughtException', (err) => {
195
- AuditLogger.getInstance().error('uncaught_exception', { error: String(err), stack: err.stack ?? '' });
196
- });
197
- process.on('unhandledRejection', (reason) => {
198
- AuditLogger.getInstance().error('unhandledRejection', { reason: String(reason) });
199
- });
200
- }
201
-
202
- const transport = process.env['MCP_TRANSPORT'] ?? 'stdio';
203
- if (transport === 'sse') {
204
- runSSE().catch((err) => {
205
- console.error('[mcp-x402] fatal:', err);
206
- process.exit(1);
207
- });
208
- } else {
209
- runStdio().catch((err) => {
210
- console.error('[mcp-x402] fatal:', err);
211
- process.exit(1);
212
- });
213
- }
@@ -1,101 +0,0 @@
1
- import { AuditLogger } from '../security/audit.js';
2
-
3
- export interface MandateParams {
4
- maxAmount: string;
5
- currency: string;
6
- toolName: string;
7
- }
8
-
9
- interface MandateCache {
10
- valid: boolean;
11
- expiresAt: number;
12
- }
13
-
14
- // In-memory mandate cache — 5 minute TTL per wallet+tool combination
15
- const mandateCache = new Map<string, MandateCache>();
16
-
17
- export class AP2Client {
18
- private static instance: AP2Client;
19
- private readonly baseUrl: string;
20
-
21
- private constructor() {
22
- this.baseUrl = process.env['SML_API_BASE'] ?? 'https://api.scriptmasterlabs.com';
23
- }
24
-
25
- static getInstance(): AP2Client {
26
- if (!AP2Client.instance) {
27
- AP2Client.instance = new AP2Client();
28
- }
29
- return AP2Client.instance;
30
- }
31
-
32
- async verifyMandate(wallet: string, params: MandateParams): Promise<boolean> {
33
- const cacheKey = `${wallet}:${params.toolName}:${params.currency}`;
34
- const cached = mandateCache.get(cacheKey);
35
-
36
- if (cached && cached.expiresAt > Date.now()) {
37
- return cached.valid;
38
- }
39
-
40
- const audit = AuditLogger.getInstance();
41
-
42
- try {
43
- const controller = new AbortController();
44
- const timeout = setTimeout(() => controller.abort(), 5000);
45
-
46
- const res = await fetch(`${this.baseUrl}/ap2/v1/mandate/verify`, {
47
- method: 'POST',
48
- headers: { 'Content-Type': 'application/json' },
49
- body: JSON.stringify({
50
- wallet,
51
- max_amount: params.maxAmount,
52
- currency: params.currency,
53
- tool: params.toolName,
54
- }),
55
- signal: controller.signal,
56
- });
57
-
58
- clearTimeout(timeout);
59
-
60
- if (!res.ok) {
61
- audit.warn('ap2_mandate_http_error', { status: res.status, wallet });
62
- mandateCache.set(cacheKey, { valid: false, expiresAt: Date.now() + 60_000 });
63
- return false;
64
- }
65
-
66
- const body = (await res.json()) as { valid: boolean; expires_in?: number };
67
- const ttl = (body.expires_in ?? 300) * 1000;
68
-
69
- mandateCache.set(cacheKey, { valid: body.valid, expiresAt: Date.now() + ttl });
70
- return body.valid;
71
- } catch (err) {
72
- audit.error('ap2_mandate_error', { error: String(err), wallet });
73
- // Fail open when AP2 service is unreachable — log and allow
74
- audit.warn('ap2_mandate_fallback', { wallet, tool: params.toolName, note: 'AP2 unreachable, auto-approving' });
75
- mandateCache.set(cacheKey, { valid: true, expiresAt: Date.now() + 60_000 });
76
- return true;
77
- }
78
- }
79
-
80
- async createMandate(
81
- wallet: string,
82
- params: { dailyCap: string; currency: string },
83
- ): Promise<string> {
84
- const res = await fetch(`${this.baseUrl}/ap2/v1/mandate/create`, {
85
- method: 'POST',
86
- headers: { 'Content-Type': 'application/json' },
87
- body: JSON.stringify({
88
- wallet,
89
- daily_cap: params.dailyCap,
90
- currency: params.currency,
91
- }),
92
- });
93
-
94
- if (!res.ok) {
95
- throw new Error(`AP2 mandate creation failed: HTTP ${res.status}`);
96
- }
97
-
98
- const body = (await res.json()) as { mandate_id: string };
99
- return body.mandate_id;
100
- }
101
- }
@@ -1,85 +0,0 @@
1
- import { createHash, randomUUID } from 'crypto';
2
- import { AuditLogger } from '../security/audit.js';
3
-
4
- export interface ReceiptInput {
5
- txHash: string;
6
- chain: string;
7
- amount: string;
8
- currency: string;
9
- tool: string;
10
- wallet: string;
11
- }
12
-
13
- export interface Receipt {
14
- id: string;
15
- txHash: string;
16
- chain: string;
17
- amount: string;
18
- currency: string;
19
- tool: string;
20
- wallet: string;
21
- issuedAt: number;
22
- hash: string;
23
- }
24
-
25
- export class ReceiptStore {
26
- private static instance: ReceiptStore;
27
- private readonly baseUrl: string;
28
-
29
- private constructor() {
30
- this.baseUrl = process.env['SML_API_BASE'] ?? 'https://api.scriptmasterlabs.com';
31
- }
32
-
33
- static getInstance(): ReceiptStore {
34
- if (!ReceiptStore.instance) {
35
- ReceiptStore.instance = new ReceiptStore();
36
- }
37
- return ReceiptStore.instance;
38
- }
39
-
40
- async create(input: ReceiptInput): Promise<Receipt> {
41
- const id = randomUUID();
42
- const issuedAt = Date.now();
43
-
44
- // Content hash for tamper detection
45
- const hash = createHash('sha256')
46
- .update(`${id}:${input.txHash}:${input.chain}:${input.amount}:${input.currency}:${input.tool}:${input.wallet}:${issuedAt}`)
47
- .digest('hex');
48
-
49
- const receipt: Receipt = { id, ...input, issuedAt, hash };
50
-
51
- // Attempt to register with 402Proof server (N7)
52
- try {
53
- await this.registerWithProofServer(receipt);
54
- } catch (err) {
55
- // Log but don't fail — local receipt is still valid
56
- AuditLogger.getInstance().warn('proof_server_register_fail', { error: String(err), receiptId: id });
57
- }
58
-
59
- AuditLogger.getInstance().info('receipt_issued', { receiptId: id, tool: input.tool });
60
- return receipt;
61
- }
62
-
63
- private async registerWithProofServer(receipt: Receipt): Promise<void> {
64
- const proofUrl = process.env['PROOF402_URL'] ?? 'https://four02proof.onrender.com';
65
- const res = await fetch(`${proofUrl}/v1/receipt`, {
66
- method: 'POST',
67
- headers: { 'Content-Type': 'application/json' },
68
- body: JSON.stringify({
69
- receipt_id: receipt.id,
70
- tx_hash: receipt.txHash,
71
- chain: receipt.chain,
72
- amount: receipt.amount,
73
- currency: receipt.currency,
74
- tool: receipt.tool,
75
- wallet: receipt.wallet,
76
- issued_at: receipt.issuedAt,
77
- hash: receipt.hash,
78
- }),
79
- });
80
-
81
- if (!res.ok) {
82
- throw new Error(`402Proof server returned HTTP ${res.status}`);
83
- }
84
- }
85
- }
@@ -1,110 +0,0 @@
1
- import { AuditLogger } from '../security/audit.js';
2
- import { BaseChain } from '../../lib/chains/base.js';
3
- import { XRPLChain } from '../../lib/chains/xrpl.js';
4
- import { SolanaChain } from '../../lib/chains/solana.js';
5
-
6
- export interface RouteParams {
7
- amount: string;
8
- currency: 'USDC' | 'RLUSD';
9
- from: string;
10
- to: string;
11
- timeoutMs?: number;
12
- }
13
-
14
- export interface RouteResult {
15
- txHash: string;
16
- chain: string;
17
- latencyMs: number;
18
- }
19
-
20
- // Chain preference order: cheapest/fastest first (N13)
21
- const CHAIN_PREFERENCE = ['base', 'xrpl', 'solana'] as const;
22
-
23
- export class ChainRouter {
24
- private static instance: ChainRouter;
25
-
26
- private constructor(
27
- private readonly base = BaseChain.getInstance(),
28
- private readonly xrpl = XRPLChain.getInstance(),
29
- private readonly solana = SolanaChain.getInstance(),
30
- ) {}
31
-
32
- static getInstance(): ChainRouter {
33
- if (!ChainRouter.instance) {
34
- ChainRouter.instance = new ChainRouter();
35
- }
36
- return ChainRouter.instance;
37
- }
38
-
39
- async route(params: RouteParams): Promise<RouteResult> {
40
- const audit = AuditLogger.getInstance();
41
- const timeout = params.timeoutMs ?? 3000;
42
-
43
- // For RLUSD, prefer XRPL
44
- const ordered =
45
- params.currency === 'RLUSD'
46
- ? (['xrpl', 'base', 'solana'] as const)
47
- : CHAIN_PREFERENCE;
48
-
49
- const errors: string[] = [];
50
-
51
- for (const chain of ordered) {
52
- try {
53
- const start = Date.now();
54
- let txHash: string;
55
-
56
- switch (chain) {
57
- case 'base':
58
- txHash = await this.withTimeout(
59
- this.base.sendPayment(params),
60
- timeout,
61
- 'base',
62
- );
63
- break;
64
- case 'xrpl':
65
- txHash = await this.withTimeout(
66
- this.xrpl.sendPayment(params),
67
- timeout,
68
- 'xrpl',
69
- );
70
- break;
71
- case 'solana':
72
- txHash = await this.withTimeout(
73
- this.solana.sendPayment(params),
74
- timeout,
75
- 'solana',
76
- );
77
- break;
78
- }
79
-
80
- const latencyMs = Date.now() - start;
81
- audit.info('chain_route_success', { chain, latencyMs, tx: txHash });
82
-
83
- return { txHash, chain, latencyMs };
84
- } catch (err) {
85
- const msg = String(err);
86
- errors.push(`${chain}: ${msg}`);
87
- audit.warn('chain_route_fail', { chain, error: msg });
88
- }
89
- }
90
-
91
- throw new Error(`All chains failed.\n${errors.join('\n')}`);
92
- }
93
-
94
- private withTimeout<T>(
95
- promise: Promise<T>,
96
- ms: number,
97
- chain: string,
98
- ): Promise<T> {
99
- return new Promise((resolve, reject) => {
100
- const t = setTimeout(
101
- () => reject(new Error(`${chain} timeout after ${ms}ms`)),
102
- ms,
103
- );
104
- promise.then(
105
- (v) => { clearTimeout(t); resolve(v); },
106
- (e) => { clearTimeout(t); reject(e); },
107
- );
108
- });
109
- }
110
- }
@@ -1,123 +0,0 @@
1
- import { AuditLogger } from '../security/audit.js';
2
-
3
- const KEYCHAIN_SERVICE = 'mcp-x402';
4
- const KEYCHAIN_ACCOUNT = 'master-seed';
5
-
6
- export interface WalletInfo {
7
- address: string;
8
- chain: 'base' | 'xrpl' | 'solana';
9
- }
10
-
11
- export class WalletManager {
12
- private static instance: WalletManager;
13
- private cachedAddress: string | null = null;
14
- private cachedSeed: string | null = null;
15
-
16
- private constructor() {}
17
-
18
- static getInstance(): WalletManager {
19
- if (!WalletManager.instance) {
20
- WalletManager.instance = new WalletManager();
21
- }
22
- return WalletManager.instance;
23
- }
24
-
25
- // Try OS keychain; returns null if keytar is unavailable (Docker/cloud) or no entry exists.
26
- private async keytarGet(): Promise<string | null> {
27
- try {
28
- const kt = await import('keytar');
29
- return await kt.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- private async keytarSet(value: string): Promise<void> {
36
- try {
37
- const kt = await import('keytar');
38
- await kt.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, value);
39
- } catch {
40
- // Silently skip — keytar unavailable in this environment
41
- }
42
- }
43
-
44
- // Seed resolution priority:
45
- // 1. OS keychain (local desktop — most secure)
46
- // 2. WALLET_SEED env var (Render secret — cloud/Docker deployment)
47
- // 3. CI_WALLET_SEED env var (CI only, never production)
48
- // 4. Generate fresh and try to persist in keychain
49
- async getSeed(): Promise<string> {
50
- if (this.cachedSeed) return this.cachedSeed;
51
-
52
- const audit = AuditLogger.getInstance();
53
-
54
- let seed = await this.keytarGet();
55
-
56
- if (!seed) {
57
- const envSeed = process.env['WALLET_SEED'];
58
- if (envSeed) {
59
- seed = envSeed;
60
- audit.warn('wallet_env_seed', { note: 'Using WALLET_SEED env var (cloud deployment).' });
61
- } else if (process.env['CI_WALLET_SEED'] && process.env['NODE_ENV'] !== 'production') {
62
- seed = process.env['CI_WALLET_SEED'];
63
- audit.warn('wallet_ci_fallback', { note: 'Using CI_WALLET_SEED. NEVER do this in production.' });
64
- } else {
65
- const { generateMnemonic } = await import('bip39');
66
- seed = generateMnemonic(256);
67
- await this.keytarSet(seed);
68
- audit.info('wallet_created', { note: 'New BIP-39 seed generated.' });
69
- }
70
- }
71
-
72
- this.cachedSeed = seed;
73
- return seed;
74
- }
75
-
76
- async getOrCreateWallet(): Promise<WalletInfo> {
77
- if (this.cachedAddress) {
78
- return { address: this.cachedAddress, chain: 'base' };
79
- }
80
-
81
- const audit = AuditLogger.getInstance();
82
- const seed = await this.getSeed();
83
- const address = await this.deriveAddress(seed);
84
- this.cachedAddress = address;
85
- audit.info('wallet_loaded', { address });
86
- return { address, chain: 'base' };
87
- }
88
-
89
- private async deriveAddress(mnemonic: string): Promise<string> {
90
- // BIP-44 deterministic derivation: m/44'/60'/0'/0/0 (Base = EVM)
91
- const { mnemonicToSeedSync } = await import('bip39');
92
- const { default: HDKey } = await import('hdkey');
93
- const { privateKeyToAccount } = await import('viem/accounts');
94
-
95
- const seed = mnemonicToSeedSync(mnemonic);
96
- const hdkey = HDKey.fromMasterSeed(seed);
97
- const child = hdkey.derive("m/44'/60'/0'/0/0");
98
-
99
- if (!child.privateKey) {
100
- throw new Error('Failed to derive private key from HD path');
101
- }
102
-
103
- const account = privateKeyToAccount(`0x${child.privateKey.toString('hex')}`);
104
- return account.address;
105
- }
106
-
107
- async signPayload(payload: string): Promise<string> {
108
- const mnemonic = await this.getSeed();
109
-
110
- const { mnemonicToSeedSync } = await import('bip39');
111
- const { default: HDKey } = await import('hdkey');
112
- const { privateKeyToAccount } = await import('viem/accounts');
113
-
114
- const seed = mnemonicToSeedSync(mnemonic);
115
- const hdkey = HDKey.fromMasterSeed(seed);
116
- const child = hdkey.derive("m/44'/60'/0'/0/0");
117
-
118
- if (!child.privateKey) throw new Error('Key derivation failed');
119
-
120
- const account = privateKeyToAccount(`0x${child.privateKey.toString('hex')}`);
121
- return account.signMessage({ message: payload });
122
- }
123
- }