@forgesworn/moneyer 0.2.0 → 0.3.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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-08-22
4
+
5
+ - **The mint fee is now ceilinged to a whole sat by default.**
6
+ `MONEYER_ROUND_FEE_TO_SAT` defaults to `true`, matching dni's
7
+ lnurl-mint, the reference. **An operator upgrading into this withholds
8
+ slightly more per mint than before** - up to one sat, whatever the ppm
9
+ cut worked out to - so it is called out here rather than changed
10
+ quietly. `MONEYER_ROUND_FEE_TO_SAT=false` restores the old behaviour.
11
+
12
+ Why: a mint that deals in fractions of a sat issues notes nothing
13
+ downstream can spend. An msat-exact fee makes notes like 94.9 sat, and
14
+ most Lightning wallets can only invoice whole sats, so such a note
15
+ cannot be withdrawn by them at all. The mint already covered that by
16
+ advertising `minWithdrawable` as the whole-sat floor and keeping the
17
+ remainder as dust; rounding at mint time means it never arises. Every
18
+ note this mint issues for a whole-sat payment is now worth a whole
19
+ number of sats, on both the pay callback and the zap path, which share
20
+ one fee function.
21
+
22
+ LUD-25 says nothing about rounding and `lnurlcash-kit`'s `mintFeeBand`
23
+ accepts either, so this is a posture change and not a compliance one.
24
+ The conformance grader passed both before and passes this now.
25
+
26
+ ## [0.2.1] - 2026-08-22
27
+
28
+ - `moneyer --dev` settles its invoices a moment after issuing them rather
29
+ than at the instant of issuance. Settling immediately made the mint
30
+ report an invoice nobody had paid as already settled, which on the wire
31
+ is indistinguishable from a mint handing out a note before it has been
32
+ paid for - the conformance grader failed it on exactly that, and was
33
+ right to. A dev mint should behave like a fast payer, not one that pays
34
+ before being asked. `autoSettleAfterMs` sets the delay; it defaults to
35
+ 1.5 seconds, which nobody developing against a local mint notices.
36
+ A freshly started `--dev` mint now grades 24 passed, 0 failed.
37
+
3
38
  ## [0.2.0] - 2026-08-22
4
39
 
5
40
  While LUD-25 is a draft, a `0.x` minor bump may be breaking; this one is
package/README.md CHANGED
@@ -87,7 +87,7 @@ default, and a variable set to an empty string counts as unset.
87
87
  | `MONEYER_PREVIOUS_SIGNING_PUBKEYS` | | compressed pubkeys this mint signed under before, comma separated (see below) |
88
88
  | `MONEYER_BASE_FEE_MSAT` | `0` | flat mint fee |
89
89
  | `MONEYER_FEE_PPM` | `0` | proportional mint fee, parts per million |
90
- | `MONEYER_ROUND_FEE_TO_SAT` | `false` | ceiling the mint fee to a whole sat (see below) |
90
+ | `MONEYER_ROUND_FEE_TO_SAT` | `true` | ceiling the mint fee to a whole sat (see below) |
91
91
  | `MONEYER_MIN_SENDABLE_MSAT` | `1000` | smallest payment the mint advertises |
92
92
  | `MONEYER_MAX_SENDABLE_MSAT` | `100000000` | largest payment the mint advertises |
93
93
  | `MONEYER_MIN_MINT_MSAT` | `1000` | dust floor: the smallest note the mint will strike |
@@ -269,16 +269,20 @@ the msat-exact amount. Neither is out of spec - lnurlcash-kit's
269
269
  `mintFeeBand` treats anything between the two as the mint keeping its
270
270
  word, and the conformance grader accepts both.
271
271
 
