@forgesworn/moneyer 0.5.0 → 0.6.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 CHANGED
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0] - 2026-08-24
4
+
5
+ - **Bound mint settlement receipts.** A pay callback asked to mint at a
6
+ wallet-chosen `h` now commits the quote to `mint: {h, amount}` whenever
7
+ signing and LUD-21 verification are available. Before settlement the
8
+ response is unsigned; after settlement `/verify` repeats the commitment
9
+ and adds the ordinary LUD-25 note `sig`. This lets a sealed signer promote
10
+ a staged output without exporting its secret. No-`h`, unsigned and
11
+ verify-disabled deployments retain their previous response shapes.
12
+ - The payRequest publishes `mintPubkey` when note signing is enabled, so a
13
+ receipt-aware wallet can establish the verification key before paying.
14
+ - The bundled web client stages a wallet-chosen secret before requesting the
15
+ invoice, validates the quote and settled receipt, and falls back before
16
+ payment to the existing preimage-import-and-rotate flow when the optional
17
+ receipt is unavailable.
18
+
19
+ - **A standing proof-of-concept disclaimer.** moneyer is an evaluation
20
+ implementation of a draft spec, its funding backends are beta, and a
21
+ note is bearer value with no recovery - so the site now says so at the
22
+ top, before anyone pays, rather than leaving it to a threat model
23
+ nobody opens. A prominent banner on the home page and the mint flow,
24
+ and on the fallback landing page: "Proof of concept · for developers …
25
+ assume you can lose anything you put in", with a link to the operator's
26
+ terms when `tosUrl` is set. Always on, because it is true of every
27
+ moneyer deployment as the spec stands. The per-step `warn` notes and
28
+ the operator MOTD are unchanged and sit alongside it.
29
+ - **The fee says its own units.** `Mint fees: 5000,1000` is the shape
30
+ LUD-25 wallets parse - lnurlcash-kit matches it anchored and
31
+ digits-only, so it cannot carry units - and testers were reading both
32
+ numbers as satoshis. On a 10k sat mint that reads as a 5000 sat fee,
33
+ which is alarming and false. The prose beside it now spells the pair
34
+ out: `fee 5 sat + 0.1% (5000 msat + 1000 ppm), rounded up to the sat`.
35
+ The wire format is untouched.
36
+ - The fee prose moves to `src/fee-words.ts`, the way `privacy.ts` already
37
+ holds the privacy statement, so the payRequest metadata and the
38
+ landing page cannot drift apart on it. The landing page had grown its
39
+ own second version reading `5000 msat flat + 0.1%`; it now uses the
40
+ shared one. `describeFee` is still exported from `server.ts`.
41
+ - **Operator guidance on the custodial position.** `DEPLOY.md` gains a
42
+ "Before you run this" section: the regulatory perimeter follows the
43
+ operator rather than the code, how to configure an evaluation mint so
44
+ it reads as one, why **not** to zero the fee to make that point (a
45
+ fee-free mint pays the melt routing floor out of its own balance and is
46
+ griefable by mint-and-melt cycling - see `THREAT-MODEL.md`), and the
47
+ no-fiat invariant. moneyer is millisatoshis throughout, and that is
48
+ load-bearing rather than incidental.
49
+ - `TERMS-TEMPLATE.md`, a starting point for `MONEYER_TOS_URL`. It defers
50
+ to the mint's own "what the mint knows" statement rather than restating
51
+ it, for the same no-drift reason.
52
+ - **The regulatory guidance grows its missing teeth.** `DEPLOY.md`'s
53
+ "Before you run this" covered the payments perimeter and stopped there;
54
+ it now walks the UK regimes that actually bear on a custodial mint:
55
+ FCA registration under the Money Laundering Regulations 2017 (custody
56
+ is the live limb, and the travel rule the structural one - an anonymous
57
+ bearer instrument cannot carry originator and beneficiary information),
58
+ the financial promotion rules that gate marketing for an unregistered
59
+ operator (`MONEYER_ANNOUNCE` and the site copy both), sanctions, which
60
+ bind regardless of classification, and the cryptoasset custody regime
61
+ the 2025 draft legislation is bringing. `TERMS-TEMPLATE.md` gains the
62
+ liability floor consumer law insists on, a sanctions prohibited-use
63
+ clause, and a privacy notice in outline.
64
+
3
65
  ## [0.5.0] - 2026-08-23
