402-mcp 4.1.2 → 4.2.1
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 +11 -3
- package/build/index.js +18 -2
- package/build/index.js.map +1 -1
- package/build/store/lnurlcash-notes.d.ts +37 -0
- package/build/store/lnurlcash-notes.js +25 -0
- package/build/store/lnurlcash-notes.js.map +1 -1
- package/build/tools/fetch-preview.d.ts +3 -0
- package/build/tools/fetch-preview.js +17 -1
- package/build/tools/fetch-preview.js.map +1 -1
- package/build/tools/fetch.d.ts +11 -0
- package/build/tools/fetch.js +49 -2
- package/build/tools/fetch.js.map +1 -1
- package/build/tools/pay.js +1 -1
- package/build/tools/pay.js.map +1 -1
- package/build/wallet/lnurlcash-ops.d.ts +91 -0
- package/build/wallet/lnurlcash-ops.js +198 -0
- package/build/wallet/lnurlcash-ops.js.map +1 -0
- package/build/wallet/lnurlcash.js +47 -119
- package/build/wallet/lnurlcash.js.map +1 -1
- package/build/wallet/types.d.ts +6 -0
- package/build/xlnurlcash/parse.d.ts +27 -0
- package/build/xlnurlcash/parse.js +71 -0
- package/build/xlnurlcash/parse.js.map +1 -0
- package/build/xlnurlcash/payment.d.ts +37 -0
- package/build/xlnurlcash/payment.js +102 -0
- package/build/xlnurlcash/payment.js.map +1 -0
- package/llms-full.txt +10 -3
- package/llms.txt +2 -1
- package/package.json +6 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { LnurlcashNoteStore, StoredNote, SettledMelt } from '../store/lnurlcash-notes.js';
|
|
2
|
+
/** What a reconcile pass turned up that a caller may want to act on. */
|
|
3
|
+
export interface ReconcileReport {
|
|
4
|
+
/**
|
|
5
|
+
* Melts that had already settled by the time we asked again, with the
|
|
6
|
+
* preimage recovered. Empty on a quiet pass. Each one is also written to
|
|
7
|
+
* the store, so nothing depends on the caller reading this.
|
|
8
|
+
*/
|
|
9
|
+
recovered: SettledMelt[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Note operations shared by the wallet provider (which melts a note to pay a
|
|
13
|
+
* BOLT-11 invoice) and the lnurlcash HTTP rail (which hands a note straight to
|
|
14
|
+
* a paywall). Both need the same crash-safe split and the same reconciliation,
|
|
15
|
+
* and two copies of that would drift.
|
|
16
|
+
*/
|
|
17
|
+
/** LUD-03/LUD-25 responses are either a success object or `{status:'ERROR'}`. */
|
|
18
|
+
export interface LnurlError {
|
|
19
|
+
status: 'ERROR';
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface WithdrawRequest {
|
|
23
|
+
status?: string;
|
|
24
|
+
tag?: string;
|
|
25
|
+
callback?: string;
|
|
26
|
+
k1?: string;
|
|
27
|
+
minWithdrawable?: number;
|
|
28
|
+
maxWithdrawable?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface CallbackOk {
|
|
31
|
+
status?: string;
|
|
32
|
+
verify?: string;
|
|
33
|
+
pr?: string;
|
|
34
|
+
reason?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function isError(res: unknown): res is LnurlError;
|
|
37
|
+
/**
|
|
38
|
+
* Mint responses are the only thing safe to surface. Anything thrown by the
|
|
39
|
+
* HTTP layer may carry a URL, and our URLs carry note secrets in the query
|
|
40
|
+
* string, so exceptions are never stringified outward.
|
|
41
|
+
*/
|
|
42
|
+
export declare class MintRefused extends Error {
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The mint doesn't recognise a note we thought we held. Expected, not
|
|
46
|
+
* exceptional: a bearer note can be spent by anyone holding a copy, so every
|
|
47
|
+
* local balance is a cache and the mint is the only authority. Signals "drop
|
|
48
|
+
* this one and try the next", never "fail the payment".
|
|
49
|
+
*/
|
|
50
|
+
export declare class NoteGone extends Error {
|
|
51
|
+
}
|
|
52
|
+
export interface NoteOpsOptions {
|
|
53
|
+
/** Per-request HTTP timeout. */
|
|
54
|
+
httpTimeoutMs?: number;
|
|
55
|
+
fetchImpl?: typeof fetch;
|
|
56
|
+
/** SSRF guard, matching farrier-kit's verifyLud21 hook. */
|
|
57
|
+
urlGuard?: (url: URL) => void | Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export interface ResolvedNote {
|
|
60
|
+
amountMsat: number;
|
|
61
|
+
callback: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `refused` is the mint saying it does not honour this note: unknown, already
|
|
65
|
+
* spent, or pending another operation. `unreachable` is the mint saying
|
|
66
|
+
* nothing at all. The difference matters because a note is only safe to drop
|
|
67
|
+
* on the first: a bearer note is money, and a flaky network must never destroy
|
|
68
|
+
* it.
|
|
69
|
+
*/
|
|
70
|
+
export type NoteProbe = {
|
|
71
|
+
ok: true;
|
|
72
|
+
note: ResolvedNote;
|
|
73
|
+
} | {
|
|
74
|
+
ok: false;
|
|
75
|
+
reason: 'refused' | 'unreachable';
|
|
76
|
+
};
|
|
77
|
+
export interface NoteOps {
|
|
78
|
+
probeNote(mint: string, secret: string): Promise<NoteProbe>;
|
|
79
|
+
resolveNote(mint: string, secret: string): Promise<ResolvedNote | null>;
|
|
80
|
+
reconcile(): Promise<ReconcileReport>;
|
|
81
|
+
split(parent: StoredNote, amountMsat: number): Promise<StoredNote>;
|
|
82
|
+
}
|
|
83
|
+
/** Appends params to a callback URL that may already carry some. */
|
|
84
|
+
export declare function callbackUrl(base: string, params: Record<string, string>): string;
|
|
85
|
+
/**
|
|
86
|
+
* The note as a LUD-25 withdraw URL: the secret as `k1`, its value alongside.
|
|
87
|
+
* This is the form a wallet shows and the form a paywall is handed. The
|
|
88
|
+
* `amount` is only a claim; the mint's `maxWithdrawable` is authoritative.
|
|
89
|
+
*/
|
|
90
|
+
export declare function noteUrl(note: Pick<StoredNote, 'mint' | 'secret' | 'amountMsat'>): string;
|
|
91
|
+
export declare function createNoteOps(store: LnurlcashNoteStore, options?: NoteOpsOptions): NoteOps;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { generatePreimage, computePaymentHash, fetchJson, verifyLud21 } from 'farrier-kit';
|
|
2
|
+
export function isError(res) {
|
|
3
|
+
return typeof res === 'object' && res !== null && res.status === 'ERROR';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Mint responses are the only thing safe to surface. Anything thrown by the
|
|
7
|
+
* HTTP layer may carry a URL, and our URLs carry note secrets in the query
|
|
8
|
+
* string, so exceptions are never stringified outward.
|
|
9
|
+
*/
|
|
10
|
+
export class MintRefused extends Error {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The mint doesn't recognise a note we thought we held. Expected, not
|
|
14
|
+
* exceptional: a bearer note can be spent by anyone holding a copy, so every
|
|
15
|
+
* local balance is a cache and the mint is the only authority. Signals "drop
|
|
16
|
+
* this one and try the next", never "fail the payment".
|
|
17
|
+
*/
|
|
18
|
+
export class NoteGone extends Error {
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 8_000;
|
|
21
|
+
/** Appends params to a callback URL that may already carry some. */
|
|
22
|
+
export function callbackUrl(base, params) {
|
|
23
|
+
const url = new URL(base);
|
|
24
|
+
for (const [k, v] of Object.entries(params))
|
|
25
|
+
url.searchParams.set(k, v);
|
|
26
|
+
return url.toString();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The note as a LUD-25 withdraw URL: the secret as `k1`, its value alongside.
|
|
30
|
+
* This is the form a wallet shows and the form a paywall is handed. The
|
|
31
|
+
* `amount` is only a claim; the mint's `maxWithdrawable` is authoritative.
|
|
32
|
+
*/
|
|
33
|
+
export function noteUrl(note) {
|
|
34
|
+
const url = new URL('/w', note.mint);
|
|
35
|
+
url.searchParams.set('k1', note.secret);
|
|
36
|
+
url.searchParams.set('amount', String(note.amountMsat));
|
|
37
|
+
return url.toString();
|
|
38
|
+
}
|
|
39
|
+
export function createNoteOps(store, options = {}) {
|
|
40
|
+
const httpTimeoutMs = options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
41
|
+
const get = async (url) => fetchJson(url, { timeoutMs: httpTimeoutMs, fetchImpl: options.fetchImpl });
|
|
42
|
+
/** Reads a note's current value from the mint, keeping why it failed. */
|
|
43
|
+
async function probeNote(mint, secret) {
|
|
44
|
+
let res;
|
|
45
|
+
try {
|
|
46
|
+
res = await get(callbackUrl(new URL('/w', mint).toString(), { k1: secret }));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { ok: false, reason: 'unreachable' };
|
|
50
|
+
}
|
|
51
|
+
if (isError(res) || typeof res.maxWithdrawable !== 'number' || typeof res.callback !== 'string') {
|
|
52
|
+
return { ok: false, reason: 'refused' };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, note: { amountMsat: res.maxWithdrawable, callback: res.callback } };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reads a note's current value from the mint. `null` means the mint doesn't
|
|
58
|
+
* recognise it: unknown, already spent, or pending another operation.
|
|
59
|
+
*/
|
|
60
|
+
async function resolveNote(mint, secret) {
|
|
61
|
+
const probe = await probeNote(mint, secret);
|
|
62
|
+
return probe.ok ? probe.note : null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Settles notes left in an indeterminate state by an earlier crash, a melt
|
|
66
|
+
* whose outcome never came back, or a note handed to a paywall that may or
|
|
67
|
+
* may not have rotated it. A provisional note the mint knows about means its
|
|
68
|
+
* split landed; one it doesn't means the split never happened. A melting note
|
|
69
|
+
* the mint still honours means the spend failed and the mint restored it.
|
|
70
|
+
*/
|
|
71
|
+
async function reconcile() {
|
|
72
|
+
const recovered = [];
|
|
73
|
+
for (const note of store.byState('provisional')) {
|
|
74
|
+
const resolved = await resolveNote(note.mint, note.secret);
|
|
75
|
+
if (resolved) {
|
|
76
|
+
store.confirm(note.secret, resolved.amountMsat);
|
|
77
|
+
// A landed split burned its parent, whatever our local copy says.
|
|
78
|
+
if (note.parent)
|
|
79
|
+
store.remove(note.parent);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
store.remove(note.secret);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// A melting note is asked about in a different order, and the order is
|
|
86
|
+
// the whole fix. Previously the mint was asked whether it still had the
|
|
87
|
+
// note, and a "no" meant remove: but "no" is exactly what a SETTLED melt
|
|
88
|
+
// looks like, because settling burns the note. The preimage the payment
|
|
89
|
+
// bought went in the bin with it.
|
|
90
|
+
//
|
|
91
|
+
// So the verify URL is consulted FIRST, while the note is still on disk.
|
|
92
|
+
// Only once settlement has been ruled out is the note's own fate asked
|
|
93
|
+
// about, and even then an unrecognised note with an unsettled verify URL
|
|
94
|
+
// is left alone rather than dropped: that is a melt still in flight.
|
|
95
|
+
for (const note of store.byState('melting')) {
|
|
96
|
+
if (note.verifyUrl && note.paymentHashHex) {
|
|
97
|
+
let result;
|
|
98
|
+
try {
|
|
99
|
+
result = await verifyLud21({
|
|
100
|
+
verifyUrl: note.verifyUrl,
|
|
101
|
+
paymentHashHex: note.paymentHashHex,
|
|
102
|
+
fetchImpl: options.fetchImpl,
|
|
103
|
+
timeoutMs: httpTimeoutMs,
|
|
104
|
+
...(options.urlGuard ? { urlGuard: options.urlGuard } : {}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// The mint said nothing. Nothing said is not a verdict, and this
|
|
109
|
+
// note is holding a payment that may have landed.
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (result?.settled) {
|
|
113
|
+
if (result.verified && result.preimage) {
|
|
114
|
+
const melt = {
|
|
115
|
+
paymentHashHex: note.paymentHashHex,
|
|
116
|
+
preimage: result.preimage,
|
|
117
|
+
amountMsat: note.amountMsat,
|
|
118
|
+
mint: note.mint,
|
|
119
|
+
settledAt: new Date().toISOString(),
|
|
120
|
+
};
|
|
121
|
+
store.recordSettledMelt(melt);
|
|
122
|
+
recovered.push(melt);
|
|
123
|
+
}
|
|
124
|
+
// Settled either way, so the note is spent and must not come back.
|
|
125
|
+
store.remove(note.secret);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// Not settled. If the mint still has the note the melt failed and it
|
|
129
|
+
// is spendable again; if it does not, the payment is still in flight
|
|
130
|
+
// and this note stays put with its URL for the next pass.
|
|
131
|
+
const resolved = await resolveNote(note.mint, note.secret);
|
|
132
|
+
if (resolved)
|
|
133
|
+
store.confirm(note.secret, resolved.amountMsat);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// No proof to consult - an older record, or a mint that offered no
|
|
137
|
+
// verify URL. Nothing better than the previous behaviour is available.
|
|
138
|
+
const resolved = await resolveNote(note.mint, note.secret);
|
|
139
|
+
if (resolved)
|
|
140
|
+
store.confirm(note.secret, resolved.amountMsat);
|
|
141
|
+
else
|
|
142
|
+
store.remove(note.secret);
|
|
143
|
+
}
|
|
144
|
+
return { recovered };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Splits `parent` into a note worth exactly `amountMsat` plus change, and
|
|
148
|
+
* returns the exact-value note. A melt invoice must match the note value
|
|
149
|
+
* exactly and a paywall must not be overpaid, so this is how an arbitrary
|
|
150
|
+
* amount gets covered.
|
|
151
|
+
*/
|
|
152
|
+
async function split(parent, amountMsat) {
|
|
153
|
+
const resolved = await resolveNote(parent.mint, parent.secret);
|
|
154
|
+
if (!resolved) {
|
|
155
|
+
store.remove(parent.secret);
|
|
156
|
+
throw new NoteGone('note not recognised by the mint');
|
|
157
|
+
}
|
|
158
|
+
const paySecret = generatePreimage();
|
|
159
|
+
const changeSecret = generatePreimage();
|
|
160
|
+
const addedAt = new Date().toISOString();
|
|
161
|
+
// Persisted BEFORE the request, deliberately. The mint keys the new notes
|
|
162
|
+
// to hashes of these secrets and never learns the secrets themselves, so a
|
|
163
|
+
// crash between its swap and our write would destroy the value outright.
|
|
164
|
+
store.addMany([
|
|
165
|
+
{ secret: paySecret, mint: parent.mint, amountMsat, state: 'provisional', parent: parent.secret, addedAt },
|
|
166
|
+
{ secret: changeSecret, mint: parent.mint, amountMsat: 0, state: 'provisional', parent: parent.secret, addedAt },
|
|
167
|
+
]);
|
|
168
|
+
let res;
|
|
169
|
+
try {
|
|
170
|
+
res = await get(callbackUrl(resolved.callback, {
|
|
171
|
+
k1: parent.secret,
|
|
172
|
+
amount: String(amountMsat),
|
|
173
|
+
h: computePaymentHash(paySecret),
|
|
174
|
+
h2: computePaymentHash(changeSecret),
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Outcome unknown. Leave both children provisional; reconcile() decides
|
|
179
|
+
// on the next call, once the mint can be asked again.
|
|
180
|
+
throw new MintRefused('Split outcome unknown; it will be reconciled on the next attempt');
|
|
181
|
+
}
|
|
182
|
+
if (isError(res)) {
|
|
183
|
+
// A refusal is synchronous and burns nothing, so the children are dead.
|
|
184
|
+
store.removeMany([paySecret, changeSecret]);
|
|
185
|
+
throw new MintRefused(res.reason ?? 'Mint refused the split');
|
|
186
|
+
}
|
|
187
|
+
store.remove(parent.secret);
|
|
188
|
+
store.confirm(paySecret, amountMsat);
|
|
189
|
+
const change = await resolveNote(parent.mint, changeSecret);
|
|
190
|
+
if (change)
|
|
191
|
+
store.confirm(changeSecret, change.amountMsat);
|
|
192
|
+
else
|
|
193
|
+
store.remove(changeSecret);
|
|
194
|
+
return store.find(paySecret);
|
|
195
|
+
}
|
|
196
|
+
return { probeNote, resolveNote, reconcile, split };
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=lnurlcash-ops.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lnurlcash-ops.js","sourceRoot":"","sources":["../../src/wallet/lnurlcash-ops.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,40 +1,22 @@
|
|
|
1
|
-
import { tryDecodeBolt11, bolt11AmountMsats,
|
|
1
|
+
import { tryDecodeBolt11, bolt11AmountMsats, verifyLud21, fetchJson, } from 'farrier-kit';
|
|
2
|
+
import { createNoteOps, callbackUrl, isError, MintRefused, NoteGone, } from './lnurlcash-ops.js';
|
|
2
3
|
const METHOD = 'lnurlcash';
|
|
3
4
|
const DEFAULTS = {
|
|
4
5
|
meltTimeoutMs: 60_000,
|
|
5
6
|
meltPollMs: 1_000,
|
|
6
7
|
httpTimeoutMs: 8_000,
|
|
7
8
|
};
|
|
8
|
-
function isError(res) {
|
|
9
|
-
return typeof res === 'object' && res !== null && res.status === 'ERROR';
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Mint responses are the only thing safe to surface. Anything thrown by the
|
|
13
|
-
* HTTP layer may carry a URL, and our URLs carry note secrets in the query
|
|
14
|
-
* string, so exceptions are never stringified outward.
|
|
15
|
-
*/
|
|
16
|
-
class MintRefused extends Error {
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* The mint doesn't recognise a note we thought we held. Expected, not
|
|
20
|
-
* exceptional: a bearer note can be spent by anyone holding a copy, so every
|
|
21
|
-
* local balance is a cache and the mint is the only authority. Signals "drop
|
|
22
|
-
* this one and try the next", never "fail the payment".
|
|
23
|
-
*/
|
|
24
|
-
class NoteGone extends Error {
|
|
25
|
-
}
|
|
26
9
|
function sleep(ms) {
|
|
27
10
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
28
11
|
}
|
|
29
|
-
/** Appends params to a callback URL that may already carry some. */
|
|
30
|
-
function callbackUrl(base, params) {
|
|
31
|
-
const url = new URL(base);
|
|
32
|
-
for (const [k, v] of Object.entries(params))
|
|
33
|
-
url.searchParams.set(k, v);
|
|
34
|
-
return url.toString();
|
|
35
|
-
}
|
|
36
12
|
export function createLnurlcashWallet(store, options = {}, lock) {
|
|
37
13
|
const cfg = { ...DEFAULTS, ...options };
|
|
14
|
+
const ops = createNoteOps(store, {
|
|
15
|
+
httpTimeoutMs: cfg.httpTimeoutMs,
|
|
16
|
+
fetchImpl: cfg.fetchImpl,
|
|
17
|
+
// reconcile polls verify URLs of its own now, so the guard has to reach it
|
|
18
|
+
...(cfg.urlGuard ? { urlGuard: cfg.urlGuard } : {}),
|
|
19
|
+
});
|
|
38
20
|
let internalLock = Promise.resolve();
|
|
39
21
|
const defaultLock = (fn) => {
|
|
40
22
|
const result = internalLock.catch(() => { }).then(() => fn());
|
|
@@ -46,95 +28,9 @@ export function createLnurlcashWallet(store, options = {}, lock) {
|
|
|
46
28
|
timeoutMs: cfg.httpTimeoutMs,
|
|
47
29
|
fetchImpl: cfg.fetchImpl,
|
|
48
30
|
});
|
|
49
|
-
/**
|
|
50
|
-
* Reads a note's current value from the mint. `null` means the mint doesn't
|
|
51
|
-
* recognise it: unknown, already spent, or pending another operation.
|
|
52
|
-
*/
|
|
53
|
-
async function resolveNote(mint, secret) {
|
|
54
|
-
let res;
|
|
55
|
-
try {
|
|
56
|
-
res = await get(callbackUrl(new URL('/w', mint).toString(), { k1: secret }));
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
if (isError(res) || typeof res.maxWithdrawable !== 'number' || typeof res.callback !== 'string')
|
|
62
|
-
return null;
|
|
63
|
-
return { amountMsat: res.maxWithdrawable, callback: res.callback };
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Settles notes left in an indeterminate state by an earlier crash or a melt
|
|
67
|
-
* whose outcome never came back. A provisional note the mint knows about
|
|
68
|
-
* means its split landed; one it doesn't means the split never happened. A
|
|
69
|
-
* melting note the mint still honours means the payment failed and the mint
|
|
70
|
-
* restored it.
|
|
71
|
-
*/
|
|
72
|
-
async function reconcile() {
|
|
73
|
-
for (const note of [...store.byState('provisional'), ...store.byState('melting')]) {
|
|
74
|
-
const resolved = await resolveNote(note.mint, note.secret);
|
|
75
|
-
if (resolved) {
|
|
76
|
-
store.confirm(note.secret, resolved.amountMsat);
|
|
77
|
-
// A landed split burned its parent, whatever our local copy says.
|
|
78
|
-
if (note.parent)
|
|
79
|
-
store.remove(note.parent);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
store.remove(note.secret);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Splits `parent` into a note worth exactly `amountMsat` plus change, and
|
|
88
|
-
* returns the exact-value note. The mint requires a melt invoice to match
|
|
89
|
-
* the note value exactly, so this is how an arbitrary invoice gets paid.
|
|
90
|
-
*/
|
|
91
|
-
async function split(parent, amountMsat) {
|
|
92
|
-
const resolved = await resolveNote(parent.mint, parent.secret);
|
|
93
|
-
if (!resolved) {
|
|
94
|
-
store.remove(parent.secret);
|
|
95
|
-
throw new NoteGone('note not recognised by the mint');
|
|
96
|
-
}
|
|
97
|
-
const paySecret = generatePreimage();
|
|
98
|
-
const changeSecret = generatePreimage();
|
|
99
|
-
const addedAt = new Date().toISOString();
|
|
100
|
-
// Persisted BEFORE the request, deliberately. The mint keys the new notes
|
|
101
|
-
// to hashes of these secrets and never learns the secrets themselves, so a
|
|
102
|
-
// crash between its swap and our write would destroy the value outright.
|
|
103
|
-
store.addMany([
|
|
104
|
-
{ secret: paySecret, mint: parent.mint, amountMsat, state: 'provisional', parent: parent.secret, addedAt },
|
|
105
|
-
{ secret: changeSecret, mint: parent.mint, amountMsat: 0, state: 'provisional', parent: parent.secret, addedAt },
|
|
106
|
-
]);
|
|
107
|
-
let res;
|
|
108
|
-
try {
|
|
109
|
-
res = await get(callbackUrl(resolved.callback, {
|
|
110
|
-
k1: parent.secret,
|
|
111
|
-
amount: String(amountMsat),
|
|
112
|
-
h: computePaymentHash(paySecret),
|
|
113
|
-
h2: computePaymentHash(changeSecret),
|
|
114
|
-
}));
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
// Outcome unknown. Leave both children provisional; reconcile() decides
|
|
118
|
-
// on the next call, once the mint can be asked again.
|
|
119
|
-
throw new MintRefused('Split outcome unknown; it will be reconciled on the next attempt');
|
|
120
|
-
}
|
|
121
|
-
if (isError(res)) {
|
|
122
|
-
// A refusal is synchronous and burns nothing, so the children are dead.
|
|
123
|
-
store.removeMany([paySecret, changeSecret]);
|
|
124
|
-
throw new MintRefused(res.reason ?? 'Mint refused the split');
|
|
125
|
-
}
|
|
126
|
-
store.remove(parent.secret);
|
|
127
|
-
store.confirm(paySecret, amountMsat);
|
|
128
|
-
const change = await resolveNote(parent.mint, changeSecret);
|
|
129
|
-
if (change)
|
|
130
|
-
store.confirm(changeSecret, change.amountMsat);
|
|
131
|
-
else
|
|
132
|
-
store.remove(changeSecret);
|
|
133
|
-
return store.find(paySecret);
|
|
134
|
-
}
|
|
135
31
|
/** Melts `note` against `invoice` and waits for cryptographic settlement proof. */
|
|
136
32
|
async function melt(note, invoice, paymentHashHex) {
|
|
137
|
-
const resolved = await resolveNote(note.mint, note.secret);
|
|
33
|
+
const resolved = await ops.resolveNote(note.mint, note.secret);
|
|
138
34
|
if (!resolved) {
|
|
139
35
|
store.remove(note.secret);
|
|
140
36
|
throw new NoteGone('note not recognised by the mint');
|
|
@@ -169,6 +65,12 @@ export function createLnurlcashWallet(store, options = {}, lock) {
|
|
|
169
65
|
reason: 'Mint accepted the melt but offers no LUD-21 verify URL, so settlement cannot be proven.',
|
|
170
66
|
};
|
|
171
67
|
}
|
|
68
|
+
// Persisted before the first poll, not after the last one. A melt that
|
|
69
|
+
// settles late still spends the note, and the verify URL is the only
|
|
70
|
+
// route back to the preimage it bought. Keeping it in a local variable
|
|
71
|
+
// meant a timeout - or a crash, or the process being killed - threw the
|
|
72
|
+
// credential away while the sats were already gone.
|
|
73
|
+
store.setMeltProof(note.secret, res.verify, paymentHashHex);
|
|
172
74
|
const deadline = Date.now() + cfg.meltTimeoutMs;
|
|
173
75
|
for (;;) {
|
|
174
76
|
let result;
|
|
@@ -185,10 +87,20 @@ export function createLnurlcashWallet(store, options = {}, lock) {
|
|
|
185
87
|
result = undefined;
|
|
186
88
|
}
|
|
187
89
|
if (result?.settled) {
|
|
188
|
-
store.remove(note.secret);
|
|
189
90
|
if (result.verified && result.preimage) {
|
|
91
|
+
// Recorded before the note goes, so a crash between the two lines
|
|
92
|
+
// cannot lose what the payment bought.
|
|
93
|
+
store.recordSettledMelt({
|
|
94
|
+
paymentHashHex,
|
|
95
|
+
preimage: result.preimage,
|
|
96
|
+
amountMsat: note.amountMsat,
|
|
97
|
+
mint: note.mint,
|
|
98
|
+
settledAt: new Date().toISOString(),
|
|
99
|
+
});
|
|
100
|
+
store.remove(note.secret);
|
|
190
101
|
return { paid: true, preimage: result.preimage, method: METHOD };
|
|
191
102
|
}
|
|
103
|
+
store.remove(note.secret);
|
|
192
104
|
// Settled per the mint, but unproven. Same stance as the NWC provider:
|
|
193
105
|
// the note is spent either way, so it must not go back in the store.
|
|
194
106
|
return {
|
|
@@ -203,7 +115,8 @@ export function createLnurlcashWallet(store, options = {}, lock) {
|
|
|
203
115
|
paid: false,
|
|
204
116
|
method: METHOD,
|
|
205
117
|
outcome: 'unknown',
|
|
206
|
-
|
|
118
|
+
verifyUrl: res.verify,
|
|
119
|
+
reason: 'Melt did not settle before the timeout. The note is held with its verify URL, so a later reconcile can still recover the preimage if the payment lands.',
|
|
207
120
|
};
|
|
208
121
|
}
|
|
209
122
|
await sleep(cfg.meltPollMs);
|
|
@@ -220,8 +133,20 @@ export function createLnurlcashWallet(store, options = {}, lock) {
|
|
|
220
133
|
if (needMsat === null) {
|
|
221
134
|
return { paid: false, method: METHOD, reason: 'Amountless BOLT-11 invoices require an explicit amount and are refused' };
|
|
222
135
|
}
|
|
136
|
+
// A reconcile can turn up a payment that landed after an earlier attempt
|
|
137
|
+
// gave up on it. That preimage is a credential somebody paid for, so it
|
|
138
|
+
// is said out loud here rather than only filed: the caller is a wallet
|
|
139
|
+
// asking to spend, and "by the way, that payment you wrote off did go
|
|
140
|
+
// through" changes what they do next.
|
|
141
|
+
let recoveredNote = '';
|
|
223
142
|
try {
|
|
224
|
-
await reconcile();
|
|
143
|
+
const report = await ops.reconcile();
|
|
144
|
+
if (report.recovered.length > 0) {
|
|
145
|
+
const each = report.recovered
|
|
146
|
+
.map(m => `${m.paymentHashHex.slice(0, 12)}… preimage ${m.preimage}`)
|
|
147
|
+
.join('; ');
|
|
148
|
+
recoveredNote = ` Also recovered ${report.recovered.length} earlier melt(s) that settled after being written off: ${each}.`;
|
|
149
|
+
}
|
|
225
150
|
}
|
|
226
151
|
catch { /* best effort; selection below simply sees fewer notes */ }
|
|
227
152
|
const live = store.live();
|
|
@@ -239,8 +164,11 @@ export function createLnurlcashWallet(store, options = {}, lock) {
|
|
|
239
164
|
try {
|
|
240
165
|
const exact = candidate.amountMsat === needMsat
|
|
241
166
|
? candidate
|
|
242
|
-
: await split(candidate, needMsat);
|
|
243
|
-
|
|
167
|
+
: await ops.split(candidate, needMsat);
|
|
168
|
+
const result = await melt(exact, invoice, decoded.paymentHashHex);
|
|
169
|
+
return recoveredNote
|
|
170
|
+
? { ...result, reason: `${result.reason ?? 'Paid.'}${recoveredNote}` }
|
|
171
|
+
: result;
|
|
244
172
|
}
|
|
245
173
|
catch (error) {
|
|
246
174
|
if (error instanceof NoteGone) {
|
|
@@ -259,7 +187,7 @@ export function createLnurlcashWallet(store, options = {}, lock) {
|
|
|
259
187
|
return {
|
|
260
188
|
paid: false,
|
|
261
189
|
method: METHOD,
|
|
262
|
-
reason: `No note large enough: need ${needMsat} msat, largest available is ${best} msat${staleNote}`,
|
|
190
|
+
reason: `No note large enough: need ${needMsat} msat, largest available is ${best} msat${staleNote}${recoveredNote}`,
|
|
263
191
|
};
|
|
264
192
|
}
|
|
265
193
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lnurlcash.js","sourceRoot":"","sources":["../../src/wallet/lnurlcash.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,
|
|
1
|
+
{"version":3,"file":"lnurlcash.js","sourceRoot":"","sources":["../../src/wallet/lnurlcash.ts"],"names":[],"mappings":"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"}
|
package/build/wallet/types.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface PaymentResult {
|
|
|
9
9
|
reason?: string;
|
|
10
10
|
/** The request may have executed but settlement could not be proven. */
|
|
11
11
|
outcome?: 'unknown';
|
|
12
|
+
/**
|
|
13
|
+
* Where settlement can still be proven later, for an `unknown` outcome
|
|
14
|
+
* that has one. A caller holding this can ask again rather than treating
|
|
15
|
+
* the payment as lost; reconcile also polls it on its own.
|
|
16
|
+
*/
|
|
17
|
+
verifyUrl?: string;
|
|
12
18
|
}
|
|
13
19
|
export interface WalletProvider {
|
|
14
20
|
method: WalletMethod;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface LnurlcashChallenge {
|
|
2
|
+
/** Price in sats. */
|
|
3
|
+
amount: number;
|
|
4
|
+
unit: 'sat';
|
|
5
|
+
/** Mints whose notes the server will accept, as hosts or URLs. */
|
|
6
|
+
mints: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Detects whether a 402 response carries an lnurlcash challenge. */
|
|
9
|
+
export declare function isLnurlcashChallenge(headers: Headers): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Parses an lnurlcash payment request from the X-LNURLcash header value:
|
|
12
|
+
* `lnurlcashreq1` followed by unpadded base64url of the request's RFC 8785
|
|
13
|
+
* canonical JSON.
|
|
14
|
+
*
|
|
15
|
+
* The request is `{ v, id, amount, currency, methodDetails: { mints } }`,
|
|
16
|
+
* with the amount a decimal STRING of whole sats. That is the shape the
|
|
17
|
+
* conformance vectors pin and `lnurlcash-kit` encodes.
|
|
18
|
+
*
|
|
19
|
+
* A shorter object, `{ a: number, u, m }`, briefly went out under the same
|
|
20
|
+
* prefix before the format settled. It is still read here, because a decoder
|
|
21
|
+
* that returns nothing on a string it can plainly understand helps no one,
|
|
22
|
+
* and because the kit's own decoder accepts it too. Nothing emits it.
|
|
23
|
+
*
|
|
24
|
+
* Only `sat` exists today; anything else returns null rather than guessing at
|
|
25
|
+
* a denomination.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseLnurlcashChallenge(header: string): LnurlcashChallenge | null;
|