@forgesworn/moneyer 0.14.0 → 0.16.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/CHANGELOG.md +35 -0
- package/README.md +17 -14
- package/dist/admin.js +1 -1
- package/dist/claim.d.ts +15 -0
- package/dist/claim.js +32 -0
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/fee-words.js +1 -1
- package/dist/landing.js +2 -2
- package/dist/live-check.d.ts +1 -1
- package/dist/live-check.js +3 -2
- package/dist/names.js +1 -1
- package/dist/server.js +20 -12
- package/dist/signing.js +2 -2
- package/dist/stats.js +1 -1
- package/dist/zap.js +25 -5
- package/llms.txt +6 -4
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 - 2026-09-16
|
|
4
|
+
|
|
5
|
+
- Bump `@lnurlcash/kit` from 0.14.2 to 0.18.1: the previous pin predated the
|
|
6
|
+
entire Schnorr `ck1` migration, so any note produced by a current wallet
|
|
7
|
+
was already unverifiable here. Brings the `ck1`/address-proof sha256
|
|
8
|
+
digest fix (signing `sha256(message)` instead of the raw string, matching
|
|
9
|
+
most conforming Schnorr signers) and a legacy-read fallback for notes
|
|
10
|
+
minted under the intermediate raw-message scheme.
|
|
11
|
+
- `spentRef` follows `recoverNoteOwnershipPubkey`'s new signature: it now
|
|
12
|
+
takes the `ck1` string directly and returns `{pubkeyXOnly, legacy}` rather
|
|
13
|
+
than raw recovered bytes.
|
|
14
|
+
- `claimMintedNote` now classifies a melt-in-flight note by catching the
|
|
15
|
+
kit's own `PendingNoteError`, replacing a `ServiceError{reason:'pending'}`
|
|
16
|
+
check the kit stopped throwing. The bump had silently broken this: an
|
|
17
|
+
unhandled `PendingNoteError` would have surfaced to a bound-mint quote's
|
|
18
|
+
poller as an unreachable-mint error rather than "pending" - untested
|
|
19
|
+
before this bump, now covered.
|
|
20
|
+
|
|
21
|
+
## 0.15.0 - 2026-09-15
|
|
22
|
+
|
|
23
|
+
- Use `@lnurlcash/kit` 0.14 as the protocol implementation. Removed kit
|
|
24
|
+
convenience APIs that are Moneyer policy remain local rather than becoming
|
|
25
|
+
changes to the reference wallet package.
|
|
26
|
+
- Match the current reference mint's mutation proof formats: legacy hash
|
|
27
|
+
outputs receive the raw 65-byte Part 1 signature when signing is available;
|
|
28
|
+
`cp1` outputs receive an amount-bearing `cs1` certificate. Informational
|
|
29
|
+
lookup still publishes a certificate only for `cp1` notes.
|
|
30
|
+
- Grade against `lnurlcash-conformance` 0.11, including amount-bearing
|
|
31
|
+
certificates, Part 2 address ownership and internal transfers.
|
|
32
|
+
- Publish a key-backed name's `cx1:<next-free-index>` as `text/xpub` metadata
|
|
33
|
+
so a wallet already holding Moneyer notes can pay it by an ordinary
|
|
34
|
+
rotate/split/merge without Lightning. The hint is derived from current
|
|
35
|
+
ledger occupancy and public-key output collisions receive the reference
|
|
36
|
+
retry signal.
|
|
37
|
+
|
|
3
38
|
## 0.14.0 - 2026-09-11
|
|
4
39
|
|
|
5
40
|
- **A plain note is unsigned.** LUD-25 Part 2 certifies `cp1` notes only:
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ split, merge and melt it against the withdraw callback. The note's spend
|
|
|
9
9
|
secret is chosen by the buyer, before the invoice exists, and named to the
|
|
10
10
|
mint only as a hash - so the mint never holds it, and neither does any node
|
|
11
11
|
that forwards the payment. It passes the full
|
|
12
|
-
[lnurlcash-conformance](https://github.com/
|
|
12
|
+
[lnurlcash-conformance](https://github.com/lnurlcash/lnurlcash-conformance)
|
|
13
13
|
grader, including the spending checks, and the grader runs in this repo's
|
|
14
14
|
own test suite.
|
|
15
15
|
|
|
@@ -43,14 +43,10 @@ lesson, that behaviour is kept deliberately and tested.
|
|
|
43
43
|
|
|
44
44
|
## Run
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
against until those publish:
|
|
46
|
+
Install the published package and build it:
|
|
48
47
|
|
|
49
48
|
```bash
|
|
50
|
-
git clone https://github.com/TheCryptoDonkey/lnurlcash-kit
|
|
51
|
-
git clone https://github.com/TheCryptoDonkey/lnurlcash-conformance
|
|
52
49
|
git clone https://github.com/forgesworn/moneyer
|
|
53
|
-
(cd lnurlcash-kit && npm install && npm run build)
|
|
54
50
|
cd moneyer && npm install && npm run build && npm run web:build
|
|
55
51
|
```
|
|
56
52
|
|
|
@@ -144,7 +140,7 @@ payRequest metadata's fee prose), `version`, and `previousPubkeys`.
|
|
|
144
140
|
|
|
145
141
|
`GET /` serves the mint's own site (`web/`, vite + anime.js, built by
|
|
146
142
|
`npm run web:build` into memory-served static files). It is a wallet-grade
|
|
147
|
-
LNURLcash client in its own right, driven entirely by
|
|
143
|
+
LNURLcash client in its own right, driven entirely by `@lnurlcash/kit`
|
|
148
144
|
against the same endpoints every wallet uses:
|
|
149
145
|
|
|
150
146
|
- **Mint a note in the browser**: amount in, fee grossed up and shown
|
|
@@ -292,7 +288,7 @@ single mint's implementation.
|
|
|
292
288
|
LUD-25 gives the fee as a flat `base_fee_msat` plus a ppm cut and says
|
|
293
289
|
nothing about rounding. dni's lnurl-mint, the reference, ceilings it to a
|
|
294
290
|
whole sat on purpose so the mint is never short a sat; moneyer withholds
|
|
295
|
-
the msat-exact amount. Neither is out of spec - lnurlcash
|
|
291
|
+
the msat-exact amount. Neither is out of spec - `@lnurlcash/kit`'s
|
|
296
292
|
`mintFeeBand` treats anything between the two as the mint keeping its
|
|
297
293
|
word, and the conformance grader accepts both.
|
|
298
294
|
|
|
@@ -354,8 +350,8 @@ named or spent, the two kinds mix freely:
|
|
|
354
350
|
- `GET /w?k1=<ck1>` or `GET /w?p=<cp1>` looks a Part 2 note up, and the
|
|
355
351
|
answer carries `sig`, the mint's certificate for it as `cs1`.
|
|
356
352
|
- `p1`/`p2` on the callback may be `cp1` keys, and the matching `sig`/`sig2`
|
|
357
|
-
come back as `cs1`. A hash output
|
|
358
|
-
|
|
353
|
+
come back as amount-bearing `cs1`. A legacy hash output instead receives
|
|
354
|
+
the raw 65-byte Part 1 signature used by the reference mint.
|
|
359
355
|
- A merge may combine Part 1 secrets and Part 2 `ck1`s in one request.
|
|
360
356
|
|
|
361
357
|
A recipient checks a Part 2 note offline from its `ck1` and `cs1` alone.
|
|
@@ -411,7 +407,7 @@ whose secret was a preimage could never offer.
|
|
|
411
407
|
|
|
412
408
|
The proposed normative wire text, invalid cases and compatibility matrix are
|
|
413
409
|
kept executable in
|
|
414
|
-
[`lnurlcash-conformance`](https://github.com/
|
|
410
|
+
[`lnurlcash-conformance`](https://github.com/lnurlcash/lnurlcash-conformance/blob/main/docs/BOUND-MINT-RECEIPTS.md).
|
|
415
411
|
|
|
416
412
|
## A retried mutation is answered, not refused
|
|
417
413
|
|
|
@@ -555,7 +551,7 @@ and credits the note to that key. It skips any key already holding a note,
|
|
|
555
551
|
and an invoice nobody pays takes no index at all, so a wallet scanning its
|
|
556
552
|
branch never meets a gap it did not make. The gift wrap still goes to the
|
|
557
553
|
owner's npub, but it carries no secret: a lookup URL,
|
|
558
|
-
`https://mint.example/w?p=<cp1>&
|
|
554
|
+
`https://mint.example/w?p=<cp1>&sig=<amount-bearing-cs1>&i=<index>`, plus an
|
|
559
555
|
`i` tag. The wallet derives the key at that index, checks the certificate,
|
|
560
556
|
and spends with its own `ck1`. A wallet that only knows note URLs sees no
|
|
561
557
|
`k1` and passes the wrap by, and the note waits at the mint for a scan of the
|
|
@@ -568,6 +564,13 @@ in `MONEYER_ZAP_NAMES`.
|
|
|
568
564
|
Sending a new branch starts it at index 0; sending the same one again keeps
|
|
569
565
|
its place. Zap receipts are unchanged.
|
|
570
566
|
|
|
567
|
+
The name's payRequest also publishes `['text/xpub', '<cx1>:<index>']`, where
|
|
568
|
+
the index is the next key not already present in this mint's ledger. A payer
|
|
569
|
+
already holding a note here can therefore rotate, split or merge straight to
|
|
570
|
+
that `cp1` without creating or routing a Lightning invoice. The index is a
|
|
571
|
+
hint: if another payment wins the key first, Moneyer returns `Output already
|
|
572
|
+
in use.` so the reference kit advances and retries.
|
|
573
|
+
|
|
571
574
|
## Reaching the mint over Tor
|
|
572
575
|
|
|
573
576
|
Set `MONEYER_ONION_URL` to this mint's hidden service address and a request
|
|
@@ -707,14 +710,14 @@ against live nodes; treat them as beta until they have.
|
|
|
707
710
|
## Dogfood
|
|
708
711
|
|
|
709
712
|
Part of the ForgeSworn LNURLcash stack: built on
|
|
710
|
-
[
|
|
713
|
+
[`@lnurlcash/kit`](https://github.com/lnurlcash/lnurl-wallet/tree/main/src/lib) (fee
|
|
711
714
|
maths, hashing, signature agreement) and
|
|
712
715
|
[`farrier-kit`](https://github.com/forgesworn/farrier-kit) (BOLT-11
|
|
713
716
|
decoding and preimage verification on the melt path). The companion wallet
|
|
714
717
|
is [`@forgesworn/notecase`](https://github.com/forgesworn/notecase).
|
|
715
718
|
|
|
716
719
|
Other mints, wallets and libraries speaking the same protocol are indexed
|
|
717
|
-
in [awesome-lnurlcash](https://github.com/
|
|
720
|
+
in [awesome-lnurlcash](https://github.com/lnurlcash/awesome-lnurlcash).
|
|
718
721
|
|
|
719
722
|
## Licence
|
|
720
723
|
|
package/dist/admin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
|
|
3
3
|
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
4
|
-
import { hashK1, noteDeclaredAmount, noteK1, noteSignature, verifyNoteSignature } from 'lnurlcash
|
|
4
|
+
import { hashK1, noteDeclaredAmount, noteK1, noteSignature, verifyNoteSignature } from '@lnurlcash/kit';
|
|
5
5
|
import { configFromEnv, pubkeyHex } from "./config.js";
|
|
6
6
|
import { NoteStore } from "./store.js";
|
|
7
7
|
import { buildStats } from "./stats.js";
|
package/dist/claim.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Result of checking the wallet-chosen secret behind a bound mint quote. */
|
|
2
|
+
export type MintClaim = {
|
|
3
|
+
state: 'minted' | 'unminted' | 'pending' | 'spent';
|
|
4
|
+
k1: string;
|
|
5
|
+
amountMsat: number | null;
|
|
6
|
+
callback: string | null;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Inspect the note a wallet named before requesting its invoice.
|
|
10
|
+
*
|
|
11
|
+
* This is Moneyer's operational orchestration, not a wire primitive: it
|
|
12
|
+
* classifies the read-only lookup so the live check can poll an unpaid quote
|
|
13
|
+
* without mistaking an unreachable mint for an unminted note.
|
|
14
|
+
*/
|
|
15
|
+
export declare const claimMintedNote: (withdrawLink: string, k1: string) => Promise<MintClaim>;
|
package/dist/claim.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NoteSpentError, NoteUnknownError, PendingNoteError, buildNoteUrl, fetchNoteInfo, isPreimage } from '@lnurlcash/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Inspect the note a wallet named before requesting its invoice.
|
|
4
|
+
*
|
|
5
|
+
* This is Moneyer's operational orchestration, not a wire primitive: it
|
|
6
|
+
* classifies the read-only lookup so the live check can poll an unpaid quote
|
|
7
|
+
* without mistaking an unreachable mint for an unminted note.
|
|
8
|
+
*/
|
|
9
|
+
export const claimMintedNote = async (withdrawLink, k1) => {
|
|
10
|
+
const secret = k1.trim().toLowerCase();
|
|
11
|
+
if (!isPreimage(secret))
|
|
12
|
+
throw new Error('A note secret must be 32 bytes of hex - nothing was sent.');
|
|
13
|
+
const blank = { k1: secret, amountMsat: null, callback: null };
|
|
14
|
+
try {
|
|
15
|
+
const info = await fetchNoteInfo(buildNoteUrl(withdrawLink, secret));
|
|
16
|
+
return {
|
|
17
|
+
state: 'minted',
|
|
18
|
+
k1: secret,
|
|
19
|
+
amountMsat: info.maxWithdrawable,
|
|
20
|
+
callback: info.callback
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof PendingNoteError)
|
|
25
|
+
return { ...blank, state: 'pending' };
|
|
26
|
+
if (error instanceof NoteSpentError)
|
|
27
|
+
return { ...blank, state: 'spent' };
|
|
28
|
+
if (error instanceof NoteUnknownError)
|
|
29
|
+
return { ...blank, state: 'unminted' };
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { bytesToHex, randomBytes } from '@noble/hashes/utils.js';
|
|
4
|
-
import { buildNoteUrl, hashK1 } from 'lnurlcash
|
|
4
|
+
import { buildNoteUrl, hashK1 } from '@lnurlcash/kit';
|
|
5
5
|
import { configFromEnv } from "./config.js";
|
|
6
6
|
import { createMoneyer } from "./server.js";
|
|
7
7
|
// moneyer - strike LNURLcash bearer notes.
|
package/dist/config.d.ts
CHANGED
package/dist/fee-words.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// The raw pair is spelled out deliberately. The machine-readable line
|
|
6
6
|
// beside it in the payRequest metadata is "Mint fees: 5000,1000" - the
|
|
7
|
-
// shape lnurlcash
|
|
7
|
+
// shape @lnurlcash/kit parses, anchored and digits-only, so it cannot
|
|
8
8
|
// carry its own units - and a reader who has not parsed LUD-25 reads
|
|
9
9
|
// both numbers as satoshis. A tester who thinks the fee is 5000 sats on
|
|
10
10
|
// a 10k sat mint has been told something alarming and false, so the
|
package/dist/landing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MINT_KNOWS, MINT_KNOWS_HEADING } from "./privacy.js";
|
|
2
2
|
import { feeInUnits } from "./fee-words.js";
|
|
3
|
-
import { applyMintFee } from 'lnurlcash
|
|
3
|
+
import { applyMintFee } from '@lnurlcash/kit';
|
|
4
4
|
// The mint's face: one self-contained page at GET /, no build step, no
|
|
5
5
|
// external assets. It states what a visitor needs before trusting a mint
|
|
6
6
|
// with sats: who it is, what it charges, the key its notes verify
|
|
@@ -101,7 +101,7 @@ ${config.tosUrl ? `<div class="kv"><span>terms</span><a href="${escapeHtml(confi
|
|
|
101
101
|
<h2>${escapeHtml(MINT_KNOWS_HEADING)}</h2>
|
|
102
102
|
${MINT_KNOWS.map(paragraph => `<p>${escapeHtml(paragraph)}</p>`).join('\n')}
|
|
103
103
|
</section>
|
|
104
|
-
<p class="small">Works with any LUD-25 wallet - <a href="https://github.com/forgesworn/notecase">notecase</a> among them. Verify a note offline against the signing key above.<br/>Independent implementation of the <a href="https://github.com/lnurl/luds/pull/301">LNURLcash draft</a> - graded by <a href="https://github.com/
|
|
104
|
+
<p class="small">Works with any LUD-25 wallet - <a href="https://github.com/forgesworn/notecase">notecase</a> among them. Verify a note offline against the signing key above.<br/>Independent implementation of the <a href="https://github.com/lnurl/luds/pull/301">LNURLcash draft</a> - graded by <a href="https://github.com/lnurlcash/lnurlcash-conformance">lnurlcash-conformance</a>.</p>
|
|
105
105
|
</main>
|
|
106
106
|
</body>
|
|
107
107
|
</html>`;
|
package/dist/live-check.d.ts
CHANGED
package/dist/live-check.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { lstat, open, readFile, rename, unlink } from 'node:fs/promises';
|
|
3
3
|
import { decodeBolt11 } from 'farrier-kit/bolt11';
|
|
4
|
-
import { NoteSpentError, PendingNoteError, buildNoteUrl,
|
|
4
|
+
import { NoteSpentError, PendingNoteError, buildNoteUrl, decodeBolt11AmountMsat, fetchInvoiceVerification, fetchPayRequest, hashK1, isBolt11Invoice, isPreimage, meltNote, probeBurnedNote, requestInvoice, requireBoundMintQuote, validateBoundMintReceipt, withinMintFeeBand } from '@lnurlcash/kit';
|
|
5
|
+
import { claimMintedNote } from "./claim.js";
|
|
5
6
|
const stages = new Set(['prepared', 'quoted', 'settled', 'claimed', 'retiring', 'retired']);
|
|
6
7
|
const errno = (error) => error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' ? error.code : undefined;
|
|
7
8
|
const serialise = (state) => `${JSON.stringify(state, null, 2)}\n`;
|
|
@@ -215,7 +216,7 @@ export const runLiveBoundMintCheck = async (options) => {
|
|
|
215
216
|
if (state.grossMsat < pay.minSendable || state.grossMsat > pay.maxSendable) {
|
|
216
217
|
throw new Error(`Test amount is outside the mint range ${pay.minSendable}-${pay.maxSendable} msat.`);
|
|
217
218
|
}
|
|
218
|
-
const quote = await requestInvoice(pay.callback, state.grossMsat,
|
|
219
|
+
const quote = await requestInvoice(pay.callback, state.grossMsat, state.h);
|
|
219
220
|
if (!quote.verify || !quote.mint)
|
|
220
221
|
throw new Error('Mint did not bind this quote to h and a verification URL.');
|
|
221
222
|
const netMsat = quote.mint.amountMsat;
|
package/dist/names.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
2
2
|
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
3
3
|
import { verifyEvent } from 'nostr-tools/pure';
|
|
4
|
-
import { decodeCx1, hashK1, noteK1 } from 'lnurlcash
|
|
4
|
+
import { decodeCx1, hashK1, noteK1 } from '@lnurlcash/kit';
|
|
5
5
|
import { NotePendingError, NoteStore, NoteUnavailableError } from "./store.js";
|
|
6
6
|
// Self-service lightning addresses.
|
|
7
7
|
//
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
|
|
3
3
|
import { finalizeEvent } from 'nostr-tools/pure';
|
|
4
|
-
import { applyMintFee,
|
|
4
|
+
import { applyMintFee, decodeCp1, encodeCs1WithAmount, grossUpForMintFee, hashK1, recoverNoteOwnershipPubkey } from '@lnurlcash/kit';
|
|
5
5
|
import { tryDecodeBolt11 } from 'farrier-kit/bolt11';
|
|
6
6
|
import { NotePendingError, NoteStore, NoteUnavailableError, OutputCollisionError, swapFingerprint } from "./store.js";
|
|
7
7
|
import { createNoteSigner } from "./signing.js";
|
|
@@ -32,9 +32,8 @@ const namedRef = (value) => {
|
|
|
32
32
|
const spentRef = (k1) => {
|
|
33
33
|
if (HEX32.test(k1))
|
|
34
34
|
return { id: hashK1(k1), cp1: false };
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
return pubkey ? { id: bytesToHex(pubkey), cp1: true } : null;
|
|
35
|
+
const owner = recoverNoteOwnershipPubkey(k1);
|
|
36
|
+
return owner ? { id: bytesToHex(owner.pubkeyXOnly), cp1: true } : null;
|
|
38
37
|
};
|
|
39
38
|
// LUD-25 renamed `h` to `p` on the lookup and `h`/`h2` to `p1`/`p2` on the
|
|
40
39
|
// callback. The old names stay accepted; sending both spellings must agree.
|
|
@@ -92,9 +91,13 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
92
91
|
const backend = deps.backend ?? backendFor(config);
|
|
93
92
|
const signer = createNoteSigner(config.signingKey);
|
|
94
93
|
// LUD-25 Part 2 signs cp1 notes only, as cs1. A hash output is a plain
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
94
|
+
// Match the reference mint's wire format: legacy hash outputs carry the
|
|
95
|
+
// compact signature as hex, while public-key outputs carry the same
|
|
96
|
+
// signature and amount in a cs1 certificate.
|
|
97
|
+
const certify = (ref, amountMsat) => {
|
|
98
|
+
const signature = signer.sign(ref.id, amountMsat);
|
|
99
|
+
return ref.cp1 ? encodeCs1WithAmount(amountMsat, hexToBytes(signature)) : signature;
|
|
100
|
+
};
|
|
98
101
|
const certified = (field, ref, amountMsat) => {
|
|
99
102
|
const sig = certify(ref, amountMsat);
|
|
100
103
|
return sig ? { [field]: sig } : {};
|
|
@@ -123,7 +126,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
123
126
|
if (!config.mintFee)
|
|
124
127
|
return 0;
|
|
125
128
|
const exact = grossMsat - applyMintFee(grossMsat, config.mintFee);
|
|
126
|
-
// Both readings sit inside lnurlcash
|
|
129
|
+
// Both readings sit inside @lnurlcash/kit's mintFeeBand, so a wallet
|
|
127
130
|
// is not misled either way - it is told the range up front.
|
|
128
131
|
//
|
|
129
132
|
// Absent means ON, the same as DEFAULTS and MONEYER_ROUND_FEE_TO_SAT:
|
|
@@ -280,7 +283,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
280
283
|
...(nodeInfo.uri ? { nodeUri: nodeInfo.uri } : {}),
|
|
281
284
|
...(nodeInfo.color ? { nodeColor: nodeInfo.color } : {}),
|
|
282
285
|
// nodeCapacity is the name the reference mint, the conformance
|
|
283
|
-
// mock and lnurlcash
|
|
286
|
+
// mock and @lnurlcash/kit all use; nodeCapacityMsat was ours alone
|
|
284
287
|
// and only survived a round trip through the kit's rest-spread.
|
|
285
288
|
// Both go out for one release, then the old name goes.
|
|
286
289
|
...(nodeInfo.capacityMsat !== undefined
|
|
@@ -929,9 +932,10 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
929
932
|
// exists.
|
|
930
933
|
...(mirrors.length ? { mirrors } : {}),
|
|
931
934
|
...(signer ? { mintPubkey: signer.pubkey } : {}),
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
+
// Part 2: a note named by cp1 or ck1 gets its certificate here, so
|
|
936
|
+
// a wallet need not rotate just to obtain one. The reference mint
|
|
937
|
+
// omits the legacy raw signature from this informational response.
|
|
938
|
+
...(ref.cp1 ? certified('sig', ref, note.amountMsat) : {})
|
|
935
939
|
});
|
|
936
940
|
}
|
|
937
941
|
// ---- the mutating callback: melt / rotate / split / merge ----
|
|
@@ -1141,6 +1145,8 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
1141
1145
|
catch (err) {
|
|
1142
1146
|
if (err instanceof NotePendingError)
|
|
1143
1147
|
return fail('pending');
|
|
1148
|
+
if (err instanceof OutputCollisionError && (o1.cp1 || o2.cp1))
|
|
1149
|
+
return fail('Output already in use.');
|
|
1144
1150
|
// OutputCollisionError shares the dead-k1 reason on purpose:
|
|
1145
1151
|
// which table the id collided with is an oracle nobody is owed.
|
|
1146
1152
|
return fail('Invalid or already spent k1.');
|
|
@@ -1162,6 +1168,8 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
1162
1168
|
catch (err) {
|
|
1163
1169
|
if (err instanceof NotePendingError)
|
|
1164
1170
|
return fail('pending');
|
|
1171
|
+
if (err instanceof OutputCollisionError && o1.cp1)
|
|
1172
|
+
return fail('Output already in use.');
|
|
1165
1173
|
// same oracle-free reason for a collision as for a dead k1
|
|
1166
1174
|
return fail('Invalid or already spent k1.');
|
|
1167
1175
|
}
|
package/dist/signing.js
CHANGED
|
@@ -6,7 +6,7 @@ import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
|
6
6
|
//
|
|
7
7
|
// "LNURLcash:" || amount_msat || ":" || note_id
|
|
8
8
|
//
|
|
9
|
-
// where note_id is hex(sha256(k1)). lnurlcash
|
|
9
|
+
// where note_id is hex(sha256(k1)). @lnurlcash/kit's noteSignatureDigest
|
|
10
10
|
// takes the SECRET and hashes it itself, which a wallet holds and a mint
|
|
11
11
|
// never does - so the digest is rebuilt here from the id. The two must
|
|
12
12
|
// agree byte for byte; the test suite proves they do by verifying every
|
|
@@ -21,7 +21,7 @@ export const signDigestRecoverable = (digest, privateKeyHex) => {
|
|
|
21
21
|
return bytesToHex(new Uint8Array([...lead.subarray(1), lead[0]]));
|
|
22
22
|
};
|
|
23
23
|
// Does this signature recover to that public key? Both byte orders are
|
|
24
|
-
// tried for the same reason lnurlcash
|
|
24
|
+
// tried for the same reason @lnurlcash/kit tries both: recovery-id-first is
|
|
25
25
|
// what noble emits, recovery-id-last is what the wire carries.
|
|
26
26
|
export const recoversToPubkey = (digest, signatureHex, pubkeyHex) => {
|
|
27
27
|
let wireSig;
|
package/dist/stats.js
CHANGED
|
@@ -79,7 +79,7 @@ export const verifyStatsSignature = (stats, signatureHex, mintPubkeyHex) => {
|
|
|
79
79
|
return false;
|
|
80
80
|
}
|
|
81
81
|
const target = mintPubkeyHex.trim().toLowerCase();
|
|
82
|
-
// Both byte orders are tried for the same reason lnurlcash
|
|
82
|
+
// Both byte orders are tried for the same reason @lnurlcash/kit tries
|
|
83
83
|
// both: recovery-id-first is what noble emits, recovery-id-last is what
|
|
84
84
|
// the wire carries.
|
|
85
85
|
const recoveryIdFirst = new Uint8Array([wireSig[64], ...wireSig.subarray(0, 64)]);
|
package/dist/zap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
|
|
2
|
-
import { decodeCx1, deriveNotePubkey, encodeCp1, hashK1 } from 'lnurlcash
|
|
2
|
+
import { decodeCx1, deriveNotePubkey, encodeCp1, hashK1 } from '@lnurlcash/kit';
|
|
3
3
|
import { tryDecodeBolt11 } from 'farrier-kit/bolt11';
|
|
4
4
|
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
|
|
5
5
|
import { SimplePool } from 'nostr-tools/pool';
|
|
@@ -78,11 +78,13 @@ export const inboxRelays = async (transport, pubkey, lookOn) => {
|
|
|
78
78
|
return [];
|
|
79
79
|
return [...new Set(latest.tags.filter(t => t[0] === 'relay' && t[1]).map(t => t[1]))];
|
|
80
80
|
};
|
|
81
|
-
const metadataFor = (name, host, mintFeeLine, feeInWords = null) => {
|
|
81
|
+
const metadataFor = (name, host, mintFeeLine, feeInWords = null, internalTransfer) => {
|
|
82
82
|
const metadata = [
|
|
83
83
|
['text/plain', `Zap ${name}@${host}: arrives as a Lightning bearer note${feeInWords ? ` (${feeInWords})` : ''}`],
|
|
84
84
|
['text/identifier', `${name}@${host}`]
|
|
85
85
|
];
|
|
86
|
+
if (internalTransfer)
|
|
87
|
+
metadata.push(['text/xpub', internalTransfer]);
|
|
86
88
|
if (mintFeeLine)
|
|
87
89
|
metadata.push(['text/plain', mintFeeLine]);
|
|
88
90
|
return JSON.stringify(metadata);
|
|
@@ -102,14 +104,32 @@ export const createZapBridge = (deps) => {
|
|
|
102
104
|
const isZapName = (name) => store.zapName(name.toLowerCase()) !== null;
|
|
103
105
|
const payRequest = (name) => {
|
|
104
106
|
const lowered = name.toLowerCase();
|
|
105
|
-
|
|
107
|
+
const registered = store.zapName(lowered);
|
|
108
|
+
if (!registered)
|
|
106
109
|
return null;
|
|
110
|
+
let internalTransfer;
|
|
111
|
+
const branch = decodeCx1(registered.cx1 ?? '');
|
|
112
|
+
if (branch) {
|
|
113
|
+
let index = registered.nextIndex;
|
|
114
|
+
for (let tries = 0; tries < 1000; tries++, index++) {
|
|
115
|
+
try {
|
|
116
|
+
const noteId = bytesToHex(deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, index));
|
|
117
|
+
if (!store.outputIdInUse(noteId)) {
|
|
118
|
+
internalTransfer = `${registered.cx1}:${index}`;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// A vanishingly rare unusable tweak consumes this public index.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
107
127
|
return {
|
|
108
128
|
tag: 'payRequest',
|
|
109
129
|
callback: `${origin}/z/cb/${lowered}`,
|
|
110
130
|
minSendable: deps.minSendableMsat,
|
|
111
131
|
maxSendable: deps.maxSendableMsat,
|
|
112
|
-
metadata: metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords),
|
|
132
|
+
metadata: metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords, internalTransfer),
|
|
113
133
|
allowsNostr: true,
|
|
114
134
|
nostrPubkey: pubkey
|
|
115
135
|
// Deliberately no withdrawLink: the preimage of this invoice is NOT
|
|
@@ -204,7 +224,7 @@ export const createZapBridge = (deps) => {
|
|
|
204
224
|
// the certificate, and the index `i` the holder derives the key at. A
|
|
205
225
|
// reader that knows only note URLs sees no k1 and passes it by, and the
|
|
206
226
|
// note waits at the mint for a scan to find it.
|
|
207
|
-
const buildKeyWrap = (row, noteId, index) => wrap(row, [...wrapTags(row), ['i', String(index)]], `${origin}/w?p=${encodeCp1(hexToBytes(noteId))}&
|
|
227
|
+
const buildKeyWrap = (row, noteId, index) => wrap(row, [...wrapTags(row), ['i', String(index)]], `${origin}/w?p=${encodeCp1(hexToBytes(noteId))}&sig=${deps.certify(noteId, row.netMsat)}&i=${index}`);
|
|
208
228
|
// NIP-57 receipt, without the preimage tag: it is optional there, and
|
|
209
229
|
// here it would only invite someone to mistake it for the note.
|
|
210
230
|
const buildReceipt = (row) => {
|
package/llms.txt
CHANGED
|
@@ -5,13 +5,13 @@ paying an invoice it issues mints a note whose spend secret is the
|
|
|
5
5
|
invoice's payment preimage, or a secret the buyer named with `h` on the
|
|
6
6
|
pay callback. ESM-only. MIT.
|
|
7
7
|
|
|
8
|
-
Install:
|
|
8
|
+
Install: npm install @forgesworn/moneyer
|
|
9
9
|
Run: node dist/cli.js --dev (fake funding source, prints a funded note)
|
|
10
10
|
|
|
11
11
|
Draft spec: https://github.com/lnurl/luds/pull/301
|
|
12
|
-
Conformance: https://github.com/
|
|
12
|
+
Conformance: https://github.com/lnurlcash/lnurlcash-conformance
|
|
13
13
|
Companion wallet: @forgesworn/notecase
|
|
14
|
-
Client library to build against a mint: lnurlcash
|
|
14
|
+
Client library to build against a mint: @lnurlcash/kit
|
|
15
15
|
|
|
16
16
|
## Rules this SERVICE follows (and any mint MUST)
|
|
17
17
|
|
|
@@ -95,5 +95,7 @@ POST /names claim a name (NIP-98; the signer owns it); body {nam
|
|
|
95
95
|
The owner may resend {name, cx1} to set a branch, or cx1: null to clear it
|
|
96
96
|
/z/cb/{name} zap invoice for a name. With a cx1 the settled note is credited to the
|
|
97
97
|
holder's next key, and the kind 2525 gift wrap carries no secret:
|
|
98
|
-
/w?p=<cp1>&
|
|
98
|
+
/w?p=<cp1>&sig=<amount-bearing-cs1>&i=<index> plus an `i` tag. The index is
|
|
99
99
|
taken at settlement, never at the invoice, and occupied keys are skipped
|
|
100
|
+
Its payRequest metadata publishes text/xpub = cx1:<next-free-index> for
|
|
101
|
+
same-mint rotate/split/merge transfers; a raced key returns "already in use"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "An LNURLcash (LUD-25) mint - strikes Lightning bearer notes. Independent implementation, cln/lnd funding sources, SQLite, zero HTTP framework.",
|
|
5
5
|
"author": "TheCryptoDonkey",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"live:bound-mint": "npm run build && node scripts/live-bound-mint-check.mjs"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"@lnurlcash/kit": "0.18.1",
|
|
59
60
|
"@noble/curves": "^2.3.0",
|
|
60
61
|
"@noble/hashes": "^2.3.0",
|
|
61
62
|
"@scure/base": "^1.2.4",
|
|
62
63
|
"farrier-kit": "^1.1.3",
|
|
63
|
-
"lnurlcash-kit": "^0.13.0",
|
|
64
64
|
"nostr-tools": "2.24.1"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@types/node": "^24.0.0",
|
|
71
71
|
"animejs": "^4.0.0",
|
|
72
72
|
"happy-dom": "^20.0.0",
|
|
73
|
-
"lnurlcash-conformance": "
|
|
73
|
+
"lnurlcash-conformance": "0.11.1",
|
|
74
74
|
"playwright": "^1.62.1",
|
|
75
75
|
"typescript": "^5.7.0",
|
|
76
76
|
"uqr": "^0.1.2",
|