4
66
 
5
67
  - **Both doors, named.** With `MONEYER_ONION_URL` and
package/README.md CHANGED
@@ -66,6 +66,19 @@ node dist/cli.js
66
66
  The mint is then payable at `mint@mint.example` (and the bare-domain `_`
67
67
  alias).
68
68
 
69
+ **Before you point one at mainnet**, read [Before you run this](DEPLOY.md)
70
+ in the deployment guide. A mint holds other people's bitcoin as bearer
71
+ liabilities, and it is the custody rather than the code that a regulator
72
+ has a view on. In the UK that view is specific: the Money Laundering
73
+ Regulations 2017 gate the custody of cryptoassets, by way of business, on
74
+ FCA registration; the financial promotion rules gate how a mint may be
75
+ marketed; sanctions law binds regardless. DEPLOY.md walks through all
76
+ three. Two things carry most of the weight on the payments side: an
77
+ evaluation mint should be configured as one (a fee kept at or below cost recovery rather
78
+ than zeroed, no priced names, a small cap, a MOTD that says so - or
79
+ better, a test network), and **no mint should ever touch fiat**. moneyer is millisatoshis throughout, deliberately, and that
80
+ boundary is what keeps a mint clear of the payments and e-money regimes.
81
+
69
82
  ## Configuration
70
83
 
71
84
  Environment only. Every variable is `MONEYER_*`; anything unset takes the
@@ -327,7 +340,9 @@ is nearly holding the money.
327
340
 
328
341
  `h` is optional and additive. A wallet that sends none gets exactly the
329
342
  behaviour it always got, so upgrading this mint breaks nothing that works
330
- today, and the LUD-25 draft needs no change to allow it.
343
+ today. The wire fields are an implementation proposal for eventual LUD-25
344
+ adoption; current wallets and dni/reference mints keep using the original
345
+ preimage-and-rotate flow unchanged.
331
346
 
332
347
  The rules:
333
348
 
@@ -348,6 +363,13 @@ The rules:
348
363
  - The payRequest and the discovery document both advertise `mintToHash:
349
364
  true`, so a wallet knows this mint takes the parameter before it asks
350
365
  rather than after it pays.
366
+ - When signing and LUD-21 verification are enabled, the quote also carries
367
+ `mint: {h, amount}`. This is the exact output and net millisatoshi value
368
+ the invoice will mint; it never carries `sig` before settlement.
369
+ - Once settled, `/verify` repeats the same `h` and `amount` and adds `sig`,
370
+ the ordinary LUD-25 signature over `LNURLcash:<amount>:<h>`. A sealed
371
+ signer can verify that receipt against the pre-payment commitment and
372
+ pinned `mintPubkey`, then confirm its staged note without exporting `k1`.
351
373
  - Claiming needs nothing else. `GET /w?k1=<the secret>` brings the note
352
374
  into existence as soon as the invoice has settled, with no `verify` poll
353
375
  and no preimage involved. The poll is still the way to claim from a mint
@@ -360,6 +382,10 @@ A named note is also derived-secret friendly: a wallet whose secrets come
360
382
  from its seed can restore a note it bought but never claimed, which a note
361
383
  whose secret was a preimage could never offer.
362
384
 