272
- `MONEYER_ROUND_FEE_TO_SAT=true` switches this mint to the reference's
273
- behaviour. Off by default: turning it on raises what the mint withholds,
274
- and that is not a change to make behind an operator's back on a redeploy.
275
-
276
- Why you probably want it on anyway: msat-exact fees make notes like 94.9
277
- sat, and most Lightning wallets can only invoice whole sats, so such a
278
- note cannot be withdrawn by them. moneyer covers the notes already out
279
- there by advertising `minWithdrawable` as the note floored to a whole sat
280
- and accepting a melt for that; the sub-sat remainder is dust the mint
281
- keeps. Rounding at mint time means it never comes up.
272
+ **moneyer rounds, by default.** A mint that deals in fractions of a sat
273
+ issues notes nothing downstream can spend: msat-exact fees make notes like
274
+ 94.9 sat, and most Lightning wallets can only invoice whole sats, so such
275
+ a note cannot be withdrawn by them at all. moneyer covers the notes
276
+ already out there by advertising `minWithdrawable` as the note floored to
277
+ a whole sat and accepting a melt for that, keeping the sub-sat remainder
278
+ as dust - a wart it should not need. Rounding at mint time means it never
279
+ comes up, and every note this mint issues for a whole-sat payment is worth
280
+ a whole number of sats.
281
+
282
+ `MONEYER_ROUND_FEE_TO_SAT=false` restores the msat-exact fee. An operator
283
+ upgrading into this default withholds slightly more per mint than before,
284
+ which is why the changelog says so plainly rather than letting a redeploy
285
+ change it quietly.
282
286
 
283
287
  ## Melting to an invoice with no amount
284
288
 
@@ -1,7 +1,9 @@
1
1
  import { type LightningBackend } from './types.ts';
2
2
  export type FakePayMode = 'succeed' | 'fail-clean' | 'fail-then-paid' | 'ambiguous-paid' | 'ambiguous-unpaid' | 'ambiguous-pending';
3
+ export declare const DEFAULT_AUTO_SETTLE_AFTER_MS = 1500;
3
4
  export type FakeBackendOptions = {
4
5
  autoSettle?: boolean;
6
+ autoSettleAfterMs?: number;
5
7
  };
