@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 +62 -0
- package/README.md +27 -1
- package/TERMS-TEMPLATE.md +132 -0
- package/dist/fee-words.d.ts +6 -0
- package/dist/fee-words.js +37 -0
- package/dist/landing.js +7 -3
- package/dist/server.d.ts +1 -4
- package/dist/server.js +32 -12
- package/package.json +4 -3
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
|
|
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,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
|
|
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
|
-
|
|
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
|
|
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({
|
|
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.
|
|
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.
|
|
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.
|
|
71
|
+
"lnurlcash-conformance": "^0.3.0",
|
|
71
72
|
"playwright": "^1.62.1",
|
|
72
73
|
"typescript": "^5.7.0",
|
|
73
74
|
"uqr": "^0.1.2",
|