@nullpay/mcp 1.0.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/README.md +49 -0
- package/dist/aleo.d.ts +67 -0
- package/dist/aleo.js +515 -0
- package/dist/backend-client.d.ts +45 -0
- package/dist/backend-client.js +93 -0
- package/dist/crypto.d.ts +3 -0
- package/dist/crypto.js +66 -0
- package/dist/esm.d.ts +1 -0
- package/dist/esm.js +7 -0
- package/dist/protocol.d.ts +19 -0
- package/dist/protocol.js +120 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +111 -0
- package/dist/service.d.ts +179 -0
- package/dist/service.js +620 -0
- package/dist/session-store.d.ts +9 -0
- package/dist/session-store.js +32 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.js +2 -0
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.js +66 -0
- package/dist/utils/env.d.ts +6 -0
- package/dist/utils/env.js +15 -0
- package/dist/utils/esm.d.ts +1 -0
- package/dist/utils/esm.js +7 -0
- package/dist/utils/formatters.d.ts +14 -0
- package/dist/utils/formatters.js +132 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @nullpay/mcp
|
|
2
|
+
|
|
3
|
+
NullPay MCP (Model Context Protocol) server for conversational invoice and payment flows.
|
|
4
|
+
|
|
5
|
+
This server allows AI agents to interact with the NullPay protocol, enabling them to create invoices, track payments, and manage merchant flows directly through chat.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Tool-based Interaction**: Exposes tools for creating NullPay invoices.
|
|
10
|
+
- **Privacy First**: Built on top of the Aleo blockchain with Zero-Knowledge Proofs.
|
|
11
|
+
- **Stdio Transport**: Compatible with MCP clients like Claude Desktop.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @nullpay/mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### As an MCP Server
|
|
22
|
+
|
|
23
|
+
Add the following to your MCP client configuration (e.g., `claude_desktop_config.json`):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"nullpay": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "@nullpay/mcp"],
|
|
31
|
+
"env": {
|
|
32
|
+
"NULLPAY_BACKEND_URL": "https://your-api.com/api",
|
|
33
|
+
"NULLPAY_MCP_SHARED_SECRET": "your-secret"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
The server requires the following environment variables:
|
|
43
|
+
|
|
44
|
+
- `NULLPAY_BACKEND_URL`: The base URL of your NullPay backend instance.
|
|
45
|
+
- `NULLPAY_MCP_SHARED_SECRET`: A shared secret to authenticate with the backend.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/dist/aleo.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Currency, InvoiceRecord, InvoiceStatusData, InvoiceType, ParsedOwnedInvoiceRecord } from './types';
|
|
2
|
+
export declare const PROGRAM_ID = "zk_pay_proofs_privacy_v22.aleo";
|
|
3
|
+
export declare function generateSalt(): string;
|
|
4
|
+
export declare function normalizeInvoiceHash(hash: string): string;
|
|
5
|
+
export declare function getInvoiceHashFromMapping(salt: string): Promise<string | null>;
|
|
6
|
+
export declare function waitForInvoiceHash(salt: string, attempts?: number, delayMs?: number): Promise<string>;
|
|
7
|
+
export declare function getInvoiceStatusData(hash: string): Promise<InvoiceStatusData | null>;
|
|
8
|
+
export declare function invoiceTypeToNumber(invoiceType: InvoiceType): number;
|
|
9
|
+
export declare function buildPaymentLink(baseUrl: string, args: {
|
|
10
|
+
merchant: string;
|
|
11
|
+
amount: number;
|
|
12
|
+
salt: string;
|
|
13
|
+
memo?: string;
|
|
14
|
+
invoiceType: InvoiceType;
|
|
15
|
+
currency: Currency;
|
|
16
|
+
invoiceHash: string;
|
|
17
|
+
}): string;
|
|
18
|
+
export declare function createInvoiceDbRecord(args: {
|
|
19
|
+
invoiceHash: string;
|
|
20
|
+
merchantAddress: string;
|
|
21
|
+
amount: number;
|
|
22
|
+
memo?: string;
|
|
23
|
+
invoiceType: InvoiceType;
|
|
24
|
+
currency: Currency;
|
|
25
|
+
salt: string;
|
|
26
|
+
invoiceTxId: string;
|
|
27
|
+
wallet: 'main' | 'burner';
|
|
28
|
+
lineItems?: Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
quantity: number;
|
|
31
|
+
unitPrice: number;
|
|
32
|
+
total: number;
|
|
33
|
+
}>;
|
|
34
|
+
merchantAddressHash: string;
|
|
35
|
+
}): {
|
|
36
|
+
invoice_hash: string;
|
|
37
|
+
merchant_address: string;
|
|
38
|
+
designated_address: string;
|
|
39
|
+
merchant_address_hash: string;
|
|
40
|
+
is_burner: boolean;
|
|
41
|
+
amount: number;
|
|
42
|
+
memo: string;
|
|
43
|
+
status: "PENDING";
|
|
44
|
+
invoice_transaction_id: string;
|
|
45
|
+
salt: string;
|
|
46
|
+
invoice_type: number;
|
|
47
|
+
token_type: number;
|
|
48
|
+
invoice_items: {
|
|
49
|
+
name: string;
|
|
50
|
+
quantity: number;
|
|
51
|
+
unitPrice: number;
|
|
52
|
+
total: number;
|
|
53
|
+
}[] | null;
|
|
54
|
+
for_sdk: boolean;
|
|
55
|
+
};
|
|
56
|
+
export declare function parseOwnedInvoiceRecord(plaintext: string): ParsedOwnedInvoiceRecord | null;
|
|
57
|
+
export declare function fetchOwnedInvoiceRecords(privateKey: string): Promise<ParsedOwnedInvoiceRecord[]>;
|
|
58
|
+
export declare function fetchOwnedInvoiceRecordByHash(privateKey: string, invoiceHash: string): Promise<ParsedOwnedInvoiceRecord | null>;
|
|
59
|
+
export declare function enrichInvoiceWithRecordAmount(invoice: InvoiceRecord, privateKey?: string | null): Promise<InvoiceRecord>;
|
|
60
|
+
export declare function createSponsoredPaymentAuthorization(args: {
|
|
61
|
+
walletPrivateKey: string;
|
|
62
|
+
invoice: InvoiceRecord;
|
|
63
|
+
amount?: number;
|
|
64
|
+
currency?: Exclude<Currency, 'ANY'>;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
authorization: string;
|
|
67
|
+
}>;
|
package/dist/aleo.js
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PROGRAM_ID = void 0;
|
|
7
|
+
exports.generateSalt = generateSalt;
|
|
8
|
+
exports.normalizeInvoiceHash = normalizeInvoiceHash;
|
|
9
|
+
exports.getInvoiceHashFromMapping = getInvoiceHashFromMapping;
|
|
10
|
+
exports.waitForInvoiceHash = waitForInvoiceHash;
|
|
11
|
+
exports.getInvoiceStatusData = getInvoiceStatusData;
|
|
12
|
+
exports.invoiceTypeToNumber = invoiceTypeToNumber;
|
|
13
|
+
exports.buildPaymentLink = buildPaymentLink;
|
|
14
|
+
exports.createInvoiceDbRecord = createInvoiceDbRecord;
|
|
15
|
+
exports.parseOwnedInvoiceRecord = parseOwnedInvoiceRecord;
|
|
16
|
+
exports.fetchOwnedInvoiceRecords = fetchOwnedInvoiceRecords;
|
|
17
|
+
exports.fetchOwnedInvoiceRecordByHash = fetchOwnedInvoiceRecordByHash;
|
|
18
|
+
exports.enrichInvoiceWithRecordAmount = enrichInvoiceWithRecordAmount;
|
|
19
|
+
exports.createSponsoredPaymentAuthorization = createSponsoredPaymentAuthorization;
|
|
20
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
21
|
+
const esm_1 = require("./esm");
|
|
22
|
+
exports.PROGRAM_ID = 'zk_pay_proofs_privacy_v22.aleo';
|
|
23
|
+
const FREEZELIST_PROGRAM_ID = 'test_usdcx_freezelist.aleo';
|
|
24
|
+
const EXPLORER_BASE = 'https://api.explorer.provable.com/v1';
|
|
25
|
+
const MAPPING_BASE = 'https://api.provable.com/v2/testnet/program';
|
|
26
|
+
const SCANNER_BASE = 'https://api.provable.com/scanner/testnet';
|
|
27
|
+
function generateSalt() {
|
|
28
|
+
const randomBuffer = crypto_1.default.randomBytes(16);
|
|
29
|
+
let randomBigInt = 0n;
|
|
30
|
+
for (const byte of randomBuffer) {
|
|
31
|
+
randomBigInt = (randomBigInt << 8n) + BigInt(byte);
|
|
32
|
+
}
|
|
33
|
+
return `${randomBigInt}field`;
|
|
34
|
+
}
|
|
35
|
+
function normalizeInvoiceHash(hash) {
|
|
36
|
+
return hash.trim().replace(/field$/, '');
|
|
37
|
+
}
|
|
38
|
+
function fieldToString(fieldVal) {
|
|
39
|
+
try {
|
|
40
|
+
const val = BigInt(fieldVal.replace('field', ''));
|
|
41
|
+
let hex = val.toString(16);
|
|
42
|
+
if (hex.length % 2 !== 0)
|
|
43
|
+
hex = '0' + hex;
|
|
44
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
45
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
46
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
47
|
+
}
|
|
48
|
+
return new TextDecoder().decode(bytes).replace(/\0/g, '');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function parseNumericValue(value) {
|
|
55
|
+
if (!value) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const normalized = value
|
|
59
|
+
.replace(/_/g, '')
|
|
60
|
+
.replace(/u64|u128|u8|field/g, '')
|
|
61
|
+
.trim();
|
|
62
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
63
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
64
|
+
}
|
|
65
|
+
async function getInvoiceHashFromMapping(salt) {
|
|
66
|
+
const response = await fetch(`${MAPPING_BASE}/${exports.PROGRAM_ID}/mapping/salt_to_invoice/${salt}`);
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const value = await response.json();
|
|
71
|
+
return value ? value.toString().replace(/(['"])/g, '') : null;
|
|
72
|
+
}
|
|
73
|
+
async function waitForInvoiceHash(salt, attempts = 60, delayMs = 2000) {
|
|
74
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
75
|
+
const hash = await getInvoiceHashFromMapping(salt);
|
|
76
|
+
if (hash) {
|
|
77
|
+
return hash;
|
|
78
|
+
}
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
80
|
+
}
|
|
81
|
+
throw new Error('Timed out while resolving invoice hash from mapping.');
|
|
82
|
+
}
|
|
83
|
+
async function getInvoiceStatusData(hash) {
|
|
84
|
+
const response = await fetch(`${MAPPING_BASE}/${exports.PROGRAM_ID}/mapping/invoices/${hash}`);
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
if (!data) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (typeof data === 'string') {
|
|
93
|
+
const statusMatch = data.match(/status:\s*(\d+)u8/);
|
|
94
|
+
const tokenMatch = data.match(/token_type:\s*(\d+)u8/);
|
|
95
|
+
const typeMatch = data.match(/invoice_type:\s*(\d+)u8/);
|
|
96
|
+
return {
|
|
97
|
+
status: statusMatch ? Number(statusMatch[1]) : 0,
|
|
98
|
+
tokenType: tokenMatch ? Number(tokenMatch[1]) : 0,
|
|
99
|
+
invoiceType: typeMatch ? Number(typeMatch[1]) : 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const objectData = data;
|
|
103
|
+
const parseValue = (value) => {
|
|
104
|
+
if (typeof value === 'number')
|
|
105
|
+
return value;
|
|
106
|
+
if (typeof value === 'string')
|
|
107
|
+
return Number(value.replace('u8', ''));
|
|
108
|
+
return 0;
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
status: parseValue(objectData.status),
|
|
112
|
+
tokenType: parseValue(objectData.token_type ?? objectData.tokenType),
|
|
113
|
+
invoiceType: parseValue(objectData.invoice_type ?? objectData.invoiceType),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function invoiceTypeToNumber(invoiceType) {
|
|
117
|
+
if (invoiceType === 'multipay')
|
|
118
|
+
return 1;
|
|
119
|
+
if (invoiceType === 'donation')
|
|
120
|
+
return 2;
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
function getTokenTypeNumber(currency) {
|
|
124
|
+
if (currency === 'USDCX')
|
|
125
|
+
return 1;
|
|
126
|
+
if (currency === 'USAD')
|
|
127
|
+
return 2;
|
|
128
|
+
if (currency === 'ANY')
|
|
129
|
+
return 3;
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
function buildPaymentLink(baseUrl, args) {
|
|
133
|
+
const url = new URL('/pay', baseUrl);
|
|
134
|
+
url.searchParams.set('merchant', args.merchant);
|
|
135
|
+
url.searchParams.set('amount', args.amount.toString());
|
|
136
|
+
url.searchParams.set('salt', args.salt);
|
|
137
|
+
url.searchParams.set('hash', args.invoiceHash);
|
|
138
|
+
if (args.memo)
|
|
139
|
+
url.searchParams.set('memo', args.memo);
|
|
140
|
+
if (args.invoiceType === 'multipay')
|
|
141
|
+
url.searchParams.set('type', 'multipay');
|
|
142
|
+
if (args.invoiceType === 'donation')
|
|
143
|
+
url.searchParams.set('type', 'donation');
|
|
144
|
+
if (args.currency === 'USDCX')
|
|
145
|
+
url.searchParams.set('token', 'usdcx');
|
|
146
|
+
if (args.currency === 'USAD')
|
|
147
|
+
url.searchParams.set('token', 'usad');
|
|
148
|
+
if (args.currency === 'ANY')
|
|
149
|
+
url.searchParams.set('token', 'any');
|
|
150
|
+
return url.toString();
|
|
151
|
+
}
|
|
152
|
+
function createInvoiceDbRecord(args) {
|
|
153
|
+
return {
|
|
154
|
+
invoice_hash: args.invoiceHash,
|
|
155
|
+
merchant_address: args.merchantAddress,
|
|
156
|
+
designated_address: args.merchantAddress,
|
|
157
|
+
merchant_address_hash: args.merchantAddressHash,
|
|
158
|
+
is_burner: args.wallet === 'burner',
|
|
159
|
+
amount: args.amount,
|
|
160
|
+
memo: args.memo || '',
|
|
161
|
+
status: 'PENDING',
|
|
162
|
+
invoice_transaction_id: args.invoiceTxId,
|
|
163
|
+
salt: args.salt,
|
|
164
|
+
invoice_type: invoiceTypeToNumber(args.invoiceType),
|
|
165
|
+
token_type: getTokenTypeNumber(args.currency),
|
|
166
|
+
invoice_items: args.lineItems || null,
|
|
167
|
+
for_sdk: false,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function resolvePaymentMode(invoice, fallbackAmount, fallbackCurrency) {
|
|
171
|
+
const invoiceType = invoice.invoice_type ?? 0;
|
|
172
|
+
const tokenType = invoice.token_type ?? (fallbackCurrency === 'USDCX' ? 1 : fallbackCurrency === 'USAD' ? 2 : 0);
|
|
173
|
+
const amountMajor = invoice.amount && invoice.amount > 0 ? Number(invoice.amount) : Number(fallbackAmount ?? 0);
|
|
174
|
+
const amountMicroFromInvoice = invoice.amount_micro && invoice.amount_micro > 0 ? BigInt(invoice.amount_micro) : null;
|
|
175
|
+
const amountMicro = amountMicroFromInvoice ?? BigInt(Math.round(amountMajor * 1000000));
|
|
176
|
+
const isDonation = invoiceType === 2;
|
|
177
|
+
if (!isDonation && amountMicro <= 0n) {
|
|
178
|
+
throw new Error('Invoice amount is missing. Add main wallet private key in env or pass amount explicitly.');
|
|
179
|
+
}
|
|
180
|
+
if (tokenType === 1) {
|
|
181
|
+
return {
|
|
182
|
+
invoiceType,
|
|
183
|
+
tokenType,
|
|
184
|
+
amountMicro,
|
|
185
|
+
functionName: isDonation ? 'pay_donation_usdcx' : 'pay_invoice_usdcx',
|
|
186
|
+
tokenProgram: 'test_usdcx_stablecoin.aleo',
|
|
187
|
+
amountSuffix: 'u128',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (tokenType === 2) {
|
|
191
|
+
return {
|
|
192
|
+
invoiceType,
|
|
193
|
+
tokenType,
|
|
194
|
+
amountMicro,
|
|
195
|
+
functionName: isDonation ? 'pay_donation_usad' : 'pay_invoice_usad',
|
|
196
|
+
tokenProgram: 'test_usad_stablecoin.aleo',
|
|
197
|
+
amountSuffix: 'u128',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
invoiceType,
|
|
202
|
+
tokenType,
|
|
203
|
+
amountMicro,
|
|
204
|
+
functionName: isDonation ? 'pay_donation' : 'pay_invoice',
|
|
205
|
+
tokenProgram: 'credits.aleo',
|
|
206
|
+
amountSuffix: 'u64',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function getProvableConsumerId() {
|
|
210
|
+
return process.env.PROVABLE_CONSUMER_ID || process.env.PROVABLE_CONSUMER_KEY;
|
|
211
|
+
}
|
|
212
|
+
async function getScannerSession(privateKey) {
|
|
213
|
+
const provableApiKey = process.env.PROVABLE_API_KEY;
|
|
214
|
+
const consumerId = getProvableConsumerId();
|
|
215
|
+
if (!provableApiKey || !consumerId) {
|
|
216
|
+
throw new Error('PROVABLE_API_KEY and PROVABLE_CONSUMER_ID/PROVABLE_CONSUMER_KEY are required for record fetching and payment automation.');
|
|
217
|
+
}
|
|
218
|
+
const jwtResponse = await fetch(`https://api.provable.com/jwts/${consumerId}`, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: { Accept: 'application/json', 'X-Provable-API-Key': provableApiKey },
|
|
221
|
+
});
|
|
222
|
+
if (!jwtResponse.ok) {
|
|
223
|
+
throw new Error(`Failed to fetch scanner JWT: ${jwtResponse.status}`);
|
|
224
|
+
}
|
|
225
|
+
const authHeader = jwtResponse.headers.get('authorization') || jwtResponse.headers.get('Authorization');
|
|
226
|
+
const scannerHeaders = { 'Content-Type': 'application/json' };
|
|
227
|
+
if (authHeader) {
|
|
228
|
+
scannerHeaders.Authorization = authHeader;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
scannerHeaders['X-Provable-API-Key'] = provableApiKey;
|
|
232
|
+
}
|
|
233
|
+
const { Account, encryptRegistrationRequest } = await (0, esm_1.dynamicImport)('@provablehq/sdk');
|
|
234
|
+
const account = new Account({ privateKey });
|
|
235
|
+
const pubkeyResponse = await fetch(`${SCANNER_BASE}/pubkey`, { method: 'GET', headers: scannerHeaders });
|
|
236
|
+
if (!pubkeyResponse.ok) {
|
|
237
|
+
throw new Error(`Failed to fetch scanner pubkey: ${pubkeyResponse.status}`);
|
|
238
|
+
}
|
|
239
|
+
const pubkey = await pubkeyResponse.json();
|
|
240
|
+
const ciphertext = encryptRegistrationRequest(pubkey.public_key, account.viewKey(), 0);
|
|
241
|
+
const registerResponse = await fetch(`${SCANNER_BASE}/register/encrypted`, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: scannerHeaders,
|
|
244
|
+
body: JSON.stringify({ key_id: pubkey.key_id, ciphertext }),
|
|
245
|
+
});
|
|
246
|
+
if (!registerResponse.ok) {
|
|
247
|
+
throw new Error(`Failed to register scanner session: ${registerResponse.status}`);
|
|
248
|
+
}
|
|
249
|
+
const registered = await registerResponse.json();
|
|
250
|
+
return {
|
|
251
|
+
scannerHeaders,
|
|
252
|
+
scannerUuid: registered.uuid,
|
|
253
|
+
account,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async function fetchOwnedProgramRecords(session, programFilter, options) {
|
|
257
|
+
const body = {
|
|
258
|
+
uuid: session.scannerUuid,
|
|
259
|
+
decrypt: true,
|
|
260
|
+
filter: { program: programFilter },
|
|
261
|
+
...(options?.unspent !== undefined ? { unspent: options.unspent } : {})
|
|
262
|
+
};
|
|
263
|
+
const response = await fetch(`${SCANNER_BASE}/records/owned`, {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: session.scannerHeaders,
|
|
266
|
+
body: JSON.stringify(body),
|
|
267
|
+
});
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
throw new Error(`Failed to fetch scanner records: ${response.status}`);
|
|
270
|
+
}
|
|
271
|
+
const data = await response.json();
|
|
272
|
+
return Array.isArray(data) ? data : (data.data || []);
|
|
273
|
+
}
|
|
274
|
+
function parseOwnedInvoiceRecord(plaintext) {
|
|
275
|
+
try {
|
|
276
|
+
const getVal = (key) => {
|
|
277
|
+
const regex = new RegExp(`(?:${key}|"${key}"):\\s*([\\w\\d\\.]+)`);
|
|
278
|
+
const match = plaintext.match(regex);
|
|
279
|
+
if (match && match[1]) {
|
|
280
|
+
return match[1].replace('.private', '').replace('.public', '');
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
};
|
|
284
|
+
const invoiceHash = getVal('invoice_hash') || getVal('invoiceHash');
|
|
285
|
+
const owner = getVal('owner');
|
|
286
|
+
const salt = getVal('salt');
|
|
287
|
+
const memoField = getVal('memo');
|
|
288
|
+
const invoiceTypeVal = getVal('invoice_type') || getVal('invoiceType');
|
|
289
|
+
if (!invoiceHash || !owner || !invoiceTypeVal) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const amountVal = getVal('amount');
|
|
293
|
+
const tokenTypeVal = getVal('token_type') || getVal('tokenType');
|
|
294
|
+
const walletTypeVal = getVal('wallet_type') || getVal('walletType');
|
|
295
|
+
return {
|
|
296
|
+
owner,
|
|
297
|
+
invoiceHash,
|
|
298
|
+
amountMicro: parseNumericValue(amountVal),
|
|
299
|
+
tokenType: parseNumericValue(tokenTypeVal),
|
|
300
|
+
invoiceType: parseNumericValue(invoiceTypeVal),
|
|
301
|
+
salt: salt || '',
|
|
302
|
+
memo: memoField ? fieldToString(memoField) : '',
|
|
303
|
+
walletType: parseNumericValue(walletTypeVal),
|
|
304
|
+
plaintext,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function fetchOwnedInvoiceRecords(privateKey) {
|
|
312
|
+
const session = await getScannerSession(privateKey);
|
|
313
|
+
const records = await fetchOwnedProgramRecords(session, exports.PROGRAM_ID);
|
|
314
|
+
const parsed = [];
|
|
315
|
+
for (const record of records) {
|
|
316
|
+
let plaintext = record.record_plaintext || record.plaintext || '';
|
|
317
|
+
if (!plaintext && record.record_ciphertext) {
|
|
318
|
+
const { RecordCiphertext } = await (0, esm_1.dynamicImport)('@provablehq/sdk');
|
|
319
|
+
const ciphertext = RecordCiphertext.fromString(record.record_ciphertext);
|
|
320
|
+
plaintext = ciphertext.decrypt(session.account.viewKey()).toString();
|
|
321
|
+
}
|
|
322
|
+
if (!plaintext)
|
|
323
|
+
continue;
|
|
324
|
+
const invoice = parseOwnedInvoiceRecord(plaintext);
|
|
325
|
+
if (invoice) {
|
|
326
|
+
parsed.push(invoice);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return parsed;
|
|
330
|
+
}
|
|
331
|
+
async function fetchOwnedInvoiceRecordByHash(privateKey, invoiceHash) {
|
|
332
|
+
const normalized = normalizeInvoiceHash(invoiceHash);
|
|
333
|
+
const records = await fetchOwnedInvoiceRecords(privateKey);
|
|
334
|
+
return records.find((record) => normalizeInvoiceHash(record.invoiceHash) === normalized) || null;
|
|
335
|
+
}
|
|
336
|
+
async function enrichInvoiceWithRecordAmount(invoice, privateKey) {
|
|
337
|
+
if (!privateKey || !invoice.invoice_transaction_id) {
|
|
338
|
+
return invoice;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const { Account } = await (0, esm_1.dynamicImport)('@provablehq/sdk');
|
|
342
|
+
const account = new Account({ privateKey });
|
|
343
|
+
const response = await fetch(`https://api.provable.com/v1/testnet/transaction/${invoice.invoice_transaction_id}`);
|
|
344
|
+
if (!response.ok)
|
|
345
|
+
return invoice;
|
|
346
|
+
// Cast it to `any` so we can safely traverse it without TS complaining
|
|
347
|
+
const tx = await response.json();
|
|
348
|
+
let transitions = [];
|
|
349
|
+
if (tx?.execution?.transitions) {
|
|
350
|
+
transitions = tx.execution.transitions;
|
|
351
|
+
}
|
|
352
|
+
else if (tx?.fee?.transition) {
|
|
353
|
+
transitions = [tx.fee.transition];
|
|
354
|
+
}
|
|
355
|
+
for (const transition of transitions) {
|
|
356
|
+
const outputs = transition?.outputs || [];
|
|
357
|
+
for (const output of outputs) {
|
|
358
|
+
if (output?.type === 'record') {
|
|
359
|
+
const ciphertext = output.value;
|
|
360
|
+
if (account.ownsRecordCiphertext(ciphertext)) {
|
|
361
|
+
const plaintextObj = account.decryptRecord(ciphertext);
|
|
362
|
+
const plaintextStr = typeof plaintextObj === 'string' ? plaintextObj : plaintextObj.toString();
|
|
363
|
+
const recordNode = parseOwnedInvoiceRecord(plaintextStr);
|
|
364
|
+
if (recordNode && recordNode.invoiceHash === invoice.invoice_hash) {
|
|
365
|
+
return {
|
|
366
|
+
...invoice,
|
|
367
|
+
amount_micro: recordNode.amountMicro,
|
|
368
|
+
amount: recordNode.amountMicro / 1000000,
|
|
369
|
+
token_type: invoice.token_type ?? recordNode.tokenType,
|
|
370
|
+
invoice_type: invoice.invoice_type ?? recordNode.invoiceType,
|
|
371
|
+
salt: invoice.salt || recordNode.salt,
|
|
372
|
+
memo: invoice.memo || recordNode.memo,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
console.error("Direct TX Enrichment Error:", e);
|
|
382
|
+
}
|
|
383
|
+
return invoice;
|
|
384
|
+
}
|
|
385
|
+
async function findSpendableRecord(session, programFilter, recordName, amountRequired, isCredits) {
|
|
386
|
+
const response = await fetch(`${SCANNER_BASE}/records/owned`, {
|
|
387
|
+
method: 'POST',
|
|
388
|
+
headers: session.scannerHeaders,
|
|
389
|
+
body: JSON.stringify({
|
|
390
|
+
uuid: session.scannerUuid,
|
|
391
|
+
unspent: true,
|
|
392
|
+
decrypt: true,
|
|
393
|
+
filter: { program: programFilter, record: recordName }
|
|
394
|
+
}),
|
|
395
|
+
});
|
|
396
|
+
if (!response.ok) {
|
|
397
|
+
throw new Error(`Failed to fetch scanner records: ${response.status}`);
|
|
398
|
+
}
|
|
399
|
+
const data = await response.json();
|
|
400
|
+
const records = Array.isArray(data) ? data : (data.data || []);
|
|
401
|
+
for (const record of records) {
|
|
402
|
+
const recordProgram = record.program_name || record.program || '';
|
|
403
|
+
if (recordProgram && recordProgram !== programFilter) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
let plaintext = record.record_plaintext || '';
|
|
407
|
+
if (!plaintext && record.record_ciphertext) {
|
|
408
|
+
const { RecordCiphertext } = await (0, esm_1.dynamicImport)('@provablehq/sdk');
|
|
409
|
+
const ciphertext = RecordCiphertext.fromString(record.record_ciphertext);
|
|
410
|
+
plaintext = ciphertext.decrypt(session.account.viewKey()).toString();
|
|
411
|
+
}
|
|
412
|
+
if (!plaintext) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (isCredits) {
|
|
416
|
+
const match = plaintext.match(/microcredits\s*:\s*([\d_]+)u64/);
|
|
417
|
+
if (match && BigInt(match[1].replace(/_/g, '')) >= amountRequired) {
|
|
418
|
+
return plaintext.trim();
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (/amount\s*:\s*[\d_]+u128/.test(plaintext) && !/invoice_hash/.test(plaintext)) {
|
|
423
|
+
const match = plaintext.match(/amount\s*:\s*([\d_]+)u128/);
|
|
424
|
+
if (match && BigInt(match[1].replace(/_/g, '')) >= amountRequired) {
|
|
425
|
+
return plaintext.trim();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
async function getFreezeListIndex(index) {
|
|
432
|
+
const { AleoNetworkClient } = await (0, esm_1.dynamicImport)('@provablehq/sdk');
|
|
433
|
+
const client = new AleoNetworkClient(EXPLORER_BASE);
|
|
434
|
+
const value = await client.getProgramMappingValue(FREEZELIST_PROGRAM_ID, 'freeze_list_index', `${index}u32`);
|
|
435
|
+
return value ? value.replace(/"/g, '') : null;
|
|
436
|
+
}
|
|
437
|
+
async function generateFreezeListProof(targetIndex = 1, occupiedLeafValue) {
|
|
438
|
+
const { Field, Poseidon4 } = await (0, esm_1.dynamicImport)('@provablehq/wasm');
|
|
439
|
+
const hasher = new Poseidon4();
|
|
440
|
+
const emptyHashes = [];
|
|
441
|
+
let currentEmpty = '0field';
|
|
442
|
+
for (let level = 0; level < 16; level += 1) {
|
|
443
|
+
emptyHashes.push(currentEmpty);
|
|
444
|
+
const field = Field.fromString(currentEmpty);
|
|
445
|
+
currentEmpty = hasher.hash([field, field]).toString();
|
|
446
|
+
}
|
|
447
|
+
let currentHash = '0field';
|
|
448
|
+
let currentIndex = targetIndex;
|
|
449
|
+
const proofSiblings = [];
|
|
450
|
+
for (let level = 0; level < 16; level += 1) {
|
|
451
|
+
const isLeft = currentIndex % 2 === 0;
|
|
452
|
+
const siblingIndex = isLeft ? currentIndex + 1 : currentIndex - 1;
|
|
453
|
+
let siblingHash = emptyHashes[level];
|
|
454
|
+
if (level === 0 && siblingIndex === 0 && occupiedLeafValue) {
|
|
455
|
+
siblingHash = occupiedLeafValue;
|
|
456
|
+
}
|
|
457
|
+
proofSiblings.push(siblingHash);
|
|
458
|
+
const left = Field.fromString(isLeft ? currentHash : siblingHash);
|
|
459
|
+
const right = Field.fromString(isLeft ? siblingHash : currentHash);
|
|
460
|
+
currentHash = hasher.hash([left, right]).toString();
|
|
461
|
+
currentIndex = Math.floor(currentIndex / 2);
|
|
462
|
+
}
|
|
463
|
+
return `[${proofSiblings.join(', ')}]`;
|
|
464
|
+
}
|
|
465
|
+
async function createSponsoredPaymentAuthorization(args) {
|
|
466
|
+
const session = await getScannerSession(args.walletPrivateKey);
|
|
467
|
+
const paymentMode = resolvePaymentMode(args.invoice, args.amount, args.currency);
|
|
468
|
+
if (!args.invoice.salt) {
|
|
469
|
+
throw new Error('Invoice is missing salt.');
|
|
470
|
+
}
|
|
471
|
+
const merchantAddress = args.invoice.designated_address || args.invoice.merchant_address;
|
|
472
|
+
if (!merchantAddress || !merchantAddress.startsWith('aleo')) {
|
|
473
|
+
throw new Error('Invoice merchant address is unavailable for automated payment.');
|
|
474
|
+
}
|
|
475
|
+
const record = await findSpendableRecord(session, paymentMode.tokenProgram, paymentMode.tokenProgram === 'credits.aleo' ? 'credits' : 'Token', paymentMode.amountMicro, paymentMode.tokenProgram === 'credits.aleo');
|
|
476
|
+
if (!record) {
|
|
477
|
+
throw new Error(`No spendable private record found for ${paymentMode.tokenProgram}.`);
|
|
478
|
+
}
|
|
479
|
+
let proofsInput;
|
|
480
|
+
if (paymentMode.tokenProgram !== 'credits.aleo') {
|
|
481
|
+
const firstIndex = await getFreezeListIndex(0);
|
|
482
|
+
let index0Field;
|
|
483
|
+
if (firstIndex) {
|
|
484
|
+
const { Address } = await (0, esm_1.dynamicImport)('@provablehq/wasm');
|
|
485
|
+
index0Field = Address.from_string(firstIndex).toGroup().toXCoordinate().toString();
|
|
486
|
+
}
|
|
487
|
+
const proof = await generateFreezeListProof(1, index0Field);
|
|
488
|
+
proofsInput = `[${proof}, ${proof}]`;
|
|
489
|
+
}
|
|
490
|
+
const { AleoKeyProvider, AleoNetworkClient, NetworkRecordProvider, ProgramManager } = await (0, esm_1.dynamicImport)('@provablehq/sdk');
|
|
491
|
+
const keyProvider = new AleoKeyProvider();
|
|
492
|
+
keyProvider.useCache(true);
|
|
493
|
+
const networkClient = new AleoNetworkClient(EXPLORER_BASE);
|
|
494
|
+
const recordProvider = new NetworkRecordProvider(session.account, networkClient);
|
|
495
|
+
const programManager = new ProgramManager(EXPLORER_BASE, keyProvider, recordProvider);
|
|
496
|
+
programManager.setAccount(session.account);
|
|
497
|
+
const paymentSecret = generateSalt();
|
|
498
|
+
const inputs = [
|
|
499
|
+
record,
|
|
500
|
+
merchantAddress,
|
|
501
|
+
`${paymentMode.amountMicro}${paymentMode.amountSuffix}`,
|
|
502
|
+
args.invoice.salt,
|
|
503
|
+
paymentSecret,
|
|
504
|
+
args.invoice.invoice_hash,
|
|
505
|
+
];
|
|
506
|
+
if (proofsInput) {
|
|
507
|
+
inputs.push(proofsInput);
|
|
508
|
+
}
|
|
509
|
+
const authorization = await programManager.buildAuthorization({
|
|
510
|
+
programName: exports.PROGRAM_ID,
|
|
511
|
+
functionName: paymentMode.functionName,
|
|
512
|
+
inputs,
|
|
513
|
+
});
|
|
514
|
+
return { authorization: authorization.toString() };
|
|
515
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { InvoiceRecord, UserProfile } from './types';
|
|
2
|
+
export declare class NullPayBackendClient {
|
|
3
|
+
private readonly baseUrl;
|
|
4
|
+
private readonly mcpSecret?;
|
|
5
|
+
constructor(baseUrl: string, mcpSecret?: string | undefined);
|
|
6
|
+
private buildUrl;
|
|
7
|
+
private request;
|
|
8
|
+
getUserProfile(addressHash: string): Promise<UserProfile | null>;
|
|
9
|
+
upsertUserProfile(body: {
|
|
10
|
+
address_hash: string;
|
|
11
|
+
main_address?: string;
|
|
12
|
+
burner_address?: string | null;
|
|
13
|
+
encrypted_burner_key?: string | null;
|
|
14
|
+
}): Promise<UserProfile>;
|
|
15
|
+
createInvoiceRow(body: Partial<InvoiceRecord>): Promise<InvoiceRecord>;
|
|
16
|
+
updateInvoice(hash: string, body: Partial<InvoiceRecord> & {
|
|
17
|
+
session_id?: string;
|
|
18
|
+
}): Promise<InvoiceRecord>;
|
|
19
|
+
updateCheckoutSession(id: string, body: {
|
|
20
|
+
status: 'SETTLED' | 'FAILED';
|
|
21
|
+
tx_id?: string;
|
|
22
|
+
}): Promise<Record<string, unknown>>;
|
|
23
|
+
getInvoice(hash: string): Promise<InvoiceRecord>;
|
|
24
|
+
getMerchantInvoices(merchantHash: string): Promise<InvoiceRecord[]>;
|
|
25
|
+
relayCreateInvoice(body: {
|
|
26
|
+
merchant_address: string;
|
|
27
|
+
amount: number;
|
|
28
|
+
currency: string;
|
|
29
|
+
salt: string;
|
|
30
|
+
memo?: string;
|
|
31
|
+
invoice_type?: number;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
success: boolean;
|
|
34
|
+
tx_id: string;
|
|
35
|
+
salt: string;
|
|
36
|
+
}>;
|
|
37
|
+
sponsorExecution(body: {
|
|
38
|
+
execution_authorization_string: string;
|
|
39
|
+
programName: string;
|
|
40
|
+
}): Promise<{
|
|
41
|
+
transaction?: {
|
|
42
|
+
id?: string;
|
|
43
|
+
};
|
|
44
|
+
}>;
|
|
45
|
+
}
|