@forgesworn/moneyer 0.1.1 → 0.1.2

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.2] - 2026-08-21
4
+
5
+ - `MONEYER_ROUND_FEE_TO_SAT` ceilings the mint fee to a whole sat, as
6
+ dni's lnurl-mint does on purpose so the mint is "never short a sat".
7
+ LUD-25 says nothing about rounding and `lnurlcash-kit`'s `mintFeeBand`
8
+ accepts both readings, so this is a posture choice rather than a
9
+ compliance one. **Off by default**: turning it on raises what the mint
10
+ withholds, which is not a change to make on a redeploy.
11
+ - The mint path called `applyMintFee` directly rather than the fee helper,
12
+ so it would have ignored that setting entirely - advertising one fee and
13
+ withholding another. Every fee site now goes through one
14
+ `netAfterMintFee`.
15
+ - `roundFeeToSat` is optional on `MoneyerConfig`: that type is public API,
16
+ and an embedder constructing a config should not have to name a field it
17
+ does not care about.
18
+
3
19
  ## [0.1.1] - 2026-08-21
4
20
 
5
21
  - `withdrawLink` is now the plain URL (`https://host/w`), the form
package/README.md CHANGED
@@ -97,6 +97,19 @@ import {createMoneyer, configFromEnv} from '@forgesworn/moneyer'
97
97
  const mint = await createMoneyer(configFromEnv())
98
98
  ```
99
99
 
100
+ ## The mint fee, and the rounding question
101
+
102
+ LUD-25 gives the fee as a flat `base_fee_msat` plus a ppm cut and says
103
+ nothing about rounding. dni's lnurl-mint, the reference, ceilings it to a
104
+ whole sat on purpose so the mint is never short a sat; moneyer withholds
105
+ the msat-exact amount. Neither is out of spec - lnurlcash-kit's
106
+ `mintFeeBand` treats anything between the two as the mint keeping its
107
+ word, and the conformance grader accepts both.
108
+
109
+ `MONEYER_ROUND_FEE_TO_SAT=true` switches this mint to the reference's
110
+ behaviour. Off by default: turning it on raises what the mint withholds,
111
+ and that is not a change to make behind an operator's back on a redeploy.
112
+
100
113
  ## Endpoints
101
114
 
102
115
  | | |
package/dist/config.d.ts CHANGED
@@ -20,6 +20,7 @@ export type MoneyerConfig = {
20
20
  maxSendableMsat: number;
21
21
  minMintMsat: number;
22
22
  mintFee: MintFee | null;
23
+ roundFeeToSat?: boolean;
23
24
  signingKey?: string;
24
25
  dbPath: string;
25
26
  backend: BackendConfig;
@@ -40,5 +41,6 @@ export declare const DEFAULTS: {
40
41
  readonly verify: true;
41
42
  readonly maxK1s: 21;
42
43
  readonly sunset: false;
44
+ readonly roundFeeToSat: false;
43
45
  };
44
46
  export declare const configFromEnv: (env?: NodeJS.ProcessEnv) => MoneyerConfig;
package/dist/config.js CHANGED
@@ -9,7 +9,8 @@ export const DEFAULTS = {
9
9
  dbPath: 'moneyer.sqlite',
10
10
  verify: true,
11
11
  maxK1s: 21,
12
- sunset: false
12
+ sunset: false,
13
+ roundFeeToSat: false
13
14
  };
14
15
  const int = (value, fallback) => {
15
16
  if (value === undefined || value === '')
@@ -89,6 +90,7 @@ export const configFromEnv = (env = process.env) => {
89
90
  maxSendableMsat,
90
91
  minMintMsat: int(env.MONEYER_MIN_MINT_MSAT, DEFAULTS.minMintMsat),
91
92
  mintFee: baseFeeMsat === 0 && feePpm === 0 ? null : { baseFeeMsat, feePpm },
93
+ roundFeeToSat: flag(env.MONEYER_ROUND_FEE_TO_SAT, DEFAULTS.roundFeeToSat),
92
94
  ...(signingKey ? { signingKey: signingKey.toLowerCase() } : {}),
93
95
  dbPath: env.MONEYER_DB ?? DEFAULTS.dbPath,
94
96
  backend,
package/dist/server.js CHANGED
@@ -40,7 +40,17 @@ export const createMoneyer = async (config, deps = {}) => {
40
40
  // Melt payment hashes with a live attempt in this process. Reconcile
41
41
  // skips these - see melt.ts.
42
42
  const inFlight = new Set();
43
- const mintFeeMsat = (grossMsat) => config.mintFee ? grossMsat - applyMintFee(grossMsat, config.mintFee) : 0;
43
+ const mintFeeMsat = (grossMsat) => {
44
+ if (!config.mintFee)
45
+ return 0;
46
+ const exact = grossMsat - applyMintFee(grossMsat, config.mintFee);
47
+ // Both readings sit inside lnurlcash-kit's mintFeeBand, so a wallet
48
+ // is not misled either way - it is told the range up front.
49
+ return config.roundFeeToSat === true ? Math.ceil(exact / 1000) * 1000 : exact;
50
+ };
51
+ // Every quote and every credit goes through here, so the fee posture
52
+ // cannot drift between what is advertised and what is withheld.
53
+ const netAfterMintFee = (grossMsat) => Math.max(0, grossMsat - mintFeeMsat(grossMsat));
44
54
  // What the mint advertises as its minimum must survive its own fee: a
45
55
  // payRequest whose minSendable nets below the dust floor invites a
46
56
  // payment it would then refuse.
@@ -164,7 +174,7 @@ export const createMoneyer = async (config, deps = {}) => {
164
174
  callback: `${origin}/w`,
165
175
  minWithdrawable: config.minMintMsat,
166
176
  maxWithdrawable: config.mintFee
167
- ? applyMintFee(config.maxSendableMsat, config.mintFee)
177
+ ? netAfterMintFee(config.maxSendableMsat)
168
178
  : config.maxSendableMsat,
169
179
  defaultDescription: config.description,
170
180
  payLink: `${origin}/.well-known/lnurlp/${lnurlwMatch[1]}`,
@@ -187,7 +197,7 @@ export const createMoneyer = async (config, deps = {}) => {
187
197
  if (amount < effectiveMinSendableMsat || amount > config.maxSendableMsat) {
188
198
  return fail('Amount out of range.');
189
199
  }
190
- const net = config.mintFee ? applyMintFee(amount, config.mintFee) : amount;
200
+ const net = netAfterMintFee(amount);
191
201
  if (net < config.minMintMsat)
192
202
  return fail('Amount too small to mint a note.');
193
203
  // The preimage is the future note's spend secret; its hash is the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/moneyer",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",
@@ -45,7 +45,7 @@
45
45
  "test:watch": "vitest",
46
46
  "typecheck": "tsc --noEmit",
47
47
  "dev": "node --experimental-strip-types src/cli.ts --dev",
48
- "conform": "node test/run-grader.mjs",
48
+ "conform": "vitest run test/conformance.test.ts",
49
49
  "check": "npm run typecheck && npm run typecheck:web && npm test && npm run build && npm run web:build && npm pack --dry-run",
50
50
  "web:dev": "vite web",
51
51
  "web:build": "vite build web",