@forgesworn/moneyer 0.4.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 +82 -0
- package/README.md +58 -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 +48 -15
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
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
|
+
|
|
65
|
+
## [0.5.0] - 2026-08-23
|
|
66
|
+
|
|
67
|
+
- **Both doors, named.** With `MONEYER_ONION_URL` and
|
|
68
|
+
`MONEYER_PUBLIC_ORIGIN` both set, `/w` and the mint address now carry
|
|
69
|
+
`mirrors`: this mint's other origin. Both serve the same notes - a note
|
|
70
|
+
is keyed by `sha256(k1)` and not by host - but a note travels as a URL,
|
|
71
|
+
and a URL names one host, so an onion note handed to somebody without
|
|
72
|
+
Tor is unspendable to them. Naming the other door is what lets a wallet
|
|
73
|
+
offer the holder a way across.
|
|
74
|
+
- Absent rather than empty when there is only one door, because a wallet
|
|
75
|
+
reading `[]` would think it had been told something. The hourly Nostr
|
|
76
|
+
announcement carries it too.
|
|
77
|
+
- Documented, because it is a real limit rather than a missing feature: a
|
|
78
|
+
wallet must **not** fail over to a mirror on its own. Every request that
|
|
79
|
+
identifies a note carries the k1, and nothing in LUD-25 lets a host
|
|
80
|
+
prove it is the same mint before receiving that secret - a hostile
|
|
81
|
+
discovery document can name any host and claim any `mintPubkey`, since
|
|
82
|
+
claiming is not proving. Closing it needs a signed challenge the spec
|
|
83
|
+
does not have.
|
|
84
|
+
|
|
3
85
|
## [0.4.0] - 2026-08-23
|
|
4
86
|
|
|
5
87
|
- **Reaching the mint over Tor.** `MONEYER_ONION_URL` names this mint's
|
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
|
|
@@ -507,6 +533,37 @@ controlled, and that property is unchanged: the worst a forged Host can do
|
|
|
507
533
|
is get back the onion URL instead of the clearnet one, and the operator
|
|
508
534
|
configured both. Anything else falls through to `MONEYER_PUBLIC_ORIGIN`.
|
|
509
535
|
|
|
536
|
+
### Serving both audiences at once
|
|
537
|
+
|
|
538
|
+
Both doors serve the **same notes**. A note is keyed by `sha256(k1)` and
|
|
539
|
+
not by host, so a k1 struck on the onion is valid at the clearnet host and
|
|
540
|
+
the other way round. What is bound to a host is only the URL a note
|
|
541
|
+
travels as - and an onion note handed to somebody without Tor is
|
|
542
|
+
unspendable to them, while a clearnet note redeemed over Tor is private
|
|
543
|
+
only if their wallet proxies.
|
|
544
|
+
|
|
545
|
+
So when both are configured, `/w` and the mint address name the other one
|
|
546
|
+
in `mirrors`:
|
|
547
|
+
|
|
548
|
+
```json
|
|
549
|
+
{"tag": "withdrawRequest", "callback": "http://<v3>.onion/w/cb",
|
|
550
|
+
"mirrors": ["https://mint.example"], "mintPubkey": "..."}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Absent when there is only one door, rather than an empty array - a wallet
|
|
554
|
+
reading `[]` would think it had been told something. The hourly Nostr
|
|
555
|
+
announcement carries it too, so a wallet that discovers this mint on a
|
|
556
|
+
relay learns both doors before it holds anything.
|
|
557
|
+
|
|
558
|
+
**A wallet must not fail over to a mirror on its own.** Every request that
|
|
559
|
+
identifies a note carries the k1, so switching hosts discloses a bearer
|
|
560
|
+
secret to the new one, and nothing in LUD-25 lets a host prove it is the
|
|
561
|
+
same mint before receiving that secret - a hostile discovery document can
|
|
562
|
+
name any host and claim any `mintPubkey`, because claiming is not proving.
|
|
563
|
+
Show the holder the other door and let them choose it. Closing that
|
|
564
|
+
properly needs a challenge the mint signs, which the spec does not have
|
|
565
|
+
yet.
|
|
566
|
+
|
|
510
567
|
Running the hidden service itself is not this mint's job. Point a Tor
|
|
511
568
|
`HiddenServiceDir` (or a proxy that speaks onion) at whatever host and port
|
|
512
569
|
it already listens on, the same way you would front it with Caddy for
|
|
@@ -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) => {
|
|
@@ -208,7 +200,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
208
200
|
// LUD-25 discovery, built here rather than inline in its route because
|
|
209
201
|
// the mint also announces itself with it, and there must be one
|
|
210
202
|
// description of a mint rather than two that can drift apart.
|
|
211
|
-
const mintAddressDocument = (origin, user) => ({
|
|
203
|
+
const mintAddressDocument = (origin, user, mirrors = []) => ({
|
|
212
204
|
tag: 'withdrawRequest',
|
|
213
205
|
callback: `${origin}/w`,
|
|
214
206
|
minWithdrawable: config.minMintMsat,
|
|
@@ -217,6 +209,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
217
209
|
: config.maxSendableMsat,
|
|
218
210
|
defaultDescription: config.description,
|
|
219
211
|
payLink: `${origin}/.well-known/lnurlp/${user}`,
|
|
212
|
+
...(mirrors.length ? { mirrors } : {}),
|
|
220
213
|
...(signer ? { mintPubkey: signer.pubkey } : {}),
|
|
221
214
|
// The human layer: who runs this, how to reach them, the terms,
|
|
222
215
|
// and today's message. Absent unless the operator set it.
|
|
@@ -294,7 +287,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
294
287
|
kind: ANNOUNCE_KIND,
|
|
295
288
|
created_at: Math.floor(Date.now() / 1000),
|
|
296
289
|
tags: [['d', ANNOUNCE_D_TAG]],
|
|
297
|
-
content: announcementContent(mintAddressDocument(config.publicOrigin, config.username), config.signingKey)
|
|
290
|
+
content: announcementContent(mintAddressDocument(config.publicOrigin, config.username, config.onionUrl ? [config.onionUrl] : []), config.signingKey)
|
|
298
291
|
}, hexToBytes(config.zap.nostrKey));
|
|
299
292
|
const { ok, failed } = await nostr.publish(config.zap.relays, event);
|
|
300
293
|
log(`mint announced to ${ok.length} relay${ok.length === 1 ? '' : 's'}${failed.length ? `, ${failed.length} refused` : ''}`);
|
|
@@ -311,6 +304,14 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
311
304
|
const origin = onionHost && askedFor === onionHost
|
|
312
305
|
? config.onionUrl
|
|
313
306
|
: (config.publicOrigin ?? `http://${req.headers.host ?? '127.0.0.1'}`);
|
|
307
|
+
// This mint's OTHER doors. A note is keyed by sha256(k1) and not by
|
|
308
|
+
// host, so every one of these serves the same notes - but a note
|
|
309
|
+
// travels as a URL, and a URL names one host. An onion note handed to
|
|
310
|
+
// somebody without Tor is unspendable to them, and a clearnet note
|
|
311
|
+
// redeemed over Tor is only private if their wallet proxies. Saying
|
|
312
|
+
// which other host answers is what lets a wallet move a note between
|
|
313
|
+
// them instead of the holder finding out it cannot.
|
|
314
|
+
const mirrors = [config.publicOrigin, config.onionUrl].filter((candidate) => Boolean(candidate) && candidate !== origin);
|
|
314
315
|
const host = new URL(origin).host;
|
|
315
316
|
const send = (body, status = 200) => {
|
|
316
317
|
res.writeHead(status, {
|
|
@@ -541,6 +542,9 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
541
542
|
// LUD-17's lnurlw:// is the scheme a wallet puts on a QR, not a
|
|
542
543
|
// field in a JSON body; every other URL here is directly fetchable.
|
|
543
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 } : {}),
|
|
544
548
|
// This mint takes `h` on the callback below, so a wallet can name
|
|
545
549
|
// the note it is buying. Advertised here as well as on the
|
|
546
550
|
// discovery document because a wallet handed nothing but a
|
|
@@ -555,7 +559,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
555
559
|
if (lnurlwMatch) {
|
|
556
560
|
if (!knownUser(lnurlwMatch[1]))
|
|
557
561
|
return fail('Unknown user.', 404);
|
|
558
|
-
return send(mintAddressDocument(origin, lnurlwMatch[1]));
|
|
562
|
+
return send(mintAddressDocument(origin, lnurlwMatch[1], mirrors));
|
|
559
563
|
}
|
|
560
564
|
// ---- LUD-06 pay callback: issue a mint invoice ----
|
|
561
565
|
if (requestUrl.pathname === '/p/cb') {
|
|
@@ -650,6 +654,12 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
650
654
|
// answer without it, and a wallet can tell the two apart before
|
|
651
655
|
// paying rather than by looking for a note afterwards.
|
|
652
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
|
+
: {}),
|
|
653
663
|
...(config.verify ? { verify: `${origin}/verify/${paymentHash}` } : {})
|
|
654
664
|
});
|
|
655
665
|
}
|
|
@@ -664,11 +674,30 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
664
674
|
if (!invoice.settled && (await backend.isInvoiceSettled(paymentHash))) {
|
|
665
675
|
store.settleMintInvoice(paymentHash);
|
|
666
676
|
}
|
|
667
|
-
const
|
|
677
|
+
const currentInvoice = store.mintInvoiceByHash(paymentHash);
|
|
678
|
+
const settled = currentInvoice.settled;
|
|
668
679
|
// The preimage IS the bearer secret. Served only once settled, and
|
|
669
680
|
// fetched live from the funding source - it is never stored here.
|
|
670
681
|
const preimageHex = settled ? await backend.invoicePreimage(paymentHash) : null;
|
|
671
|
-
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
|
+
});
|
|
672
701
|
}
|
|
673
702
|
const melt = store.meltByHash(paymentHash);
|
|
674
703
|
if (melt) {
|
|
@@ -725,6 +754,10 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
725
754
|
// signing keys. Without it a wallet that only ever received notes
|
|
726
755
|
// cannot tell an announced key rotation from a substituted key.
|
|
727
756
|
payLink: `${origin}/.well-known/lnurlp/${config.username}`,
|
|
757
|
+
// Where else this same note can be redeemed. A holder with nothing
|
|
758
|
+
// but a note learns from the note itself that the other door
|
|
759
|
+
// exists.
|
|
760
|
+
...(mirrors.length ? { mirrors } : {}),
|
|
728
761
|
...(signer ? { mintPubkey: signer.pubkey } : {})
|
|
729
762
|
});
|
|
730
763
|
}
|
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",
|