@nullpay/mcp 1.0.1 → 1.0.4
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 +186 -30
- package/dist/aleo.d.ts +16 -0
- package/dist/aleo.js +134 -7
- package/dist/env.d.ts +1 -1
- package/dist/env.js +1 -1
- package/dist/service.js +90 -9
- package/package.json +32 -32
package/README.md
CHANGED
|
@@ -1,60 +1,216 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nullpay/mcp
|
|
2
2
|
|
|
3
|
-
NullPay MCP
|
|
3
|
+
NullPay MCP is a local Model Context Protocol server for Claude that lets users create invoices, pay invoices, and inspect transactions through chat.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The package is designed so the user only needs to provide their own wallet credentials:
|
|
6
|
+
|
|
7
|
+
- `NULLPAY_MAIN_ADDRESS`
|
|
8
|
+
- `NULLPAY_MAIN_PRIVATE_KEY`
|
|
9
|
+
- `NULLPAY_MAIN_PASSWORD`
|
|
10
|
+
|
|
11
|
+
NullPay-owned infrastructure stays inside the package or on the backend:
|
|
12
|
+
|
|
13
|
+
- default backend API: `https://nullpay-backend-ib5q4.ondigitalocean.app/api`
|
|
14
|
+
- default public app URL: `https://nullpay.app`
|
|
15
|
+
- relayer private key: backend only
|
|
16
|
+
|
|
17
|
+
## How It Works
|
|
18
|
+
|
|
19
|
+
1. The user runs the setup wizard.
|
|
20
|
+
2. The wizard asks whether to install into Claude Code or Claude Desktop.
|
|
21
|
+
3. The wizard asks for the user's address, private key, and password.
|
|
22
|
+
4. The wizard writes the MCP config file automatically on the user's machine.
|
|
23
|
+
5. Claude starts the local NullPay MCP server with `@nullpay/mcp server`.
|
|
24
|
+
6. The MCP server talks to the NullPay backend and keeps the user's private-key operations local.
|
|
25
|
+
|
|
26
|
+
## Install For End Users
|
|
27
|
+
|
|
28
|
+
Run the setup wizard:
|
|
6
29
|
|
|
7
30
|
```bash
|
|
8
31
|
npx -y @nullpay/mcp
|
|
9
32
|
```
|
|
10
33
|
|
|
11
|
-
|
|
34
|
+
You can also call the setup command explicitly:
|
|
12
35
|
|
|
13
|
-
|
|
36
|
+
```bash
|
|
37
|
+
npx -y @nullpay/mcp setup
|
|
38
|
+
```
|
|
14
39
|
|
|
15
|
-
|
|
40
|
+
The wizard writes the required MCP config entry automatically.
|
|
16
41
|
|
|
17
|
-
|
|
18
|
-
- `NULLPAY_MAIN_PRIVATE_KEY`
|
|
19
|
-
- `NULLPAY_MAIN_PASSWORD`
|
|
42
|
+
## Manual Claude Configuration
|
|
20
43
|
|
|
21
|
-
|
|
44
|
+
The setup wizard is the recommended path, but you can also add the MCP server manually.
|
|
45
|
+
|
|
46
|
+
### Claude Desktop
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"nullpay": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "@nullpay/mcp", "server"],
|
|
54
|
+
"env": {
|
|
55
|
+
"NULLPAY_MAIN_ADDRESS": "aleo1...",
|
|
56
|
+
"NULLPAY_MAIN_PRIVATE_KEY": "APrivateKey1...",
|
|
57
|
+
"NULLPAY_MAIN_PASSWORD": "your-password"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
On Windows, Claude Desktop may need:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"nullpay": {
|
|
70
|
+
"command": "cmd",
|
|
71
|
+
"args": ["/c", "npx", "-y", "@nullpay/mcp", "server"],
|
|
72
|
+
"env": {
|
|
73
|
+
"NULLPAY_MAIN_ADDRESS": "aleo1...",
|
|
74
|
+
"NULLPAY_MAIN_PRIVATE_KEY": "APrivateKey1...",
|
|
75
|
+
"NULLPAY_MAIN_PASSWORD": "your-password"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Claude Code
|
|
83
|
+
|
|
84
|
+
The setup wizard can also write the Claude Code config automatically. If needed, the same `server` command is used there too.
|
|
85
|
+
|
|
86
|
+
After setup, restart Claude and verify the MCP server is available.
|
|
87
|
+
|
|
88
|
+
## Commands
|
|
89
|
+
|
|
90
|
+
### Published package
|
|
91
|
+
|
|
92
|
+
Start the interactive installer:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx -y @nullpay/mcp
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Run the MCP server directly:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx -y @nullpay/mcp server
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Show help:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx -y @nullpay/mcp --help
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Local development
|
|
111
|
+
|
|
112
|
+
From `packages/nullpay-mcp`:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm install
|
|
116
|
+
npm run build
|
|
117
|
+
node dist/cli.js
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Run the local MCP server directly:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
node dist/cli.js server
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Run in TypeScript during development:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm run dev
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Tooling And Login Model
|
|
133
|
+
|
|
134
|
+
The MCP server exposes four tools:
|
|
22
135
|
|
|
23
136
|
- `login`
|
|
24
137
|
- `create_invoice`
|
|
25
138
|
- `pay_invoice`
|
|
26
139
|
- `get_transaction_info`
|
|
27
140
|
|
|
28
|
-
|
|
141
|
+
### `login`
|
|
142
|
+
|
|
143
|
+
`login` creates or resumes the NullPay session inside the MCP process.
|
|
144
|
+
|
|
145
|
+
Normal path:
|
|
29
146
|
|
|
30
|
-
|
|
147
|
+
- uses address + password from tool input or env
|
|
148
|
+
- loads the backend profile
|
|
149
|
+
- validates the password by decrypting stored wallet data
|
|
150
|
+
- restores burner wallet metadata when present
|
|
31
151
|
|
|
32
|
-
|
|
33
|
-
- public NullPay base URL
|
|
34
|
-
- Provable API key
|
|
35
|
-
- Provable consumer ID
|
|
152
|
+
Recovery path:
|
|
36
153
|
|
|
37
|
-
|
|
154
|
+
- if password is missing but `NULLPAY_MAIN_PRIVATE_KEY` is available
|
|
155
|
+
- the server can recover the backed-up password from on-chain backup records
|
|
156
|
+
- if a full burner backup exists on-chain, it restores the burner wallet automatically
|
|
157
|
+
|
|
158
|
+
### `create_invoice`
|
|
159
|
+
|
|
160
|
+
- uses the active wallet address
|
|
161
|
+
- calls the NullPay backend relayer endpoint
|
|
162
|
+
- waits for the invoice hash to resolve from Aleo mapping
|
|
163
|
+
- stores the invoice row in the backend
|
|
164
|
+
- returns a payment link
|
|
165
|
+
|
|
166
|
+
### `pay_invoice`
|
|
167
|
+
|
|
168
|
+
- accepts either a full NullPay payment link or an invoice hash
|
|
169
|
+
- prefers the full payment link so merchant address, amount, salt, token, and session id are available immediately
|
|
170
|
+
- builds the payment authorization locally with the user's wallet key
|
|
171
|
+
- sends the authorization to the backend sponsor endpoint
|
|
172
|
+
|
|
173
|
+
### `get_transaction_info`
|
|
174
|
+
|
|
175
|
+
- fetches invoice rows from the backend
|
|
176
|
+
- enriches private record-backed data locally when the main private key is available
|
|
177
|
+
|
|
178
|
+
## Credential Model
|
|
179
|
+
|
|
180
|
+
User-provided values:
|
|
38
181
|
|
|
39
182
|
- `NULLPAY_MAIN_ADDRESS`
|
|
40
|
-
- `NULLPAY_MAIN_PASSWORD`
|
|
41
183
|
- `NULLPAY_MAIN_PRIVATE_KEY`
|
|
184
|
+
- `NULLPAY_MAIN_PASSWORD`
|
|
185
|
+
|
|
186
|
+
Bundled or backend-side values:
|
|
187
|
+
|
|
188
|
+
- `NULLPAY_BACKEND_URL` defaults internally to production
|
|
189
|
+
- `NULLPAY_PUBLIC_BASE_URL` defaults internally to production
|
|
190
|
+
- relayer secrets stay on the backend
|
|
42
191
|
|
|
43
|
-
|
|
192
|
+
## Security Notes
|
|
44
193
|
|
|
45
|
-
|
|
194
|
+
- The user's private key is intended to stay local to the MCP process.
|
|
195
|
+
- The config file used by Claude contains sensitive wallet credentials and should be treated like a secret file.
|
|
196
|
+
- The MCP server never returns decrypted private keys in tool output.
|
|
197
|
+
- Password recovery from on-chain records only works when the main private key is available locally.
|
|
46
198
|
|
|
47
|
-
|
|
48
|
-
- repo-root `.env`
|
|
49
|
-
- `backend/.env`
|
|
199
|
+
## Publishing
|
|
50
200
|
|
|
51
|
-
|
|
201
|
+
From `packages/nullpay-mcp`:
|
|
52
202
|
|
|
53
|
-
|
|
203
|
+
```bash
|
|
204
|
+
npm run build
|
|
205
|
+
npm publish --access public
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
If npm 2FA is enabled:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm publish --access public --otp=123456
|
|
212
|
+
```
|
|
54
213
|
|
|
55
|
-
##
|
|
214
|
+
## License
|
|
56
215
|
|
|
57
|
-
|
|
58
|
-
- The MCP server decrypts burner keys only in memory during payment execution.
|
|
59
|
-
- If `NULLPAY_MAIN_PRIVATE_KEY` is available, the MCP server can fetch invoice amounts from main-wallet records and pay invoices from the main wallet without exposing that key to the model.
|
|
60
|
-
- If the main private key is not available, the MCP server still allows login and invoice creation, and it prompts the user to add the env var for record-backed amount lookup and automated main-wallet payments.
|
|
216
|
+
MIT
|
package/dist/aleo.d.ts
CHANGED
|
@@ -53,8 +53,24 @@ export declare function createInvoiceDbRecord(args: {
|
|
|
53
53
|
}[] | null;
|
|
54
54
|
for_sdk: boolean;
|
|
55
55
|
};
|
|
56
|
+
export interface ParsedBurnerBackupRecord {
|
|
57
|
+
owner: string;
|
|
58
|
+
burnerAddress: string;
|
|
59
|
+
passwordPart: string;
|
|
60
|
+
pkParts: string[];
|
|
61
|
+
plaintext: string;
|
|
62
|
+
}
|
|
63
|
+
export interface RecoveredOnChainWalletBackup {
|
|
64
|
+
password: string;
|
|
65
|
+
burnerAddress?: string;
|
|
66
|
+
encryptedBurnerKey?: string;
|
|
67
|
+
source: 'password_only' | 'full_burner';
|
|
68
|
+
}
|
|
56
69
|
export declare function parseOwnedInvoiceRecord(plaintext: string): ParsedOwnedInvoiceRecord | null;
|
|
70
|
+
export declare function parseBurnerBackupRecord(plaintext: string): ParsedBurnerBackupRecord | null;
|
|
57
71
|
export declare function fetchOwnedInvoiceRecords(privateKey: string): Promise<ParsedOwnedInvoiceRecord[]>;
|
|
72
|
+
export declare function fetchOwnedBurnerBackupRecords(privateKey: string): Promise<ParsedBurnerBackupRecord[]>;
|
|
73
|
+
export declare function recoverOnChainWalletBackup(privateKey: string, ownerAddress: string): Promise<RecoveredOnChainWalletBackup | null>;
|
|
58
74
|
export declare function fetchOwnedInvoiceRecordByHash(privateKey: string, invoiceHash: string): Promise<ParsedOwnedInvoiceRecord | null>;
|
|
59
75
|
export declare function enrichInvoiceWithRecordAmount(invoice: InvoiceRecord, privateKey?: string | null): Promise<InvoiceRecord>;
|
|
60
76
|
export declare function createSponsoredPaymentAuthorization(args: {
|
package/dist/aleo.js
CHANGED
|
@@ -13,7 +13,10 @@ exports.invoiceTypeToNumber = invoiceTypeToNumber;
|
|
|
13
13
|
exports.buildPaymentLink = buildPaymentLink;
|
|
14
14
|
exports.createInvoiceDbRecord = createInvoiceDbRecord;
|
|
15
15
|
exports.parseOwnedInvoiceRecord = parseOwnedInvoiceRecord;
|
|
16
|
+
exports.parseBurnerBackupRecord = parseBurnerBackupRecord;
|
|
16
17
|
exports.fetchOwnedInvoiceRecords = fetchOwnedInvoiceRecords;
|
|
18
|
+
exports.fetchOwnedBurnerBackupRecords = fetchOwnedBurnerBackupRecords;
|
|
19
|
+
exports.recoverOnChainWalletBackup = recoverOnChainWalletBackup;
|
|
17
20
|
exports.fetchOwnedInvoiceRecordByHash = fetchOwnedInvoiceRecordByHash;
|
|
18
21
|
exports.enrichInvoiceWithRecordAmount = enrichInvoiceWithRecordAmount;
|
|
19
22
|
exports.createSponsoredPaymentAuthorization = createSponsoredPaymentAuthorization;
|
|
@@ -52,6 +55,30 @@ function fieldToString(fieldVal) {
|
|
|
52
55
|
return '';
|
|
53
56
|
}
|
|
54
57
|
}
|
|
58
|
+
function fieldChunksToString(chunks) {
|
|
59
|
+
let result = '';
|
|
60
|
+
for (const chunk of chunks) {
|
|
61
|
+
if (!chunk || chunk === '0field' || chunk === '0') {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const numeric = chunk.replace('field', '').replace('u128', '').replace('u64', '');
|
|
65
|
+
let value;
|
|
66
|
+
try {
|
|
67
|
+
value = BigInt(numeric);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let hex = value.toString(16);
|
|
73
|
+
if (hex.length % 2 !== 0) {
|
|
74
|
+
hex = '0' + hex;
|
|
75
|
+
}
|
|
76
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
77
|
+
result += String.fromCharCode(Number.parseInt(hex.slice(i, i + 2), 16));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
55
82
|
function parseNumericValue(value) {
|
|
56
83
|
if (!value) {
|
|
57
84
|
return 0;
|
|
@@ -305,6 +332,41 @@ function parseOwnedInvoiceRecord(plaintext) {
|
|
|
305
332
|
return null;
|
|
306
333
|
}
|
|
307
334
|
}
|
|
335
|
+
function parseBurnerBackupRecord(plaintext) {
|
|
336
|
+
try {
|
|
337
|
+
const getVal = (key) => {
|
|
338
|
+
const regex = new RegExp(`(?:${key}|"${key}"):\\s*([\\w\\d\\.]+)`);
|
|
339
|
+
const match = plaintext.match(regex);
|
|
340
|
+
if (match && match[1]) {
|
|
341
|
+
return match[1].replace('.private', '').replace('.public', '');
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
};
|
|
345
|
+
const owner = getVal('owner');
|
|
346
|
+
const burnerAddress = getVal('burner_address');
|
|
347
|
+
const passwordPart = getVal('password_part');
|
|
348
|
+
if (!burnerAddress || !passwordPart) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const pkParts = [];
|
|
352
|
+
for (let i = 1; i <= 10; i += 1) {
|
|
353
|
+
const part = getVal(`pk_part_${i}`);
|
|
354
|
+
if (part && part !== '0field' && part !== '0') {
|
|
355
|
+
pkParts.push(part);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
owner: owner || '',
|
|
360
|
+
burnerAddress,
|
|
361
|
+
passwordPart,
|
|
362
|
+
pkParts,
|
|
363
|
+
plaintext,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
308
370
|
async function fetchOwnedInvoiceRecords(privateKey) {
|
|
309
371
|
const session = await getScannerSession(privateKey);
|
|
310
372
|
const records = await fetchOwnedProgramRecords(session, exports.PROGRAM_ID);
|
|
@@ -325,6 +387,67 @@ async function fetchOwnedInvoiceRecords(privateKey) {
|
|
|
325
387
|
}
|
|
326
388
|
return parsed;
|
|
327
389
|
}
|
|
390
|
+
async function fetchOwnedBurnerBackupRecords(privateKey) {
|
|
391
|
+
const session = await getScannerSession(privateKey);
|
|
392
|
+
const records = await fetchOwnedProgramRecords(session, exports.PROGRAM_ID);
|
|
393
|
+
const parsed = [];
|
|
394
|
+
for (const record of records) {
|
|
395
|
+
let plaintext = record.record_plaintext || record.plaintext || '';
|
|
396
|
+
if (!plaintext && record.record_ciphertext) {
|
|
397
|
+
const { RecordCiphertext } = await (0, esm_1.dynamicImport)('@provablehq/sdk');
|
|
398
|
+
const ciphertext = RecordCiphertext.fromString(record.record_ciphertext);
|
|
399
|
+
plaintext = ciphertext.decrypt(session.account.viewKey()).toString();
|
|
400
|
+
}
|
|
401
|
+
if (!plaintext)
|
|
402
|
+
continue;
|
|
403
|
+
const burnerRecord = parseBurnerBackupRecord(plaintext);
|
|
404
|
+
if (burnerRecord) {
|
|
405
|
+
parsed.push(burnerRecord);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return parsed;
|
|
409
|
+
}
|
|
410
|
+
async function recoverOnChainWalletBackup(privateKey, ownerAddress) {
|
|
411
|
+
const records = await fetchOwnedBurnerBackupRecords(privateKey);
|
|
412
|
+
let passwordOnlyMatch = null;
|
|
413
|
+
let fullBurnerMatch = null;
|
|
414
|
+
for (const record of records) {
|
|
415
|
+
const ownerMatches = record.owner === ownerAddress;
|
|
416
|
+
const passwordOnlyMatches = record.burnerAddress === ownerAddress;
|
|
417
|
+
if (!ownerMatches && !passwordOnlyMatches) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const encryptedPayload = fieldChunksToString(record.pkParts);
|
|
421
|
+
const hasRealPayload = Boolean(encryptedPayload && !encryptedPayload.startsWith('0'));
|
|
422
|
+
if (hasRealPayload) {
|
|
423
|
+
fullBurnerMatch = record;
|
|
424
|
+
}
|
|
425
|
+
else if (!passwordOnlyMatch) {
|
|
426
|
+
passwordOnlyMatch = record;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const bestMatch = fullBurnerMatch || passwordOnlyMatch;
|
|
430
|
+
if (!bestMatch) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
const password = fieldChunksToString([bestMatch.passwordPart]);
|
|
434
|
+
if (!password) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
if (fullBurnerMatch) {
|
|
438
|
+
const encryptedBurnerKey = fieldChunksToString(fullBurnerMatch.pkParts);
|
|
439
|
+
return {
|
|
440
|
+
password,
|
|
441
|
+
burnerAddress: fullBurnerMatch.burnerAddress,
|
|
442
|
+
encryptedBurnerKey: encryptedBurnerKey || undefined,
|
|
443
|
+
source: 'full_burner',
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
password,
|
|
448
|
+
source: 'password_only',
|
|
449
|
+
};
|
|
450
|
+
}
|
|
328
451
|
async function fetchOwnedInvoiceRecordByHash(privateKey, invoiceHash) {
|
|
329
452
|
const normalized = normalizeInvoiceHash(invoiceHash);
|
|
330
453
|
const records = await fetchOwnedInvoiceRecords(privateKey);
|
|
@@ -432,14 +555,18 @@ async function getFreezeListIndex(index) {
|
|
|
432
555
|
return value ? value.replace(/"/g, '') : null;
|
|
433
556
|
}
|
|
434
557
|
async function generateFreezeListProof(targetIndex = 1, occupiedLeafValue) {
|
|
435
|
-
const {
|
|
558
|
+
const { Plaintext } = await (0, esm_1.dynamicImport)('@provablehq/sdk');
|
|
559
|
+
const { Poseidon4 } = await (0, esm_1.dynamicImport)('@provablehq/wasm');
|
|
436
560
|
const hasher = new Poseidon4();
|
|
437
561
|
const emptyHashes = [];
|
|
438
562
|
let currentEmpty = '0field';
|
|
563
|
+
const hashFields = (values) => {
|
|
564
|
+
const plaintext = Plaintext.fromString(`[${values.join(', ')}]`);
|
|
565
|
+
return hasher.hash(plaintext.toFields()).toString();
|
|
566
|
+
};
|
|
439
567
|
for (let level = 0; level < 16; level += 1) {
|
|
440
568
|
emptyHashes.push(currentEmpty);
|
|
441
|
-
|
|
442
|
-
currentEmpty = hasher.hash([field, field]).toString();
|
|
569
|
+
currentEmpty = hashFields([currentEmpty, currentEmpty]);
|
|
443
570
|
}
|
|
444
571
|
let currentHash = '0field';
|
|
445
572
|
let currentIndex = targetIndex;
|
|
@@ -452,12 +579,12 @@ async function generateFreezeListProof(targetIndex = 1, occupiedLeafValue) {
|
|
|
452
579
|
siblingHash = occupiedLeafValue;
|
|
453
580
|
}
|
|
454
581
|
proofSiblings.push(siblingHash);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
582
|
+
currentHash = isLeft
|
|
583
|
+
? hashFields([currentHash, siblingHash])
|
|
584
|
+
: hashFields([siblingHash, currentHash]);
|
|
458
585
|
currentIndex = Math.floor(currentIndex / 2);
|
|
459
586
|
}
|
|
460
|
-
return `[${proofSiblings.join(', ')}]`;
|
|
587
|
+
return `{ siblings: [${proofSiblings.join(', ')}], leaf_index: ${targetIndex}u32 }`;
|
|
461
588
|
}
|
|
462
589
|
async function createSponsoredPaymentAuthorization(args) {
|
|
463
590
|
const session = await getScannerSession(args.walletPrivateKey);
|
package/dist/env.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const DEFAULT_BACKEND_URL = "
|
|
1
|
+
export declare const DEFAULT_BACKEND_URL = "https://nullpay-backend-ib5q4.ondigitalocean.app/api";
|
|
2
2
|
export declare const DEFAULT_PUBLIC_BASE_URL = "https://nullpay.app";
|
|
3
3
|
export declare function parseDotEnv(content: string): Record<string, string>;
|
|
4
4
|
export declare function loadEnvFiles(): void;
|
package/dist/env.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.getRuntimeConfig = getRuntimeConfig;
|
|
|
10
10
|
exports.getProvableConfig = getProvableConfig;
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
-
exports.DEFAULT_BACKEND_URL = '
|
|
13
|
+
exports.DEFAULT_BACKEND_URL = 'https://nullpay-backend-ib5q4.ondigitalocean.app/api';
|
|
14
14
|
exports.DEFAULT_PUBLIC_BASE_URL = 'https://nullpay.app';
|
|
15
15
|
const DEFAULT_PROVABLE_API_KEY = 'tWR9YWkM5SVmx1u3m7My8S4p4e2s84Oe';
|
|
16
16
|
const DEFAULT_PROVABLE_CONSUMER_ID = '73ba1b21-d8f7-4d7f-bfd9-0408a4e183f3';
|
package/dist/service.js
CHANGED
|
@@ -142,7 +142,7 @@ class NullPayMcpService {
|
|
|
142
142
|
return [
|
|
143
143
|
{
|
|
144
144
|
name: 'login',
|
|
145
|
-
description: 'Login to NullPay, validate password, create burner wallet, or switch active wallet. If NULLPAY_MAIN_ADDRESS and NULLPAY_MAIN_PASSWORD are configured, call this tool with empty arguments and do not ask the user to share secrets in chat. The MCP server can also read NULLPAY_MAIN_PRIVATE_KEY from env without exposing it to the model.',
|
|
145
|
+
description: 'Login to NullPay, validate password, create burner wallet, recover a backed-up password or burner wallet from on-chain records, or switch active wallet. If NULLPAY_MAIN_ADDRESS and NULLPAY_MAIN_PASSWORD are configured, call this tool with empty arguments and do not ask the user to share secrets in chat. The MCP server can also read NULLPAY_MAIN_PRIVATE_KEY from env without exposing it to the model.',
|
|
146
146
|
inputSchema: {
|
|
147
147
|
type: 'object',
|
|
148
148
|
properties: {
|
|
@@ -222,21 +222,79 @@ class NullPayMcpService {
|
|
|
222
222
|
async login(args) {
|
|
223
223
|
const envMain = getMainWalletEnv();
|
|
224
224
|
const address = (args.address || envMain.address || '').trim();
|
|
225
|
-
|
|
225
|
+
let password = args.password || envMain.password || '';
|
|
226
226
|
const mainPrivateKey = args.main_private_key || envMain.privateKey || null;
|
|
227
|
-
if (!address
|
|
228
|
-
throw new Error('Address
|
|
227
|
+
if (!address) {
|
|
228
|
+
throw new Error('Address is required. You can pass it directly or set NULLPAY_MAIN_ADDRESS in env.');
|
|
229
229
|
}
|
|
230
230
|
const addressHash = (0, crypto_1.hashAddress)(address);
|
|
231
231
|
const existingProfile = await this.backend.getUserProfile(addressHash);
|
|
232
232
|
let encryptedMainAddress = existingProfile?.main_address || null;
|
|
233
|
+
let recoveredBackupSource = null;
|
|
234
|
+
let recoveredBurnerAddress = null;
|
|
235
|
+
let recoveredEncryptedBurnerKey = null;
|
|
236
|
+
let usedRecoveredPassword = false;
|
|
237
|
+
let restoredBurnerFromChain = false;
|
|
238
|
+
let chainBackupLoaded = false;
|
|
239
|
+
const loadChainBackup = async () => {
|
|
240
|
+
if (chainBackupLoaded || !mainPrivateKey) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
chainBackupLoaded = true;
|
|
244
|
+
const recovered = await (0, aleo_1.recoverOnChainWalletBackup)(mainPrivateKey, address);
|
|
245
|
+
if (!recovered) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
recoveredBackupSource = recovered.source;
|
|
249
|
+
recoveredBurnerAddress = recovered.burnerAddress || null;
|
|
250
|
+
recoveredEncryptedBurnerKey = recovered.encryptedBurnerKey || null;
|
|
251
|
+
return recovered;
|
|
252
|
+
};
|
|
253
|
+
const attemptRecovery = async () => {
|
|
254
|
+
const recovered = await loadChainBackup();
|
|
255
|
+
if (!recovered?.password) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
password = recovered.password;
|
|
259
|
+
usedRecoveredPassword = true;
|
|
260
|
+
return true;
|
|
261
|
+
};
|
|
233
262
|
if (encryptedMainAddress) {
|
|
234
|
-
|
|
263
|
+
if (!password) {
|
|
264
|
+
const recovered = await attemptRecovery();
|
|
265
|
+
if (!recovered) {
|
|
266
|
+
throw new Error('Password is required for this NullPay account. Set NULLPAY_MAIN_PASSWORD, pass password directly, or provide NULLPAY_MAIN_PRIVATE_KEY so the MCP can recover a backed-up password from on-chain records.');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
let decrypted;
|
|
270
|
+
try {
|
|
271
|
+
decrypted = await (0, crypto_1.decryptWithPassword)(encryptedMainAddress, password);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
const recovered = await attemptRecovery();
|
|
275
|
+
if (!recovered) {
|
|
276
|
+
throw new Error('Password is incorrect for this NullPay account, and no recoverable on-chain password backup was found for the provided main private key.');
|
|
277
|
+
}
|
|
278
|
+
decrypted = await (0, crypto_1.decryptWithPassword)(encryptedMainAddress, password);
|
|
279
|
+
}
|
|
235
280
|
if (decrypted !== address) {
|
|
236
|
-
|
|
281
|
+
const recovered = await attemptRecovery();
|
|
282
|
+
if (!recovered) {
|
|
283
|
+
throw new Error('Password is incorrect for this NullPay account.');
|
|
284
|
+
}
|
|
285
|
+
const recoveredAddress = await (0, crypto_1.decryptWithPassword)(encryptedMainAddress, password);
|
|
286
|
+
if (recoveredAddress !== address) {
|
|
287
|
+
throw new Error('Recovered password does not match the provided address.');
|
|
288
|
+
}
|
|
237
289
|
}
|
|
238
290
|
}
|
|
239
291
|
else {
|
|
292
|
+
if (!password) {
|
|
293
|
+
const recovered = await attemptRecovery();
|
|
294
|
+
if (!recovered) {
|
|
295
|
+
throw new Error('Password is required to create a new NullPay account unless the MCP can recover it from on-chain backup records using NULLPAY_MAIN_PRIVATE_KEY.');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
240
298
|
encryptedMainAddress = await (0, crypto_1.encryptWithPassword)(address, password);
|
|
241
299
|
}
|
|
242
300
|
if (!encryptedMainAddress) {
|
|
@@ -246,7 +304,21 @@ class NullPayMcpService {
|
|
|
246
304
|
let encryptedBurnerKey = existingProfile?.encrypted_burner_key || null;
|
|
247
305
|
let burnerAddress = null;
|
|
248
306
|
if (encryptedBurnerAddress) {
|
|
249
|
-
|
|
307
|
+
try {
|
|
308
|
+
burnerAddress = await (0, crypto_1.decryptWithPassword)(encryptedBurnerAddress, password);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
burnerAddress = null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!encryptedBurnerKey || !burnerAddress || !encryptedBurnerAddress) {
|
|
315
|
+
await loadChainBackup();
|
|
316
|
+
}
|
|
317
|
+
if ((!encryptedBurnerAddress || !encryptedBurnerKey || !burnerAddress) && recoveredBurnerAddress && recoveredEncryptedBurnerKey) {
|
|
318
|
+
burnerAddress = recoveredBurnerAddress;
|
|
319
|
+
encryptedBurnerAddress = await (0, crypto_1.encryptWithPassword)(recoveredBurnerAddress, password);
|
|
320
|
+
encryptedBurnerKey = recoveredEncryptedBurnerKey;
|
|
321
|
+
restoredBurnerFromChain = true;
|
|
250
322
|
}
|
|
251
323
|
if (args.create_burner_wallet && !encryptedBurnerKey) {
|
|
252
324
|
const { PrivateKey } = await (0, esm_1.dynamicImport)('@provablehq/sdk');
|
|
@@ -282,11 +354,17 @@ class NullPayMcpService {
|
|
|
282
354
|
});
|
|
283
355
|
const usedEnvCredentials = Boolean(envMain.address && envMain.password && !args.address && !args.password);
|
|
284
356
|
const lines = [
|
|
285
|
-
usedEnvCredentials ? 'Used main-wallet address and password from MCP env.' :
|
|
357
|
+
usedEnvCredentials ? 'Used main-wallet address and password from MCP env.' : 'Logged in as ' + address + '.',
|
|
286
358
|
encryptedBurnerKey
|
|
287
|
-
?
|
|
359
|
+
? 'Active wallet: ' + preferredWallet + '. Burner wallet is available' + (burnerAddress ? ' at ' + burnerAddress : '') + '.'
|
|
288
360
|
: 'Active wallet: main. No burner wallet is stored yet.',
|
|
289
361
|
];
|
|
362
|
+
if (usedRecoveredPassword) {
|
|
363
|
+
lines.push('Recovered your NullPay password from on-chain backup records using the main wallet private key (' + (recoveredBackupSource === 'full_burner' ? 'full burner backup' : 'password backup') + ').');
|
|
364
|
+
}
|
|
365
|
+
if (restoredBurnerFromChain) {
|
|
366
|
+
lines.push('Recovered your backed-up burner wallet from on-chain records and restored it into the MCP session.');
|
|
367
|
+
}
|
|
290
368
|
if (mainPrivateKey) {
|
|
291
369
|
lines.push('Main wallet private key is available for record-backed amount lookup and main-wallet payments. Active wallet is set to main by default, and you can switch to burner anytime by logging in again with wallet_preference set to burner. Invoice lookup will prefer the main wallet records even when you pay from burner.');
|
|
292
370
|
}
|
|
@@ -306,6 +384,9 @@ class NullPayMcpService {
|
|
|
306
384
|
has_main_private_key: Boolean(mainPrivateKey),
|
|
307
385
|
main_private_key_from_env: Boolean(envMain.privateKey),
|
|
308
386
|
used_env_credentials: usedEnvCredentials,
|
|
387
|
+
used_recovered_password: usedRecoveredPassword,
|
|
388
|
+
recovery_source: recoveredBackupSource,
|
|
389
|
+
restored_burner_from_chain: restoredBurnerFromChain,
|
|
309
390
|
},
|
|
310
391
|
};
|
|
311
392
|
}
|
package/package.json
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name":
|
|
3
|
-
"version":
|
|
4
|
-
"description":
|
|
5
|
-
"type":
|
|
6
|
-
"main":
|
|
7
|
-
"types":
|
|
8
|
-
"bin":
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"files":
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"scripts":
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"publishConfig":
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"dependencies":
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"devDependencies":
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
2
|
+
"name": "@nullpay/mcp",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "NullPay MCP server and Claude setup wizard for conversational payment flows",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"types": "dist/server.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"nullpay-mcp": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"start": "node dist/cli.js server",
|
|
18
|
+
"dev": "ts-node src/cli.ts server",
|
|
19
|
+
"setup": "ts-node src/cli.ts setup",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@provablehq/sdk": "^0.9.18",
|
|
27
|
+
"@provablehq/wasm": "^0.9.18"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.4.0",
|
|
31
|
+
"ts-node": "^10.9.2",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
}
|
|
34
34
|
}
|