6
8
  export type FakeBackend = LightningBackend & {
7
9
  control: {
@@ -4,10 +4,28 @@ import { hexToBytes } from '@noble/hashes/utils.js';
4
4
  import { bolt11PaymentHash } from 'farrier-kit/bolt11';
5
5
  import { fakeBolt11 } from "./fake-bolt11.js";
6
6
  import { PaymentAlreadyKnownError, PaymentFailedError, PaymentPendingError } from "./types.js";
7
+ // Long enough that an unpaid invoice reads as unpaid to anything looking
8
+ // at it in the same breath it was issued, short enough that nobody
9
+ // developing against a dev mint notices the wait.
10
+ export const DEFAULT_AUTO_SETTLE_AFTER_MS = 1_500;
11
+ // An invoice is paid if something paid it, or if autoSettle's moment has
12
+ // arrived. Read rather than scheduled: no timer to leak when a mint is
13
+ // closed, and a test that fakes the clock sees the same answer.
14
+ const isSettled = (invoice) => {
15
+ if (!invoice)
16
+ return false;
17
+ if (invoice.settled)
18
+ return true;
19
+ return invoice.settleAt !== null && Date.now() >= invoice.settleAt;
20
+ };
7
21
  // One bitcoin, so a development mint covers anything it is likely to mint.
8
22
  export const FAKE_LOCAL_BALANCE_MSAT = 100_000_000_000;
9
23
  export const createFakeBackend = (options = {}) => {
10
24
  const autoSettle = options.autoSettle === true;
25
+ const autoSettleAfterMs = options.autoSettleAfterMs ?? DEFAULT_AUTO_SETTLE_AFTER_MS;
26
+ // `settleAt` is when autoSettle starts calling this invoice paid; null
27
+ // for an invoice only control.settleInvoice can settle, which is every
28
+ // invoice unless autoSettle is on.
11
29
  const invoices = new Map();
12
30
  const payments = new Map();
13
31
  const knownPreimages = new Map();
@@ -18,7 +36,12 @@ export const createFakeBackend = (options = {}) => {
18
36
  async createInvoice({ amountMsat, preimageHex, memo }) {
19
37
  const paymentHashHex = bytesToHex(sha256(hexToBytes(preimageHex)));
20
38
  const pr = fakeBolt11({ amountMsat, paymentHashHex, memo });
21
- invoices.set(paymentHashHex, { preimageHex, amountMsat, settled: autoSettle });
39
+ invoices.set(paymentHashHex, {
40
+ preimageHex,
41
+ amountMsat,
42
+ settled: false,
43
+ settleAt: autoSettle ? Date.now() + autoSettleAfterMs : null
44
+ });
22
45
  return { pr };
23
46
  },
24
47
  async payInvoice({ pr, amountMsat }) {
@@ -63,11 +86,11 @@ export const createFakeBackend = (options = {}) => {
63
86
  return payment.status === 'complete';
64
87
  },
65
88
  async isInvoiceSettled(paymentHashHex) {
66
- return invoices.get(paymentHashHex)?.settled ?? false;
89
+ return isSettled(invoices.get(paymentHashHex));
67
90
  },
68
91
  async invoicePreimage(paymentHashHex) {
69
92
  const invoice = invoices.get(paymentHashHex);
70
- return invoice?.settled ? invoice.preimageHex : null;
93
+ return isSettled(invoice) ? invoice.preimageHex : null;
71
94
  },
72
95
  async paymentPreimage(paymentHashHex) {
73
96
  const payment = payments.get(paymentHashHex);
@@ -113,7 +136,11 @@ export const createFakeBackend = (options = {}) => {
113
136
  localBalanceMsat = msat;
114
137
  },
115
138
  invoiceByHash(paymentHashHex) {
116
- return invoices.get(paymentHashHex);
139
+ const invoice = invoices.get(paymentHashHex);
140
+ // `settled` here answers "is it paid now", so autoSettle's moment
141
+ // counts. A caller reading the stored flag would see false for an
142
+ // invoice the mint itself treats as paid.
143
+ return invoice && { ...invoice, settled: isSettled(invoice) };
117
144
  }
118
145
  }
119
146
  };
package/dist/config.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { MintFee } from 'lnurlcash-kit';
2
2
  export type BackendConfig = {
3
3
  kind: 'fake';
4
4
  autoSettle?: boolean;
5
+ autoSettleAfterMs?: number;
5
6
  } | {
6
7
  kind: 'cln';
7
8
  url: string;
@@ -64,7 +65,7 @@ export declare const DEFAULTS: {
64
65
  readonly verify: true;
65
66
  readonly maxK1s: 21;
66
67
  readonly sunset: false;
67
- readonly roundFeeToSat: false;
68
+ readonly roundFeeToSat: true;
68
69
  readonly stats: true;
69
70
  readonly statsRatioOnly: false;
70
71
  readonly statsPublish: false;
package/dist/config.js CHANGED
@@ -14,7 +14,7 @@ export const DEFAULTS = {
14
14
  verify: true,
15
15
  maxK1s: 21,
16
16
  sunset: false,
17
- roundFeeToSat: false,
17
+ roundFeeToSat: true,
18
18
  stats: true,
19
19
  statsRatioOnly: false,
20
20
  statsPublish: false,
package/dist/server.js CHANGED
@@ -49,7 +49,12 @@ const wholeSatFloor = (msat) => Math.floor(msat / 1000) * 1000;
49
49
  const backendFor = (config) => {
50
50
  switch (config.backend.kind) {
51
51
  case 'fake':
52
- return createFakeBackend({ autoSettle: config.backend.autoSettle === true });
52
+ return createFakeBackend({
53
+ autoSettle: config.backend.autoSettle === true,
54
+ ...(config.backend.autoSettleAfterMs === undefined
55
+ ? {}
56
+ : { autoSettleAfterMs: config.backend.autoSettleAfterMs })
57
+ });
53
58
  case 'cln':
54
59
  return createClnBackend(config.backend);
55
60
  case 'lnd':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/moneyer",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",