@forgesworn/moneyer 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -1
- package/README.md +8 -0
- package/dist/backends/cln.js +10 -2
- package/dist/backends/lnd.js +4 -0
- package/dist/backends/types.d.ts +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +24 -0
- package/dist/landing.js +1 -0
- package/dist/server.js +22 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.11.1 - 2026-09-07
|
|
4
|
+
|
|
5
|
+
- Hash lookups report `Note already spent.` for retained burned notes,
|
|
6
|
+
`Unknown note.` for unregistered hashes, and `pending` for unresolved
|
|
7
|
+
melts. This follows the proposed LUD-25 status revision and lets wallets
|
|
8
|
+
recognise a redeemed note without sending its bearer secret. Existing
|
|
9
|
+
burned rows are retained already; no database migration is required.
|
|
10
|
+
- Test tooling uses an immutable conformance commit matching this response
|
|
11
|
+
contract. It is a development dependency, not a runtime dependency.
|
|
12
|
+
|
|
13
|
+
## 0.11.0 - 2026-09-06
|
|
14
|
+
|
|
15
|
+
**Three fields the reference mint publishes and moneyer did not.** Each
|
|
16
|
+
answers a question a wallet had no way to ask.
|
|
17
|
+
|
|
18
|
+
- `nodeUris` - every address the funding node announces, not just the first.
|
|
19
|
+
A node reachable over Tor as well as clearnet announces both, and `nodeUri`
|
|
20
|
+
alone told a peer about a door it may not be able to open. `nodeUri` is
|
|
21
|
+
unchanged and stays the first of the list. lnd already fetched all of them;
|
|
22
|
+
cln now builds the list the same way instead of taking `address[0]`.
|
|
23
|
+
- `sunsetDate` - `MONEYER_SUNSET_DATE`, an ISO-8601 day, published so a wallet
|
|
24
|
+
can warn its holder while spending is still possible. Deliberately separate
|
|
25
|
+
from `MONEYER_SUNSET`, which stops minting: by the time that flag goes on,
|
|
26
|
+
a holder who was going to be told has already not been told. The value is
|
|
27
|
+
validated at boot, so a typo fails loudly rather than putting a wrong date
|
|
28
|
+
in front of a holder. It also shows on the mint's own landing page.
|
|
29
|
+
- `outstandingNotesMsat` - what the mint owes, in msat. A fact about its own
|
|
30
|
+
database, so it survives an unreachable funding source, but it is the same
|
|
31
|
+
disclosure `/stats` makes and answers to the same two switches:
|
|
32
|
+
`MONEYER_STATS=false` or `MONEYER_STATS_RATIO_ONLY=true` leaves it off.
|
|
33
|
+
|
|
34
|
+
- Graded against `lnurlcash-conformance` 0.7.0. Its one addition is
|
|
35
|
+
`cash-derivation.json`, LUD-25's own seed-recoverable note secrets, which is
|
|
36
|
+
a wallet-side vector - a mint never sees a derivation, only `sha256(k1)`. So
|
|
37
|
+
nothing here changes; the pin moves so the grade names the current suite.
|
|
4
38
|
|
|
5
39
|
## 0.10.0 - 2026-09-04
|
|
6
40
|
|
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ default, and a variable set to an empty string counts as unset.
|
|
|
114
114
|
| `MONEYER_VERIFY` | `true` | the LUD-21 `verify` endpoint. Off means 404 |
|
|
115
115
|
| `MONEYER_WALLET_URL` | | a companion web wallet the mint's site links notes into |
|
|
116
116
|
| `MONEYER_SUNSET` | `false` | wind down: refuse anything that grows liabilities, keep every way out open |
|
|
117
|
+
| `MONEYER_SUNSET_DATE` | | the day this mint plans to close, ISO-8601 (`2026-12-31`). Published so a wallet can warn its holder while spending is still possible |
|
|
117
118
|
| `MONEYER_STATS` | `true` | the `/stats` endpoint. Off means 404 |
|
|
118
119
|
| `MONEYER_STATS_RATIO_ONLY` | `false` | publish the coverage ratio alone, without the size of the book |
|
|
119
120
|
| `MONEYER_STATS_PUBLISH` | `false` | publish a signed hourly snapshot of `/stats` to Nostr |
|
|
@@ -239,6 +240,13 @@ endpoint is public by design. `MONEYER_STATS=false` switches it off;
|
|
|
239
240
|
`MONEYER_STATS_RATIO_ONLY=true` publishes the ratio without the size of
|
|
240
241
|
the book.
|
|
241
242
|
|
|
243
|
+
The discovery document carries the same number as `outstandingNotesMsat`,
|
|
244
|
+
so a wallet reading a mint address learns what it owes without a second
|
|
245
|
+
request. Both switches govern it: an operator who has turned `/stats` off,
|
|
246
|
+
or down to the ratio, has already said not to publish the size of the
|
|
247
|
+
book, and a second endpoint doing it anyway would make those switches a
|
|
248
|
+
lie.
|
|
249
|
+
|
|
242
250
|
### Signed snapshots
|
|
243
251
|
|
|
244
252
|
With `MONEYER_STATS_PUBLISH=true`, and the Nostr identity that
|
package/dist/backends/cln.js
CHANGED
|
@@ -126,8 +126,15 @@ export const createClnBackend = (config) => {
|
|
|
126
126
|
async nodeInfo() {
|
|
127
127
|
const info = await mustCall('/v1/getinfo', {});
|
|
128
128
|
const id = info?.id;
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
// Every announced address, not just the first: a node reachable over
|
|
130
|
+
// both Tor and clearnet announces both, and a peer that can only use
|
|
131
|
+
// one of them needs to be told about it.
|
|
132
|
+
const uris = id
|
|
133
|
+
? (info?.address ?? [])
|
|
134
|
+
.filter((entry) => entry?.address && entry?.port)
|
|
135
|
+
.map((entry) => `${id}@${entry.address}:${entry.port}`)
|
|
136
|
+
: [];
|
|
137
|
+
const uri = uris[0] ?? id;
|
|
131
138
|
const color = typeof info?.color === 'string' ? `#${info.color.replace(/^#/, '')}` : undefined;
|
|
132
139
|
const numChannels = Number(info?.num_active_channels);
|
|
133
140
|
const numPeers = Number(info?.num_peers);
|
|
@@ -146,6 +153,7 @@ export const createClnBackend = (config) => {
|
|
|
146
153
|
return {
|
|
147
154
|
...(info?.alias ? { alias: info.alias } : {}),
|
|
148
155
|
...(uri ? { uri } : {}),
|
|
156
|
+
...(uris.length ? { uris } : {}),
|
|
149
157
|
...(color && /^#[0-9a-fA-F]{6}$/.test(color) ? { color } : {}),
|
|
150
158
|
...(Number.isSafeInteger(numChannels) ? { numChannels } : {}),
|
|
151
159
|
...(Number.isSafeInteger(numPeers) ? { numPeers } : {}),
|
package/dist/backends/lnd.js
CHANGED
|
@@ -224,6 +224,10 @@ export const createLndBackend = (config) => {
|
|
|
224
224
|
return {
|
|
225
225
|
...(res.json?.alias ? { alias: res.json.alias } : {}),
|
|
226
226
|
...(uris[0] || res.json?.identity_pubkey ? { uri: uris[0] ?? res.json.identity_pubkey } : {}),
|
|
227
|
+
// getinfo already hands back every announced address; `uri` only
|
|
228
|
+
// ever carried the first. A node with nothing announced falls back
|
|
229
|
+
// to the bare pubkey above and has no list to publish.
|
|
230
|
+
...(uris.length ? { uris } : {}),
|
|
227
231
|
...(color && /^#[0-9a-fA-F]{6}$/.test(color) ? { color } : {}),
|
|
228
232
|
// !== undefined narrows for exactOptionalPropertyTypes; the isFinite
|
|
229
233
|
// half keeps a NaN from an unparseable channel capacity out.
|
package/dist/backends/types.d.ts
CHANGED
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -94,6 +94,7 @@ export const configFromEnv = (env = process.env) => {
|
|
|
94
94
|
throw new Error('MONEYER_SIGNING_KEY must be 32 bytes of hex.');
|
|
95
95
|
}
|
|
96
96
|
const previousSigningPubkeys = previousPubkeysFromEnv(env, signingKey);
|
|
97
|
+
const sunsetDate = sunsetDateFromEnv(env);
|
|
97
98
|
const kind = env.MONEYER_BACKEND ?? 'fake';
|
|
98
99
|
let backend;
|
|
99
100
|
if (kind === 'fake') {
|
|
@@ -225,10 +226,33 @@ export const configFromEnv = (env = process.env) => {
|
|
|
225
226
|
...(env.MONEYER_WALLET_URL ? { walletUrl: env.MONEYER_WALLET_URL.replace(/\/+$/, '') } : {}),
|
|
226
227
|
maxK1s: int(env.MONEYER_MAX_K1S, DEFAULTS.maxK1s),
|
|
227
228
|
sunset: flag(env.MONEYER_SUNSET, DEFAULTS.sunset),
|
|
229
|
+
...(sunsetDate ? { sunsetDate } : {}),
|
|
228
230
|
...(zap ? { zap } : {}),
|
|
229
231
|
...(namePriceMsat !== undefined ? { namePriceMsat } : {})
|
|
230
232
|
};
|
|
231
233
|
};
|
|
234
|
+
// MONEYER_SUNSET_DATE="2026-12-31" - the day this mint plans to stop.
|
|
235
|
+
// Validated rather than passed through, because a wallet showing a holder
|
|
236
|
+
// "this mint closes on 31/12/2026" off a string nobody checked is worse
|
|
237
|
+
// than showing nothing: a typo that parses as a date in 2027 buys the
|
|
238
|
+
// holder confidence they have not earned, and one that parses as nothing
|
|
239
|
+
// silently disables the warning the operator thought they had switched on.
|
|
240
|
+
// Whole days only - an hour of notice is not notice.
|
|
241
|
+
const sunsetDateFromEnv = (env) => {
|
|
242
|
+
const raw = env.MONEYER_SUNSET_DATE?.trim();
|
|
243
|
+
if (!raw)
|
|
244
|
+
return undefined;
|
|
245
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
246
|
+
throw new Error('MONEYER_SUNSET_DATE must be an ISO-8601 date, e.g. 2026-12-31.');
|
|
247
|
+
}
|
|
248
|
+
// Round-tripped, so 2026-02-31 is caught: Date accepts it and rolls it
|
|
249
|
+
// forward to 3 March, which is not the day the operator typed.
|
|
250
|
+
const parsed = new Date(`${raw}T00:00:00Z`);
|
|
251
|
+
if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== raw) {
|
|
252
|
+
throw new Error(`MONEYER_SUNSET_DATE is not a real date: ${JSON.stringify(raw)}.`);
|
|
253
|
+
}
|
|
254
|
+
return raw;
|
|
255
|
+
};
|
|
232
256
|
// MONEYER_PREVIOUS_SIGNING_PUBKEYS="02ab...,03cd..." - the keys this mint
|
|
233
257
|
// signed under before the current one. Every entry must be a point the
|
|
234
258
|
// curve accepts, because a typo here would quietly tell wallets to accept
|
package/dist/landing.js
CHANGED
|
@@ -93,6 +93,7 @@ ${mintPubkey ? `<div class="kv"><span>notes signed by</span><code>${escapeHtml(m
|
|
|
93
93
|
${nodeInfo.uri ? `<div class="kv"><span>node</span><code>${escapeHtml(nodeInfo.uri)}</code></div>` : ''}
|
|
94
94
|
${coverageLine ? `<div class="kv"><span>coverage</span><b>${escapeHtml(coverageLine)}</b></div>` : ''}
|
|
95
95
|
${config.sunset ? '<div class="kv"><span>status</span><b>sunsetting - redeem only</b></div>' : ''}
|
|
96
|
+
${!config.sunset && config.sunsetDate ? `<div class="kv"><span>closing</span><b>${escapeHtml(config.sunsetDate)} - spend or move notes before then</b></div>` : ''}
|
|
96
97
|
${contacts.map(entry => `<div class="kv"><span>${entry.label}</span><code>${escapeHtml(entry.value)}</code></div>`).join('\n')}
|
|
97
98
|
${config.tosUrl ? `<div class="kv"><span>terms</span><a href="${escapeHtml(config.tosUrl)}" rel="noopener noreferrer">${escapeHtml(config.tosUrl)}</a></div>` : ''}
|
|
98
99
|
</div>
|
package/dist/server.js
CHANGED
|
@@ -249,7 +249,25 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
249
249
|
? { nodeCapacity: nodeInfo.capacityMsat, nodeCapacityMsat: nodeInfo.capacityMsat }
|
|
250
250
|
: {}),
|
|
251
251
|
...(nodeInfo.numChannels !== undefined ? { nodeNumChannels: nodeInfo.numChannels } : {}),
|
|
252
|
-
...(nodeInfo.numPeers !== undefined ? { nodeNumPeers: nodeInfo.numPeers } : {})
|
|
252
|
+
...(nodeInfo.numPeers !== undefined ? { nodeNumPeers: nodeInfo.numPeers } : {}),
|
|
253
|
+
// Every address the node announces. nodeUri stays, and stays the
|
|
254
|
+
// first of these: a wallet that only reads the singular field must
|
|
255
|
+
// keep working, and the reference mint publishes both for the same
|
|
256
|
+
// reason.
|
|
257
|
+
...(nodeInfo.uris?.length ? { nodeUris: nodeInfo.uris } : {}),
|
|
258
|
+
// Advance warning of a planned shutdown, so a wallet can tell its
|
|
259
|
+
// holder while spending is still possible.
|
|
260
|
+
...(config.sunsetDate ? { sunsetDate: config.sunsetDate } : {}),
|
|
261
|
+
// What this mint owes, in msat: the combined value of every note it
|
|
262
|
+
// has issued and not burned. A fact about its own database, so it
|
|
263
|
+
// survives an unreachable funding source - but it is the same
|
|
264
|
+
// disclosure /stats makes, so it answers to the same two switches. An
|
|
265
|
+
// operator who has turned stats off, or down to the coverage ratio
|
|
266
|
+
// alone, has already said not to put the size of the book on the
|
|
267
|
+
// internet, and this endpoint is not a way around that.
|
|
268
|
+
...(config.stats !== false && config.statsRatioOnly !== true
|
|
269
|
+
? { outstandingNotesMsat: store.liabilities().outstandingMsat }
|
|
270
|
+
: {})
|
|
253
271
|
});
|
|
254
272
|
// An hourly signed snapshot, so the coverage history can be checked
|
|
255
273
|
// after the fact rather than taken on the operator's word for it today.
|
|
@@ -819,12 +837,10 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
819
837
|
const note = h ? await resolveNoteId(h) : await resolveNote(k1);
|
|
820
838
|
if (!note)
|
|
821
839
|
return fail('Unknown note.');
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
// response as an unknown k1. A caller presenting the bearer k1 still
|
|
825
|
-
// receives the useful already-spent distinction.
|
|
840
|
+
// Hash lookups protect the secret, not the note's spent state.
|
|
841
|
+
// Retained burned rows let a wallet reconcile without revealing k1.
|
|
826
842
|
if (note.state === 'burned')
|
|
827
|
-
return fail(
|
|
843
|
+
return fail('Note already spent.');
|
|
828
844
|
// A note reserved by an in-flight melt is not withdrawable, and must
|
|
829
845
|
// not be advertised as though it were. LUD-25 makes this GET the way
|
|
830
846
|
// anyone checks what a note is worth, so answering "live, worth all
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@types/node": "^24.0.0",
|
|
71
71
|
"animejs": "^4.0.0",
|
|
72
72
|
"happy-dom": "^20.0.0",
|
|
73
|
-
"lnurlcash-conformance": "
|
|
73
|
+
"lnurlcash-conformance": "https://codeload.github.com/lnurlcash/lnurlcash-conformance/tar.gz/6c6aee66cf1b6cba72a03af976ecec113f470c9e",
|
|
74
74
|
"playwright": "^1.62.1",
|
|
75
75
|
"typescript": "^5.7.0",
|
|
76
76
|
"uqr": "^0.1.2",
|