385
+ The proposed normative wire text, invalid cases and compatibility matrix are
386
+ kept executable in
387
+ [`lnurlcash-conformance`](https://github.com/TheCryptoDonkey/lnurlcash-conformance/blob/main/docs/BOUND-MINT-RECEIPTS.md).
388
+
363
389
  ## A retried mutation is answered, not refused
364
390
 
365
391
  Rotate, split and merge are GETs, and transports retry GETs. Go's
@@ -0,0 +1,132 @@
1
+ # Terms template
2
+
3
+ A starting point for `MONEYER_TOS_URL`. Adapt it, host it, point the
4
+ variable at it. Angle brackets mark what an operator must fill in; the
5
+ rest is written to be true of any moneyer mint and should only change if
6
+ your deployment makes it untrue.
7
+
8
+ This is a template, not legal advice. If your mint holds value that would
9
+ matter to the person who lost it, have someone qualified in your
10
+ jurisdiction read this before you publish it.
11
+
12
+ ---
13
+
14
+ ## Terms for <mint.example>
15
+
16
+ Last updated: <date>
17
+
18
+ ### What this is
19
+
20
+ <mint.example> is an evaluation mint for the
21
+ [LUD-25 draft](https://github.com/lnurl/luds/pull/301), running
22
+ [moneyer](https://github.com/forgesworn/moneyer). It exists to
23
+ demonstrate the protocol and to give wallet and mint implementers
24
+ something to grade against.
25
+
26
+ It is not a place to keep money. Notes on this mint are for testing the
27
+ protocol, and you should not hold value here that you would mind losing.
28
+
29
+ ### What a note is
30
+
31
+ A note is a bearer instrument. Whoever holds its secret can spend it, and
32
+ the mint cannot tell one holder from another - there are no accounts, no
33
+ passwords and no recovery. In particular:
34
+
35
+ - **Lose the secret and the value is gone.** Nobody can restore it, this
36
+ mint included.
37
+ - **Disclose the secret and whoever sees it can spend the note.** Treat it
38
+ the way you would treat cash.
39
+ - A note is a claim on this mint and nothing else. It is not backed by any
40
+ third party, and it is not a claim on the Bitcoin network.
41
+
42
+ ### No guarantee of redemption
43
+
44
+ This mint will try to honour every note it has struck. It does not
45
+ promise to, and you should not rely on it doing so. Redemption can fail
46
+ for reasons inside the operator's control and outside it: insufficient
47
+ channel liquidity, a funding source that is down, a host that is gone, or
48
+ the operator winding the mint down.
49
+
50
+ The mint publishes what it owes and what it holds at `/stats`, updated
51
+ continuously and signed hourly. Read it before you trust the mint with
52
+ anything. A `coverage` figure below 1 means the mint could not pay every
53
+ note out today.
54
+
55
+ The service is provided as is, without warranty of any kind. To the
56
+ fullest extent the law allows, the operator accepts no liability for any
57
+ loss arising from its use. That sentence has a floor: nothing in these
58
+ terms limits a liability the law does not allow to be limited, and if you
59
+ use this mint as a consumer, the statutory rights your law does not let
60
+ you sign away - in the UK, those the Consumer Rights Act 2015 implies -
61
+ are unaffected.
62
+
63
+ ### Wind-down
64
+
65
+ If this mint is retired, it will be put into sunset mode: it will refuse
66
+ anything that grows what it owes, and keep every way out open, so
67
+ outstanding notes can be melted. <Notice will be given through the mint's
68
+ MOTD and at <contact> at least <n> days beforehand.> Notes not melted by
69
+ then may not be redeemable.
70
+
71
+ ### Limits and fees
72
+
73
+ - Smallest note: <n> sats. Largest note: <n> sats.
74
+ - The mint fee is `<base> msat + <ppm> ppm`, published in the payRequest
75
+ metadata and on the discovery endpoint before you pay.
76
+
77
+ The fee is set to recover the routing cost the mint commits to when it
78
+ pays a note out, and is not priced to make a margin. The melt routing
79
+ budget is `max(0.5% of the amount, 5000 msat, the mint fee)`, so above
80
+ roughly 1,250 sats the fee does not cover it.
81
+
82
+ ### What the mint knows
83
+
84
+ This mint is not blind, and that is a property of LUD-25 rather than a
85
+ choice this deployment made. See
86
+ [What the mint knows](<mint.example>) - published on the mint's own site
87
+ and in its discovery document - for the full statement. In short: it
88
+ knows every note it issued, what each is worth, the links between them,
89
+ and the address each request came from. It does not know who holds a note
90
+ between operations.
91
+
92
+ Do not use this mint for anything where that matters to you.
93
+
94
+ That statement is also the privacy notice in outline. The personal data
95
+ are the network addresses requests arrive from and any zap names bound to
96
+ pubkeys; the operator holds them to run the mint and for nothing else,
97
+ keeps them for <retention period>, and does not sell or share them except
98
+ under compulsion of law. Where your law gives you rights over your data -
99
+ in the UK, the UK GDPR rights of access, correction and deletion -
100
+ contact <contact> to exercise them.
101
+
102
+ ### What this mint is not
103
+
104
+ - **Not a bank, and not a deposit-taker.** A note is not a deposit. There
105
+ is no depositor protection scheme behind it, in any jurisdiction.
106
+ - **Not a payment or e-money service.** This mint accepts and pays out
107
+ bitcoin only. It does not accept, hold, or pay out any national
108
+ currency, and no note is denominated in one.
109
+ - **Not a regulated financial service**, and not offered as one.
110
+ <The operator is not authorised or registered by <regulator>.>
111
+
112
+ ### Use
113
+
114
+ Use this mint lawfully. Do not use it if you are designated under UK, UN,
115
+ EU or US sanctions lists (or those of <jurisdiction>), and do not use it
116
+ to move value to or for anyone who is: a bearer mint cannot screen its
117
+ holders, so that duty sits with you. The operator may refuse or reverse
118
+ service, and may cooperate with lawful requests from authorities in
119
+ <jurisdiction>.
120
+
121
+ ### Operator and contact
122
+
123
+ <Operated by <name>, <jurisdiction>. Reach the operator at <contact>.>
124
+
125
+ ### Changes
126
+
127
+ These terms may change. The current version is always the one at this
128
+ URL, and material changes will be flagged through the mint's MOTD.
129
+
130
+ ### Governing law
131
+
132
+ <These terms are governed by the law of <jurisdiction>.>
@@ -0,0 +1,6 @@
1
+ export type MintFeeLike = {
2
+ baseFeeMsat: number;
3
+ feePpm: number;
4
+ };
5
+ export declare const feeInUnits: (fee: MintFeeLike | null, roundedToSat: boolean) => string;
6
+ export declare const describeFee: (fee: MintFeeLike, roundedToSat: boolean) => string;
@@ -0,0 +1,37 @@
1
+ // The mint fee as a person reads it, in one place so the payRequest
2
+ // metadata, the landing page and the mint's own site cannot drift apart
3
+ // on it - the same reason `privacy.ts` exists.
4
+ //
5
+ // The raw pair is spelled out deliberately. The machine-readable line
6
+ // beside it in the payRequest metadata is "Mint fees: 5000,1000" - the
7
+ // shape lnurlcash-kit parses, anchored and digits-only, so it cannot
8
+ // carry its own units - and a reader who has not parsed LUD-25 reads
9
+ // both numbers as satoshis. A tester who thinks the fee is 5000 sats on
10
+ // a 10k sat mint has been told something alarming and false, so the
11
+ // prose says msat and ppm where the wire cannot.
12
+ // "5 sat + 0.1% (5000 msat + 1000 ppm), rounded up to the sat", or
13
+ // "none". Bare, for a page that supplies its own "mint fee" label.
14
+ export const feeInUnits = (fee, roundedToSat) => {
15
+ if (!fee)
16
+ return 'none';
17
+ const shown = [];
18
+ const raw = [];
19
+ if (fee.baseFeeMsat > 0) {
20
+ shown.push(`${fee.baseFeeMsat % 1000 === 0 ? fee.baseFeeMsat / 1000 : (fee.baseFeeMsat / 1000).toFixed(3)} sat`);
21
+ raw.push(`${fee.baseFeeMsat} msat`);
22
+ }
23
+ if (fee.feePpm > 0) {
24
+ shown.push(`${fee.feePpm / 10_000}%`);
25
+ raw.push(`${fee.feePpm} ppm`);
26
+ }
27
+ if (!shown.length)
28
+ return 'none';
29
+ const body = `${shown.join(' + ')} (${raw.join(' + ')})`;
30
+ return roundedToSat ? `${body}, rounded up to the sat` : body;
31
+ };
32
+ // The same fee prefixed for prose: "fee 5 sat + 0.1% (5000 msat + 1000
33
+ // ppm)" - what a payer sees inside their wallet's description.
34
+ export const describeFee = (fee, roundedToSat) => {
35
+ const words = feeInUnits(fee, roundedToSat);
36
+ return words === 'none' ? 'no fee' : `fee ${words}`;
37
+ };
package/dist/landing.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { MINT_KNOWS, MINT_KNOWS_HEADING } from "./privacy.js";
2
+ import { feeInUnits } from "./fee-words.js";
2
3
  import { applyMintFee } from 'lnurlcash-kit';
3
4
  // The mint's face: one self-contained page at GET /, no build step, no
4
5
  // external assets. It states what a visitor needs before trusting a mint
@@ -11,9 +12,7 @@ export const landingPage = (args) => {
11
12
  const stats = args.stats ?? null;
12
13
  const address = `${config.username}@${host}`;
13
14
  const fee = config.mintFee;
14
- const feeLine = fee
15
- ? `${fee.baseFeeMsat > 0 ? `${fee.baseFeeMsat} msat flat` : ''}${fee.baseFeeMsat > 0 && fee.feePpm > 0 ? ' + ' : ''}${fee.feePpm > 0 ? `${fee.feePpm / 10_000}%` : ''}`
16
- : 'none';
15
+ const feeLine = feeInUnits(fee, config.roundFeeToSat !== false);
17
16
  const maxNet = fee ? applyMintFee(config.maxSendableMsat, fee) : config.maxSendableMsat;
18
17
  const sats = (msat) => `${(msat / 1000).toLocaleString('en-GB')} sat`;
19
18
  const title = config.name ?? nodeInfo.alias ?? 'moneyer';
@@ -72,12 +71,17 @@ p.small{color:var(--dim);font-size:13.5px;line-height:1.65;text-align:center}
72
71
  .knows p{font-size:14px;line-height:1.65}
73
72
  .motd{background:var(--raise);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:14px;padding:14px 18px;font-size:14.5px;line-height:1.6}
74
73
  .motd b{display:block;font-size:12px;letter-spacing:.09em;text-transform:uppercase;color:var(--dim);margin-bottom:4px}
74
+ .disclaimer{background:color-mix(in srgb,var(--warn,#c4a35e) 12%,var(--raise));border:1px solid color-mix(in srgb,var(--warn,#c4a35e) 50%,transparent);border-left:3px solid var(--warn,#c4a35e);border-radius:14px;padding:14px 18px;font-size:14.5px;line-height:1.55}
75
+ .disclaimer b{display:block;font-size:11px;font-weight:700;letter-spacing:.13em;text-transform:uppercase;color:var(--warn,#c4a35e);margin-bottom:5px}
76
+ .disclaimer strong{color:var(--warn,#c4a35e)}
77
+ .disclaimer a{color:var(--ink);text-decoration:underline;text-underline-offset:2px}
75
78
  a{color:var(--accent)}
76
79
  </style>
77
80
  </head>
78
81
  <body>
79
82
  <main>
80
83
  <svg class="mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7.6v8.8"/><path d="M15.4 9.4c-.7-1.1-1.9-1.8-3.4-1.8-2 0-3.6 1.1-3.6 2.7 0 3.4 7.2 1.8 7.2 5 0 1.6-1.6 2.7-3.6 2.7-1.5 0-2.7-.7-3.4-1.8"/></svg>
84
+ <div class="disclaimer" role="note"><b>Proof of concept · for developers</b><span>An evaluation mint for the <a href="https://github.com/lnurl/luds/pull/301" rel="noopener noreferrer">draft LNURLcash spec</a>. A note is real bearer value with no protection, no recovery and no guaranteed redemption - <strong>assume you can lose anything you put in.</strong>${config.tosUrl ? ` <a href="${escapeHtml(config.tosUrl)}" rel="noopener noreferrer">Terms</a>` : ''}</span></div>
81
85
  <h1>${escapeHtml(title)}<small>An LNURLcash mint. Pay the address below and the invoice's payment preimage <em>is</em> your bearer note - money as a secret you hold.</small></h1>
82
86
  ${config.motd ? `<div class="motd"><b>notice</b>${escapeHtml(config.motd)}</div>` : ''}
83
87
  <div class="addr"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 4.5 13.5H11L9.5 22 18 10.5h-6.5L13 2z"/></svg>${escapeHtml(address)}</div>
package/dist/server.d.ts CHANGED
@@ -29,9 +29,6 @@ export type MoneyerDeps = {
29
29
  zapPollMs?: number;
30
30
  statsPublishMs?: number;
31
31
  };
32
- export declare const describeFee: (fee: {
33
- baseFeeMsat: number;
34
- feePpm: number;
35
- }, roundedToSat: boolean) => string;
32
+ export { describeFee } from './fee-words.ts';
36
33
  export declare const createMoneyer: (config: MoneyerConfig, deps?: MoneyerDeps) => Promise<Moneyer>;
37
34
  export declare const sweepExpiredMintInvoices: (store: NoteStore, nowMs?: number) => number;
package/dist/server.js CHANGED
@@ -5,6 +5,7 @@ import { applyMintFee, grossUpForMintFee, hashK1 } 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";
8
+ import { describeFee } from "./fee-words.js";
8
9
  import { createFakeBackend } from "./backends/fake.js";
9
10
  import { createClnBackend } from "./backends/cln.js";
10
11
  import { createLndBackend } from "./backends/lnd.js";
@@ -34,16 +35,7 @@ const readBody = async (req, limit = MAX_BODY_BYTES) => {
34
35
  }
35
36
  return Buffer.concat(chunks).toString('utf8');
36
37
  };
37
- // "fee 5 sat + 0.1%" - what a payer sees in their wallet's description.
38
- export const describeFee = (fee, roundedToSat) => {
39
- const parts = [];
40
- if (fee.baseFeeMsat > 0)
41
- parts.push(`${fee.baseFeeMsat % 1000 === 0 ? fee.baseFeeMsat / 1000 : (fee.baseFeeMsat / 1000).toFixed(3)} sat`);
42
- if (fee.feePpm > 0)
43
- parts.push(`${(fee.feePpm / 10_000).toString()}%`);
44
- const base = parts.length ? `fee ${parts.join(' + ')}` : 'no fee';
45
- return roundedToSat && parts.length ? `${base}, rounded up to the sat` : base;
46
- };
38
+ export { describeFee } from "./fee-words.js";
47
39
  // A note's value rounded down to a whole sat; unchanged when already whole.
48
40
  const wholeSatFloor = (msat) => Math.floor(msat / 1000) * 1000;
49
41
  const backendFor = (config) => {
@@ -550,6 +542,9 @@ export const createMoneyer = async (config, deps = {}) => {
550
542
  // LUD-17's lnurlw:// is the scheme a wallet puts on a QR, not a
551
543
  // field in a JSON body; every other URL here is directly fetchable.
552
544
  withdrawLink: `${origin}/w`,
545
+ // Receipt-aware wallets need the note-signing key before they pay,
546
+ // so they can authenticate the later LUD-21 mint receipt.
547
+ ...(signer ? { mintPubkey: signer.pubkey } : {}),
553
548
  // This mint takes `h` on the callback below, so a wallet can name
554
549
  // the note it is buying. Advertised here as well as on the
555
550
  // discovery document because a wallet handed nothing but a
@@ -659,6 +654,12 @@ export const createMoneyer = async (config, deps = {}) => {
659
654
  // answer without it, and a wallet can tell the two apart before
660
655
  // paying rather than by looking for a note afterwards.
661
656
  ...(outputId !== null ? { mintToHash: true } : {}),
657
+ // Optional bound-receipt commitment: the exact output and net note
658
+ // value this invoice will mint. It is only offered when /verify can
659
+ // later authenticate settlement with this mint's signing key.
660
+ ...(outputId !== null && config.verify && signer
661
+ ? { mint: { h: outputId, amount: net } }
662
+ : {}),
662
663
  ...(config.verify ? { verify: `${origin}/verify/${paymentHash}` } : {})
663
664
  });
664
665
  }
@@ -673,11 +674,30 @@ export const createMoneyer = async (config, deps = {}) => {
673
674
  if (!invoice.settled && (await backend.isInvoiceSettled(paymentHash))) {
674
675
  store.settleMintInvoice(paymentHash);
675
676
  }
676
- const settled = store.mintInvoiceByHash(paymentHash).settled;
677
+ const currentInvoice = store.mintInvoiceByHash(paymentHash);
678
+ const settled = currentInvoice.settled;
677
679
  // The preimage IS the bearer secret. Served only once settled, and
678
680
  // fetched live from the funding source - it is never stored here.
679
681
  const preimageHex = settled ? await backend.invoicePreimage(paymentHash) : null;
680
- return send({ status: 'OK', settled, preimage: preimageHex, pr: invoice.pr });
682
+ return send({
683
+ status: 'OK',
684
+ settled,
685
+ preimage: preimageHex,
686
+ pr: invoice.pr,
687
+ // Repeating h and amount binds this response to the quote. The
688
+ // ordinary note signature appears only once value exists at h.
689
+ ...(currentInvoice.outputId !== null && signer
690
+ ? {
691
+ mint: {
692
+ h: currentInvoice.outputId,
693
+ amount: currentInvoice.netMsat,
694
+ ...(settled
695
+ ? { sig: signer.sign(currentInvoice.outputId, currentInvoice.netMsat) }
696
+ : {})
697
+ }
698
+ }
699
+ : {})
700
+ });
681
701
  }
682
702
  const melt = store.meltByHash(paymentHash);
683
703
  if (melt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/moneyer",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -37,6 +37,7 @@
37
37
  "CHANGELOG.md",
38
38
  "SECURITY.md",
39
39
  "THREAT-MODEL.md",
40
+ "TERMS-TEMPLATE.md",
40
41
  "llms.txt"
41
42
  ],
42
43
  "scripts": {
@@ -57,7 +58,7 @@
57
58
  "@noble/hashes": "^2.3.0",
58
59
  "@scure/base": "^1.2.4",
59
60
  "farrier-kit": "^1.1.3",
60
- "lnurlcash-kit": "^0.2.0",
61
+ "lnurlcash-kit": "^0.3.0",
61
62
  "nostr-tools": "2.24.1"
62
63
  },
63
64
  "devDependencies": {
@@ -67,7 +68,7 @@
67
68
  "@types/node": "^24.0.0",
68
69
  "animejs": "^4.0.0",
69
70
  "happy-dom": "^20.0.0",
70
- "lnurlcash-conformance": "^0.2.1",
71
+ "lnurlcash-conformance": "^0.3.0",
71
72
  "playwright": "^1.62.1",
72
73
  "typescript": "^5.7.0",
73
74
  "uqr": "^0.1.2",