@forgesworn/moneyer 0.1.1 → 0.2.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,191 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0] - 2026-08-22
4
+
5
+ While LUD-25 is a draft, a `0.x` minor bump may be breaking; this one is
6
+ additive on the wire apart from the node-capacity rename below.
7
+
8
+ - The test suite has a 30 second timeout rather than vitest's 5 second
9
+ default. Nearly every test here starts an HTTP server, opens a database
10
+ and does real curve and KDF work; 5 seconds is a unit test's budget, and
11
+ on a loaded machine the slower cases crossed it while passing perfectly
12
+ well. A gate that fails at random teaches people to bypass it.
13
+ - **`moneyer --dev` is a mint a wallet can actually use.** The fake
14
+ funding source settled nothing on its own: settlement was reachable only
15
+ through an in-process handle a test holds, so a wallet pointed at a
16
+ standalone `MONEYER_BACKEND=fake` mint asked for an invoice, nothing ever
17
+ paid it, no note was ever minted, and every flow needing a note - split,
18
+ merge, melt, send - dead-ended behind an empty wallet while the mint
19
+ looked healthy throughout. `--dev` now treats every invoice it issues as
20
+ paid the moment it is issued, so minting, splitting and melting all work
21
+ locally against a wallet. It can only be set on the fake backend, which
22
+ moves no money, and the startup banner says plainly that the notes are
23
+ worth nothing. `MONEYER_FAKE_AUTOSETTLE=true` sets it explicitly for a
24
+ compose file that cannot pass a flag.
25
+ - The mint can announce itself. With `MONEYER_ANNOUNCE=true` and the Nostr
26
+ identity zap-to-note already uses, it publishes its own discovery
27
+ document hourly as a parameterised replaceable event (kind 30078, `d`
28
+ tag `lnurlcash-mint`), alongside the liabilities snapshot. Until now a
29
+ wallet could only learn of a mint by being told its address; this is the
30
+ half that has to exist before finding one means anything. The content is
31
+ the document exactly as the discovery endpoint serves it, so there is one
32
+ description of a mint and not two, plus a `sig` over the canonicalised
33
+ body made with the note signing key, so a holder can check the
34
+ announcement against the same key their notes verify against.
35
+ `verifyAnnouncement` is exported for that. Off unless the operator turns
36
+ it on: a mint that does not want to be listed says nothing. No new event
37
+ kind, and no recommendations or reviews; both are protocol decisions that
38
+ belong with the NIPs.
39
+ - A note melts to an invoice that states no amount. Such an invoice used
40
+ to be refused outright, which is a papercut for anyone paying one: for a
41
+ bearer note the amount was never in question, since the note's value is
42
+ the amount. The mint sends the whole-sat floor of the note's value, the
43
+ same figure it already accepts from a wallet that fills the amount in
44
+ itself, and keeps the sub-sat remainder as the same dust. A note worth
45
+ less than a single satoshi has nothing left once the floor applies and
46
+ is refused with `insufficient value`; it can still be melted by a wallet
47
+ that invoices its exact value. Everything else about the melt path is
48
+ untouched, including the pending and restore discipline.
49
+ - A wallet can name the note it is buying. It chooses the note's spend
50
+ secret itself, keeps it, and sends `h`, the sha256 of that secret, on
51
+ the pay callback; the mint credits the note at `h` when the invoice
52
+ settles. The invoice's payment preimage then buys nothing and is only a
53
+ payment proof, which is what it always should have been: a preimage is
54
+ known to the funding source, to every node that forwarded the payment,
55
+ and to anyone who polls LUD-21 `verify` with the payment hash written
56
+ inside the invoice. Naming the note leaves the buyer as the only party
57
+ who ever held the secret, and replaces "claim and rotate faster than
58
+ anybody else" with nothing to race for. A malformed `h` is refused
59
+ before an invoice is issued, so a wallet never pays for a quote the
60
+ mint would reject; an `h` that already names a note or an invoice is
61
+ refused with `Invalid or already spent k1.`, the same oracle-free
62
+ sentence a colliding output gets on the withdraw callback. The reply
63
+ carries `mintToHash: true` when the binding was made, and the payRequest
64
+ and the discovery document advertise `mintToHash: true` so a wallet
65
+ knows before it asks. Claiming needs no `verify` poll: the wallet asks
66
+ `/w?k1=<its own secret>` directly. Entirely optional and additive: a
67
+ wallet that sends no `h` gets exactly today's behaviour, and no existing
68
+ wallet or service breaks. The draft itself does need one additive
69
+ amendment, because it says a `SERVICE` MUST accept `k1=P` and a bound
70
+ mint deliberately does not; that is proposed separately. Wallets are urged to persist the secret before
71
+ requesting the invoice, which is the one thing that could make this
72
+ worse than what it replaces.
73
+ - A live note's informational GET now carries `payLink`, pointing at this
74
+ mint's payRequest, which is the counterpart of the `withdrawLink` a
75
+ payRequest already advertises. It is the route home for a holder who has
76
+ nothing but a note: from it a wallet reaches the discovery document, and
77
+ so the mint's terms and its retired signing keys. Without it a wallet
78
+ that only ever received notes cannot tell an announced key rotation from
79
+ a substituted key. Optional and additive; unknown and spent notes still
80
+ say nothing about the mint beyond the refusal.
81
+ - Both database connections now wait up to five seconds for a lock instead
82
+ of giving up at once. `node:sqlite` opens with no busy timeout, so a
83
+ second writer met `database is locked` the moment the first one held it.
84
+ The operator CLI is that second writer while the mint is running, and the
85
+ times you reach for `moneyer admin reconcile` are the busy ones. Nothing
86
+ was ever at risk of corruption; the command simply failed when it was
87
+ least convenient.
88
+ - The discovery endpoint carries the human layer: `name`, `description`,
89
+ `contact` (`nostr` as an npub, `email`, `url`), `tosUrl`, `motd`,
90
+ `fees`, `version` and `previousPubkeys`. Every one is optional and an
91
+ unset one is absent rather than empty. New environment variables:
92
+ `MONEYER_NAME`, `MONEYER_CONTACT_NOSTR`, `MONEYER_CONTACT_EMAIL`,
93
+ `MONEYER_CONTACT_URL`, `MONEYER_TOS_URL`, `MONEYER_MOTD`.
94
+ - **Wire-name change**: node capacity now goes out as `nodeCapacity`, the
95
+ name the reference mint, the conformance mock and `lnurlcash-kit` all
96
+ use. `nodeCapacityMsat`, which was moneyer's alone, is emitted alongside
97
+ it for this one release and then removed. Both are milli-satoshis.
98
+ - The mint's own site and the fallback landing page show the message of
99
+ the day, the operator's contact and the terms link, and read capacity
100
+ under either name.
101
+ - `GET /stats` states what the mint owes and what its funding node holds:
102
+ outstanding liabilities and note count, melts in flight and the age of
103
+ the oldest, the node's local balance, and the coverage ratio between
104
+ the two to four decimal places. Cached 30 seconds, never per-note,
105
+ public by design. `MONEYER_STATS=false` switches it off and
106
+ `MONEYER_STATS_RATIO_ONLY=true` publishes the ratio alone. Both the
107
+ mint's site and the fallback landing page carry a coverage row.
108
+ - `MONEYER_STATS_PUBLISH=true` publishes an hourly signed snapshot of
109
+ those figures to Nostr (kind 30078, `d` tag `lnurlcash-liabilities`),
110
+ signed with the note signing key so anyone can check the history
111
+ against the mint's advertised pubkey. `scripts/verify-stats.mjs` does
112
+ exactly that, and `verifyStatsSnapshot` is exported for wallets.
113
+ - The lnd and cln backends report the node's local channel balance;
114
+ the fake one reports a configurable balance, one bitcoin by default.
115
+ - **Self-service lightning addresses.** With `MONEYER_NAME_PRICE_MSAT`
116
+ set, anyone with an npub can claim `name@<host>` by posting to
117
+ `/names` with a NIP-98 Authorization and a note of this mint. The key
118
+ that signs the request owns the name; no other identity is accepted.
119
+ The note is burned and the name recorded in one transaction, so
120
+ nothing is ever paid for a name somebody else got first. Unset means
121
+ registration is closed; `0` means free, three names per pubkey.
122
+ Registered and operator-configured names live in one table and are
123
+ served by one lookup, and `GET /.well-known/nostr.json?name=` resolves
124
+ them over NIP-05 as well, one name at a time. Discovery advertises
125
+ `namePriceMsat` while registration is open.
126
+ - The kind 2525 rumor for a zap-funded note carries the zap request in a
127
+ `description` tag - the same content the kind 9735 receipt carries - so
128
+ a wallet can show who zapped and what they wrote without fetching the
129
+ receipt. Readers take tags by name, so an older one does not notice.
130
+ - `MONEYER_ZAP_NAMES` is now optional: a mint that opens registration can
131
+ start with no names of its own. `MONEYER_NOSTR_KEY` and
132
+ `MONEYER_NOSTR_RELAYS` still go together.
133
+ - `moneyer admin names` gains `add <name> <npub>` and `rm <name>`, and
134
+ `list` shows configured names the running mint has not loaded yet.
135
+ - **A retried rotate, split or merge is answered instead of refused.**
136
+ These are GETs, and transports retry GETs on their own: the retry
137
+ arrives byte-identical after the inputs are burned, and the old answer
138
+ - `Invalid or already spent k1.` - told the wallet to drop the only
139
+ copy of a secret the mint really had minted a note against. The mint
140
+ now records which request minted which outputs, and replays the same
141
+ reply: same signatures, nothing burned, nothing minted, no balance
142
+ moved. A request counts as the same request when it names the same
143
+ input notes and asks for the same `h`, `h2` and `amount`; input order
144
+ does not matter. Anything else naming a burned note is still refused
145
+ with the same reason string, so no oracle appears, and the melt path is
146
+ untouched. moneyer's own behaviour: the draft says nothing about
147
+ retries yet.
148
+ - `moneyer admin <command>`: the operator surface. `status`, `notes`,
149
+ `note`, `melts`, `reconcile`, `sweep`, `snapshot`, `names list`,
150
+ `keys rotate` and `verify-note`, all reading the same `MONEYER_*`
151
+ environment the mint does, and all opening the database read-only
152
+ unless the command mutates. `snapshot` uses `VACUUM INTO`, so it needs
153
+ no sqlite3 binary and is safe against a live mint; `keys rotate`
154
+ generates a key and prints the two environment lines without touching
155
+ anything.
156
+ - `MONEYER_METRICS=true` serves `GET /metrics` in the OpenMetrics text
157
+ format. Off by default and never authenticated by the app: restrict the
158
+ path at the reverse proxy. The README's Operating section names the
159
+ three alerts worth having.
160
+ - A "What the mint knows" section in the README, on the mint's own site
161
+ and on the fallback landing page, in the same words: what a LUD-25 mint
162
+ can see, what it cannot, why the wallet-side mitigations are weak, and
163
+ why the design was chosen anyway. Blindness is the one ecash property
164
+ this cannot offer, and a reader should not have to infer that.
165
+ - `MONEYER_PREVIOUS_SIGNING_PUBKEYS` lists the compressed pubkeys this
166
+ mint signed notes under before, published as `previousPubkeys` on
167
+ discovery. Rotating the signing key no longer invalidates every
168
+ outstanding note's signature or trips a pinned wallet's mismatch
169
+ check. Pubkeys only, validated as points on the curve at startup: the
170
+ old private keys are not needed to verify and should not stay on the
171
+ server. The runbook is in the README.
172
+
173
+ ## [0.1.2] - 2026-08-21
174
+
175
+ - `MONEYER_ROUND_FEE_TO_SAT` ceilings the mint fee to a whole sat, as
176
+ dni's lnurl-mint does on purpose so the mint is "never short a sat".
177
+ LUD-25 says nothing about rounding and `lnurlcash-kit`'s `mintFeeBand`
178
+ accepts both readings, so this is a posture choice rather than a
179
+ compliance one. **Off by default**: turning it on raises what the mint
180
+ withholds, which is not a change to make on a redeploy.
181
+ - The mint path called `applyMintFee` directly rather than the fee helper,
182
+ so it would have ignored that setting entirely - advertising one fee and
183
+ withholding another. Every fee site now goes through one
184
+ `netAfterMintFee`.
185
+ - `roundFeeToSat` is optional on `MoneyerConfig`: that type is public API,
186
+ and an embedder constructing a config should not have to name a field it
187
+ does not care about.
188
+
3
189
  ## [0.1.1] - 2026-08-21
4
190
 
5
191
  - `withdrawLink` is now the plain URL (`https://host/w`), the form