nostr-bray 2.4.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/cli/commands/wallet.d.ts +2 -2
- package/dist/cli/commands/wallet.js +13 -11
- package/dist/cli/commands/zap.js +4 -2
- package/dist/cli/index.js +3 -3
- package/dist/config.js +46 -31
- package/dist/help.js +4 -4
- package/dist/marketplace/tools.js +9 -6
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +1 -1
- package/dist/zap/handlers.d.ts +48 -144
- package/dist/zap/handlers.js +177 -223
- package/dist/zap/index.d.ts +1 -0
- package/dist/zap/index.js +1 -0
- package/dist/zap/nwc-file.d.ts +5 -0
- package/dist/zap/nwc-file.js +65 -0
- package/dist/zap/tools.js +20 -15
- package/llms-full.txt +7 -6
- package/llms.txt +1 -1
- package/package.json +8 -3
package/dist/zap/handlers.js
CHANGED
|
@@ -1,50 +1,101 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { chmodSync, renameSync, unlinkSync, writeFileSync, mkdirSync, existsSync, } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
4
|
+
import { NwcClient, NwcError, inspectNwcConnection } from '@forgesworn/nwc-kit';
|
|
5
|
+
import { NwcTransactionHistoryClient } from '@forgesworn/nwc-kit/extensions/05';
|
|
6
|
+
import { tryDecodeBolt11, verifyPreimage } from 'farrier-kit';
|
|
7
|
+
import { readPrivateRegularFile, readNwcUriFile, } from './nwc-file.js';
|
|
8
|
+
const PUBKEY_HEX = /^[0-9a-f]{64}$/i;
|
|
9
|
+
const MAX_WALLETS_FILE_BYTES = 1_048_576;
|
|
10
|
+
const MAX_WALLETS = 128;
|
|
11
|
+
const MAX_PATH_CHARS = 4096;
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function validateWalletFiles(value) {
|
|
16
|
+
if (!isRecord(value) || Object.keys(value).length > MAX_WALLETS) {
|
|
17
|
+
throw new Error(`Wallet registry must contain at most ${MAX_WALLETS} entries`);
|
|
18
|
+
}
|
|
19
|
+
const walletFiles = {};
|
|
20
|
+
for (const [pubkey, filePath] of Object.entries(value)) {
|
|
21
|
+
if (!PUBKEY_HEX.test(pubkey) ||
|
|
22
|
+
typeof filePath !== 'string' ||
|
|
23
|
+
filePath.length === 0 ||
|
|
24
|
+
filePath.length > MAX_PATH_CHARS ||
|
|
25
|
+
!isAbsolute(filePath)) {
|
|
26
|
+
throw new Error('Wallet registry contains an invalid public key or secret-file path');
|
|
27
|
+
}
|
|
28
|
+
walletFiles[pubkey.toLowerCase()] = filePath;
|
|
29
|
+
}
|
|
30
|
+
return walletFiles;
|
|
31
|
+
}
|
|
5
32
|
/**
|
|
6
33
|
* Load the wallets map from the JSON file. Returns empty map if file missing.
|
|
7
34
|
*
|
|
8
35
|
* @param walletsFile - Absolute path to the JSON wallets store.
|
|
9
|
-
* @returns A map of public key hex strings to NWC
|
|
36
|
+
* @returns A map of public key hex strings to absolute NWC secret-file paths.
|
|
10
37
|
*
|
|
11
38
|
* @example
|
|
12
39
|
* const wallets = loadWallets('/home/user/.bray/wallets.json')
|
|
13
|
-
* // { 'abc123...': '
|
|
40
|
+
* // { 'abc123...': '/run/secrets/alice-nwc' }
|
|
14
41
|
*/
|
|
15
42
|
export function loadWallets(walletsFile) {
|
|
16
43
|
if (!walletsFile || !existsSync(walletsFile))
|
|
17
44
|
return {};
|
|
45
|
+
const bytes = readPrivateRegularFile(walletsFile, MAX_WALLETS_FILE_BYTES, 'Wallet registry');
|
|
46
|
+
let parsed;
|
|
18
47
|
try {
|
|
19
|
-
|
|
20
|
-
return data.wallets ?? {};
|
|
48
|
+
parsed = JSON.parse(bytes.toString('utf-8'));
|
|
21
49
|
}
|
|
22
50
|
catch {
|
|
23
|
-
|
|
51
|
+
throw new Error('Wallet registry is not valid JSON');
|
|
24
52
|
}
|
|
53
|
+
finally {
|
|
54
|
+
bytes.fill(0);
|
|
55
|
+
}
|
|
56
|
+
if (!isRecord(parsed) ||
|
|
57
|
+
parsed.version !== 1 ||
|
|
58
|
+
!('walletFiles' in parsed) ||
|
|
59
|
+
Object.keys(parsed).some((key) => key !== 'version' && key !== 'walletFiles')) {
|
|
60
|
+
throw new Error('Wallet registry has an unsupported or unsafe format');
|
|
61
|
+
}
|
|
62
|
+
return validateWalletFiles(parsed.walletFiles);
|
|
25
63
|
}
|
|
26
64
|
/**
|
|
27
65
|
* Save the wallets map to the JSON file. Creates parent dirs and sets 0600.
|
|
28
66
|
*
|
|
29
67
|
* @param walletsFile - Absolute path to the JSON wallets store.
|
|
30
|
-
* @param wallets - Map of public key hex strings to NWC
|
|
68
|
+
* @param wallets - Map of public key hex strings to absolute NWC secret-file paths.
|
|
31
69
|
* @returns void
|
|
32
70
|
*
|
|
33
71
|
* @example
|
|
34
72
|
* saveWallets('/home/user/.bray/wallets.json', {
|
|
35
|
-
* 'abc123...': '
|
|
73
|
+
* 'abc123...': '/run/secrets/alice-nwc',
|
|
36
74
|
* })
|
|
37
75
|
*/
|
|
38
76
|
export function saveWallets(walletsFile, wallets) {
|
|
77
|
+
if (!walletsFile)
|
|
78
|
+
throw new Error('Wallet registry path is not configured');
|
|
79
|
+
const validated = validateWalletFiles(wallets);
|
|
39
80
|
const dir = dirname(walletsFile);
|
|
40
81
|
if (!existsSync(dir))
|
|
41
82
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
42
|
-
const data = {
|
|
43
|
-
|
|
83
|
+
const data = { version: 1, walletFiles: validated };
|
|
84
|
+
const temporary = join(dir, `.${basename(walletsFile)}.${process.pid}.${randomUUID()}.tmp`);
|
|
85
|
+
try {
|
|
86
|
+
writeFileSync(temporary, JSON.stringify(data, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
87
|
+
chmodSync(temporary, 0o600);
|
|
88
|
+
renameSync(temporary, walletsFile);
|
|
89
|
+
chmodSync(walletsFile, 0o600);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
if (existsSync(temporary))
|
|
93
|
+
unlinkSync(temporary);
|
|
94
|
+
}
|
|
44
95
|
}
|
|
45
96
|
/**
|
|
46
97
|
* Resolve the NWC URI for the active identity.
|
|
47
|
-
* 1. Per-identity
|
|
98
|
+
* 1. Per-identity NWC secret file referenced by the wallet registry
|
|
48
99
|
* 2. Global NWC URI (fallback)
|
|
49
100
|
* 3. undefined (no wallet configured)
|
|
50
101
|
*
|
|
@@ -53,71 +104,36 @@ export function saveWallets(walletsFile, wallets) {
|
|
|
53
104
|
* @returns The resolved NWC URI, or `undefined` if no wallet is configured.
|
|
54
105
|
*
|
|
55
106
|
* @example
|
|
56
|
-
* const uri = resolveNwcUri(ctx, '/home/user/.bray/wallets.json',
|
|
107
|
+
* const uri = resolveNwcUri(ctx, '/home/user/.bray/wallets.json', configuredNwcUri)
|
|
57
108
|
* if (!uri) throw new Error('No wallet configured')
|
|
58
109
|
*/
|
|
59
110
|
export function resolveNwcUri(ctx, walletsFile, globalNwcUri) {
|
|
60
111
|
const pubkey = ctx.activePublicKeyHex;
|
|
61
112
|
const wallets = loadWallets(walletsFile);
|
|
62
|
-
|
|
113
|
+
const walletFile = wallets[pubkey.toLowerCase()];
|
|
114
|
+
return walletFile ? readNwcUriFile(walletFile) : globalNwcUri;
|
|
63
115
|
}
|
|
64
116
|
/**
|
|
65
117
|
* Parse a nostr+walletconnect:// URI.
|
|
66
118
|
*
|
|
67
119
|
* @param uri - A `nostr+walletconnect://` URI as specified in NIP-47.
|
|
68
|
-
* @returns
|
|
69
|
-
* @throws {Error} If
|
|
120
|
+
* @returns Public connection metadata. The connection secret is never returned.
|
|
121
|
+
* @throws {Error} If the URI is malformed or insecure.
|
|
70
122
|
*
|
|
71
123
|
* @example
|
|
72
124
|
* const conn = parseNwcUri(
|
|
73
125
|
* 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
|
|
74
126
|
* )
|
|
75
|
-
* // { pubkey: 'abc123', relay: 'wss://relay.example',
|
|
127
|
+
* // { pubkey: 'abc123', relay: 'wss://relay.example/', relays: ['wss://relay.example/'] }
|
|
76
128
|
*/
|
|
77
129
|
export function parseNwcUri(uri) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
return { pubkey, relay, secret };
|
|
86
|
-
}
|
|
87
|
-
/** Build an encrypted NIP-47 request event (kind 23194) */
|
|
88
|
-
function buildNwcRequest(conn, method, params) {
|
|
89
|
-
const secretBytes = Buffer.from(conn.secret, 'hex');
|
|
90
|
-
try {
|
|
91
|
-
const conversationKey = getConversationKey(secretBytes, conn.pubkey);
|
|
92
|
-
const content = encrypt(JSON.stringify({ method, params }), conversationKey);
|
|
93
|
-
const event = finalizeEvent({
|
|
94
|
-
kind: 23194,
|
|
95
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
96
|
-
tags: [['p', conn.pubkey]],
|
|
97
|
-
content,
|
|
98
|
-
}, secretBytes);
|
|
99
|
-
return event;
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
secretBytes.fill(0);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/** Decrypt a NIP-47 response event (kind 23195) */
|
|
106
|
-
function decryptNwcResponse(conn, event) {
|
|
107
|
-
const secretBytes = Buffer.from(conn.secret, 'hex');
|
|
108
|
-
try {
|
|
109
|
-
const conversationKey = getConversationKey(secretBytes, conn.pubkey);
|
|
110
|
-
const plaintext = decrypt(event.content, conversationKey);
|
|
111
|
-
try {
|
|
112
|
-
return JSON.parse(plaintext);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
throw new Error('NWC response is not valid JSON');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
finally {
|
|
119
|
-
secretBytes.fill(0);
|
|
120
|
-
}
|
|
130
|
+
const connection = inspectNwcConnection(uri);
|
|
131
|
+
return {
|
|
132
|
+
pubkey: connection.walletPubkey,
|
|
133
|
+
relay: connection.relays[0],
|
|
134
|
+
relays: connection.relays,
|
|
135
|
+
...(connection.lud16 ? { lud16: connection.lud16 } : {}),
|
|
136
|
+
};
|
|
121
137
|
}
|
|
122
138
|
/**
|
|
123
139
|
* Parse zap receipts (kind 9735) for the active identity.
|
|
@@ -167,184 +183,122 @@ function parseZapReceipt(event) {
|
|
|
167
183
|
* Decode basic bolt11 invoice fields.
|
|
168
184
|
*
|
|
169
185
|
* @param bolt11 - A BOLT-11 Lightning invoice string (e.g. `lnbc...`).
|
|
170
|
-
* @returns
|
|
171
|
-
*
|
|
172
|
-
* yet decoded and will be absent in the current implementation.
|
|
186
|
+
* @returns The bounded amount, description and expiry fields committed by the
|
|
187
|
+
* invoice. `amountMsats` is absent for an amountless invoice.
|
|
173
188
|
*
|
|
174
189
|
* @example
|
|
175
190
|
* const { amountMsats } = handleZapDecode('lnbc1000n1...')
|
|
176
191
|
* console.log(`Invoice is for ${amountMsats} msats`)
|
|
177
192
|
*/
|
|
178
193
|
export function handleZapDecode(bolt11) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const multipliers = {
|
|
185
|
-
'm': 100_000_000,
|
|
186
|
-
'u': 100_000,
|
|
187
|
-
'n': 100,
|
|
188
|
-
'p': 0.1,
|
|
189
|
-
};
|
|
190
|
-
if (multiplier && multipliers[multiplier]) {
|
|
191
|
-
result.amountMsats = Math.round(num * multipliers[multiplier]);
|
|
192
|
-
}
|
|
194
|
+
const decoded = tryDecodeBolt11(bolt11);
|
|
195
|
+
if (!decoded)
|
|
196
|
+
return {};
|
|
197
|
+
if (decoded.amountMsats !== null && decoded.amountMsats > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
198
|
+
throw new Error('BOLT-11 amount exceeds JavaScript safe integer range');
|
|
193
199
|
}
|
|
194
|
-
return
|
|
200
|
+
return {
|
|
201
|
+
...(decoded.amountMsats !== null ? { amountMsats: Number(decoded.amountMsats) } : {}),
|
|
202
|
+
...(decoded.description !== null ? { description: decoded.description } : {}),
|
|
203
|
+
expiry: decoded.expirySeconds,
|
|
204
|
+
};
|
|
195
205
|
}
|
|
196
|
-
// --- NWC Operations ---
|
|
197
206
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* @param args - Payment arguments.
|
|
201
|
-
* @param args.invoice - BOLT-11 Lightning invoice to pay.
|
|
202
|
-
* @param args.nwcUri - NWC URI identifying the wallet to use.
|
|
203
|
-
* @returns The signed NIP-47 request event and relay publish result.
|
|
204
|
-
* @throws {Error} If `nwcUri` is not provided.
|
|
205
|
-
*
|
|
206
|
-
* @example
|
|
207
|
-
* const { event, publish } = await handleZapSend(ctx, pool, {
|
|
208
|
-
* invoice: 'lnbc500n1...',
|
|
209
|
-
* nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
|
|
210
|
-
* })
|
|
207
|
+
* The wallet may have submitted a payment but Bray cannot prove settlement.
|
|
208
|
+
* Callers must reconcile `paymentHash` before attempting the invoice again.
|
|
211
209
|
*/
|
|
212
|
-
export
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
export class ZapPaymentOutcomeUnknownError extends Error {
|
|
211
|
+
paymentHash;
|
|
212
|
+
code = 'PAYMENT_OUTCOME_UNKNOWN';
|
|
213
|
+
constructor(paymentHash) {
|
|
214
|
+
super(`Payment outcome is unknown for ${paymentHash}. Reconcile the original invoice before retrying.`);
|
|
215
|
+
this.paymentHash = paymentHash;
|
|
216
|
+
this.name = 'ZapPaymentOutcomeUnknownError';
|
|
215
217
|
}
|
|
216
|
-
const conn = parseNwcUri(args.nwcUri);
|
|
217
|
-
const event = buildNwcRequest(conn, 'pay_invoice', { invoice: args.invoice });
|
|
218
|
-
const publish = await pool.publish(ctx.activeNpub, event);
|
|
219
|
-
return { event, publish };
|
|
220
218
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
* @param args - Balance request arguments.
|
|
225
|
-
* @param args.nwcUri - NWC URI identifying the wallet to query.
|
|
226
|
-
* @returns The signed NIP-47 `get_balance` request event and relay publish result.
|
|
227
|
-
* @throws {Error} If `nwcUri` is not provided.
|
|
228
|
-
*
|
|
229
|
-
* @example
|
|
230
|
-
* const { event, publish } = await handleZapBalance(ctx, pool, {
|
|
231
|
-
* nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
|
|
232
|
-
* })
|
|
233
|
-
*/
|
|
234
|
-
export async function handleZapBalance(ctx, pool, args) {
|
|
235
|
-
if (!args.nwcUri) {
|
|
236
|
-
throw new Error('Wallet not configured. Set NWC_URI or NWC_URI_FILE to check balance.');
|
|
219
|
+
function requireWallet(nwcUri, action) {
|
|
220
|
+
if (!nwcUri) {
|
|
221
|
+
throw new Error(`Wallet not configured. Set NWC_URI_FILE or connect a persona wallet file to ${action}.`);
|
|
237
222
|
}
|
|
238
|
-
|
|
239
|
-
const event = buildNwcRequest(conn, 'get_balance', {});
|
|
240
|
-
const publish = await pool.publish(ctx.activeNpub, event);
|
|
241
|
-
return { event, publish };
|
|
223
|
+
return nwcUri;
|
|
242
224
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
* @param args.description - Optional human-readable memo attached to the invoice.
|
|
249
|
-
* @param args.nwcUri - NWC URI identifying the wallet to use.
|
|
250
|
-
* @returns The signed NIP-47 `make_invoice` request event and relay publish result.
|
|
251
|
-
* @throws {Error} If `nwcUri` is not provided.
|
|
252
|
-
*
|
|
253
|
-
* @example
|
|
254
|
-
* const { event, publish } = await handleZapMakeInvoice(ctx, pool, {
|
|
255
|
-
* amountMsats: 21_000,
|
|
256
|
-
* description: 'Coffee ☕',
|
|
257
|
-
* nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
|
|
258
|
-
* })
|
|
259
|
-
*/
|
|
260
|
-
export async function handleZapMakeInvoice(ctx, pool, args) {
|
|
261
|
-
if (!args.nwcUri) {
|
|
262
|
-
throw new Error('Wallet not configured. Set NWC_URI or NWC_URI_FILE to create invoices.');
|
|
263
|
-
}
|
|
264
|
-
const conn = parseNwcUri(args.nwcUri);
|
|
265
|
-
const event = buildNwcRequest(conn, 'make_invoice', {
|
|
266
|
-
amount: args.amountMsats,
|
|
267
|
-
description: args.description,
|
|
268
|
-
});
|
|
269
|
-
const publish = await pool.publish(ctx.activeNpub, event);
|
|
270
|
-
return { event, publish };
|
|
225
|
+
function requestOptions(args) {
|
|
226
|
+
return {
|
|
227
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
228
|
+
...(args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {}),
|
|
229
|
+
};
|
|
271
230
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
* @returns The signed NIP-47 `lookup_invoice` request event and relay publish result.
|
|
280
|
-
* @throws {Error} If `nwcUri` is not provided.
|
|
281
|
-
*
|
|
282
|
-
* @example
|
|
283
|
-
* const { event, publish } = await handleZapLookupInvoice(ctx, pool, {
|
|
284
|
-
* paymentHash: 'a1b2c3...',
|
|
285
|
-
* nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
|
|
286
|
-
* })
|
|
287
|
-
*/
|
|
288
|
-
export async function handleZapLookupInvoice(ctx, pool, args) {
|
|
289
|
-
if (!args.nwcUri) {
|
|
290
|
-
throw new Error('Wallet not configured.');
|
|
231
|
+
async function withClient(nwcUri, transport, operation) {
|
|
232
|
+
const client = new NwcClient(nwcUri, { ...(transport ? { transport } : {}) });
|
|
233
|
+
try {
|
|
234
|
+
return await operation(client);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
client.close();
|
|
291
238
|
}
|
|
292
|
-
const conn = parseNwcUri(args.nwcUri);
|
|
293
|
-
const params = {};
|
|
294
|
-
if (args.paymentHash)
|
|
295
|
-
params.payment_hash = args.paymentHash;
|
|
296
|
-
if (args.invoice)
|
|
297
|
-
params.invoice = args.invoice;
|
|
298
|
-
const event = buildNwcRequest(conn, 'lookup_invoice', params);
|
|
299
|
-
const publish = await pool.publish(ctx.activeNpub, event);
|
|
300
|
-
return { event, publish };
|
|
301
239
|
}
|
|
302
|
-
/**
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
* @throws {Error} If `nwcUri` is not provided.
|
|
311
|
-
*
|
|
312
|
-
* @example
|
|
313
|
-
* const { event, publish } = await handleZapListTransactions(ctx, pool, {
|
|
314
|
-
* limit: 5,
|
|
315
|
-
* offset: 0,
|
|
316
|
-
* nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
|
|
317
|
-
* })
|
|
318
|
-
*/
|
|
319
|
-
export async function handleZapListTransactions(ctx, pool, args) {
|
|
320
|
-
if (!args.nwcUri) {
|
|
321
|
-
throw new Error('Wallet not configured.');
|
|
240
|
+
/** Pay a BOLT-11 invoice and return only after the wallet proves settlement. */
|
|
241
|
+
export async function handleZapSend(_ctx, _pool, args) {
|
|
242
|
+
const uri = requireWallet(args.nwcUri, 'enable payments');
|
|
243
|
+
const decoded = tryDecodeBolt11(args.invoice);
|
|
244
|
+
if (!decoded)
|
|
245
|
+
throw new Error('Invalid BOLT-11 invoice');
|
|
246
|
+
if (decoded.amountMsats === null) {
|
|
247
|
+
throw new Error('Amountless BOLT-11 invoices are refused because no explicit payment amount was supplied');
|
|
322
248
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
249
|
+
if (decoded.amountMsats > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
250
|
+
throw new Error('BOLT-11 amount exceeds JavaScript safe integer range');
|
|
251
|
+
}
|
|
252
|
+
let paid;
|
|
253
|
+
try {
|
|
254
|
+
paid = await withClient(uri, args.transport, (client) => client.payInvoice({ invoice: args.invoice }, requestOptions(args)));
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
// An authenticated wallet rejection is definitive. Transport, timeout,
|
|
258
|
+
// abort, and malformed-response failures can happen after submission.
|
|
259
|
+
if (error instanceof NwcError && error.code === 'WALLET_ERROR')
|
|
260
|
+
throw error;
|
|
261
|
+
throw new ZapPaymentOutcomeUnknownError(decoded.paymentHashHex);
|
|
262
|
+
}
|
|
263
|
+
if (!verifyPreimage(paid.preimage, decoded.paymentHashHex)) {
|
|
264
|
+
throw new ZapPaymentOutcomeUnknownError(decoded.paymentHashHex);
|
|
265
|
+
}
|
|
266
|
+
return { ...paid, paymentHash: decoded.paymentHashHex, verified: true };
|
|
330
267
|
}
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
268
|
+
/** Return the wallet's confirmed balance in millisatoshis. */
|
|
269
|
+
export async function handleZapBalance(_ctx, _pool, args) {
|
|
270
|
+
const uri = requireWallet(args.nwcUri, 'check the balance');
|
|
271
|
+
return withClient(uri, args.transport, (client) => client.getBalance(requestOptions(args)));
|
|
272
|
+
}
|
|
273
|
+
/** Generate a Lightning invoice through the configured wallet. */
|
|
274
|
+
export async function handleZapMakeInvoice(_ctx, _pool, args) {
|
|
275
|
+
const uri = requireWallet(args.nwcUri, 'create invoices');
|
|
276
|
+
return withClient(uri, args.transport, (client) => client.makeInvoice({
|
|
277
|
+
amount: args.amountMsats,
|
|
278
|
+
...(args.description !== undefined ? { description: args.description } : {}),
|
|
279
|
+
}, requestOptions(args)));
|
|
280
|
+
}
|
|
281
|
+
/** Look up an invoice and return the wallet's authenticated result. */
|
|
282
|
+
export async function handleZapLookupInvoice(_ctx, _pool, args) {
|
|
283
|
+
const uri = requireWallet(args.nwcUri, 'look up invoices');
|
|
284
|
+
return withClient(uri, args.transport, (client) => client.lookupInvoice({
|
|
285
|
+
...(args.paymentHash !== undefined ? { payment_hash: args.paymentHash } : {}),
|
|
286
|
+
...(args.invoice !== undefined ? { invoice: args.invoice } : {}),
|
|
287
|
+
}, requestOptions(args)));
|
|
288
|
+
}
|
|
289
|
+
/** List recent transactions through the isolated NIP-47 extension 05 client. */
|
|
290
|
+
export async function handleZapListTransactions(_ctx, _pool, args) {
|
|
291
|
+
const uri = requireWallet(args.nwcUri, 'list transactions');
|
|
292
|
+
const client = new NwcTransactionHistoryClient(uri, {
|
|
293
|
+
...(args.transport ? { transport: args.transport } : {}),
|
|
294
|
+
});
|
|
295
|
+
try {
|
|
296
|
+
return await client.listTransactions({
|
|
297
|
+
limit: args.limit ?? 10,
|
|
298
|
+
offset: args.offset ?? 0,
|
|
299
|
+
}, requestOptions(args));
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
client.close();
|
|
303
|
+
}
|
|
350
304
|
}
|
package/dist/zap/index.d.ts
CHANGED
package/dist/zap/index.js
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function readPrivateRegularFile(filePath: string, maximumBytes: number, label: string): Buffer;
|
|
2
|
+
/** Read and validate an NWC connection from a private local secret file. */
|
|
3
|
+
export declare function readNwcUriFile(filePath: string): string;
|
|
4
|
+
/** Resolve and validate a secret-file path before adding it to the registry. */
|
|
5
|
+
export declare function normaliseNwcUriFile(filePath: string): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { inspectNwcConnection } from '@forgesworn/nwc-kit';
|
|
4
|
+
const MAX_NWC_URI_BYTES = 8192;
|
|
5
|
+
const MAX_PATH_CHARS = 4096;
|
|
6
|
+
export function readPrivateRegularFile(filePath, maximumBytes, label) {
|
|
7
|
+
const descriptor = openSync(filePath, 'r');
|
|
8
|
+
let bytes;
|
|
9
|
+
try {
|
|
10
|
+
const stats = fstatSync(descriptor);
|
|
11
|
+
if (!stats.isFile() || stats.size === 0 || stats.size > maximumBytes) {
|
|
12
|
+
throw new Error(`${label} must be a non-empty regular file no larger than ${maximumBytes} bytes`);
|
|
13
|
+
}
|
|
14
|
+
if (process.platform !== 'win32' && (stats.mode & 0o077) !== 0) {
|
|
15
|
+
throw new Error(`${label} permissions are too broad; run chmod 600 on the file`);
|
|
16
|
+
}
|
|
17
|
+
bytes = Buffer.allocUnsafe(stats.size);
|
|
18
|
+
let offset = 0;
|
|
19
|
+
while (offset < bytes.length) {
|
|
20
|
+
const count = readSync(descriptor, bytes, offset, bytes.length - offset, null);
|
|
21
|
+
if (count === 0)
|
|
22
|
+
throw new Error(`${label} changed while it was being read`);
|
|
23
|
+
offset += count;
|
|
24
|
+
}
|
|
25
|
+
const extra = Buffer.allocUnsafe(1);
|
|
26
|
+
try {
|
|
27
|
+
if (readSync(descriptor, extra, 0, 1, null) !== 0) {
|
|
28
|
+
throw new Error(`${label} changed while it was being read`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
extra.fill(0);
|
|
33
|
+
}
|
|
34
|
+
return bytes;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
bytes?.fill(0);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
closeSync(descriptor);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Read and validate an NWC connection from a private local secret file. */
|
|
45
|
+
export function readNwcUriFile(filePath) {
|
|
46
|
+
if (typeof filePath !== 'string' || filePath.length === 0 || filePath.length > MAX_PATH_CHARS) {
|
|
47
|
+
throw new Error('NWC URI file path is missing or too long');
|
|
48
|
+
}
|
|
49
|
+
const absolutePath = resolve(filePath);
|
|
50
|
+
const bytes = readPrivateRegularFile(absolutePath, MAX_NWC_URI_BYTES, 'NWC URI file');
|
|
51
|
+
try {
|
|
52
|
+
const uri = bytes.toString('utf-8').trim();
|
|
53
|
+
inspectNwcConnection(uri);
|
|
54
|
+
return uri;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
bytes.fill(0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Resolve and validate a secret-file path before adding it to the registry. */
|
|
61
|
+
export function normaliseNwcUriFile(filePath) {
|
|
62
|
+
const absolutePath = resolve(filePath);
|
|
63
|
+
readNwcUriFile(absolutePath);
|
|
64
|
+
return absolutePath;
|
|
65
|
+
}
|