@forgesworn/moneyer 0.1.0-next.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 +40 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/SECURITY.md +29 -0
- package/THREAT-MODEL.md +104 -0
- package/dist/backends/cln.d.ts +5 -0
- package/dist/backends/cln.js +130 -0
- package/dist/backends/fake-bolt11.d.ts +6 -0
- package/dist/backends/fake-bolt11.js +60 -0
- package/dist/backends/fake.d.ts +17 -0
- package/dist/backends/fake.js +104 -0
- package/dist/backends/lnd.d.ts +5 -0
- package/dist/backends/lnd.js +191 -0
- package/dist/backends/types.d.ts +38 -0
- package/dist/backends/types.js +25 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +61 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +100 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +23 -0
- package/dist/landing.d.ts +8 -0
- package/dist/landing.js +63 -0
- package/dist/melt.d.ts +17 -0
- package/dist/melt.js +91 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.js +502 -0
- package/dist/signing.d.ts +6 -0
- package/dist/signing.js +37 -0
- package/dist/store.d.ts +53 -0
- package/dist/store.js +245 -0
- package/dist/web-assets.d.ts +12 -0
- package/dist/web-assets.js +51 -0
- package/llms.txt +64 -0
- package/package.json +76 -0
- package/web/dist/art/plate-l.webp +0 -0
- package/web/dist/art/plate-p.webp +0 -0
- package/web/dist/assets/cinzel-latin-400-normal-C8jUSQqm.woff +0 -0
- package/web/dist/assets/cinzel-latin-400-normal-DnUIPmzd.woff2 +0 -0
- package/web/dist/assets/cinzel-latin-700-normal-C-gK7hA8.woff +0 -0
- package/web/dist/assets/cinzel-latin-700-normal-Dkw14w9r.woff2 +0 -0
- package/web/dist/assets/cinzel-latin-ext-400-normal-DJ0Lq8y-.woff +0 -0
- package/web/dist/assets/cinzel-latin-ext-400-normal-XQK_CSAr.woff2 +0 -0
- package/web/dist/assets/cinzel-latin-ext-700-normal-C24KFjuG.woff2 +0 -0
- package/web/dist/assets/cinzel-latin-ext-700-normal-CORa-yIv.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff +0 -0
- package/web/dist/assets/index-BGYBQuiQ.css +1 -0
- package/web/dist/assets/index-CIuRQd9k.js +130 -0
- package/web/dist/assets/spectral-cyrillic-400-italic-C6o5RXh-.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-400-italic-C95RxGpc.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-400-normal-Bx9Tn3WZ.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-400-normal-X3p2PSzL.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-500-italic-BLpmaxIM.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-500-italic-D14WfPba.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-600-normal-Dl75bDKp.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-600-normal-veT-ELBO.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-400-italic-C6Gmo4JY.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-400-italic-CZOVIkJA.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-400-normal--n_iQ4wr.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-400-normal-B4-YHs9i.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-500-italic-CxkamsWL.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-500-italic-DGZ5KH5h.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-600-normal-BVATWin6.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-600-normal-CvRHnPuy.woff2 +0 -0
- package/web/dist/assets/spectral-latin-400-italic-CDO25mTW.woff2 +0 -0
- package/web/dist/assets/spectral-latin-400-italic-Ds-NcNVC.woff +0 -0
- package/web/dist/assets/spectral-latin-400-normal-CTVgUekv.woff2 +0 -0
- package/web/dist/assets/spectral-latin-400-normal-D34IWA1h.woff +0 -0
- package/web/dist/assets/spectral-latin-500-italic-CqeZqSS8.woff2 +0 -0
- package/web/dist/assets/spectral-latin-500-italic-DxwFSvPk.woff +0 -0
- package/web/dist/assets/spectral-latin-600-normal-DE3OxvnQ.woff2 +0 -0
- package/web/dist/assets/spectral-latin-600-normal-DXnQJnYb.woff +0 -0
- package/web/dist/assets/spectral-latin-ext-400-italic-BcKU4A_a.woff +0 -0
- package/web/dist/assets/spectral-latin-ext-400-italic-BgTM1Mst.woff2 +0 -0
- package/web/dist/assets/spectral-latin-ext-400-normal-BHbqXr-7.woff +0 -0
- package/web/dist/assets/spectral-latin-ext-400-normal-XDLFwhqg.woff2 +0 -0
- package/web/dist/assets/spectral-latin-ext-500-italic-DMigwuN9.woff2 +0 -0
- package/web/dist/assets/spectral-latin-ext-500-italic-UnF3HUpx.woff +0 -0
- package/web/dist/assets/spectral-latin-ext-600-normal-DKlDBbgb.woff2 +0 -0
- package/web/dist/assets/spectral-latin-ext-600-normal-DnKmvInL.woff +0 -0
- package/web/dist/assets/spectral-vietnamese-400-italic-D8dhja4j.woff +0 -0
- package/web/dist/assets/spectral-vietnamese-400-italic-DO3bsfcj.woff2 +0 -0
- package/web/dist/assets/spectral-vietnamese-400-normal-C7CRHYqk.woff2 +0 -0
- package/web/dist/assets/spectral-vietnamese-400-normal-CWkMWCzb.woff +0 -0
- package/web/dist/assets/spectral-vietnamese-500-italic-e7VYE3f9.woff2 +0 -0
- package/web/dist/assets/spectral-vietnamese-500-italic-q7ilnnst.woff +0 -0
- package/web/dist/assets/spectral-vietnamese-600-normal-ZNiwCzjU.woff2 +0 -0
- package/web/dist/assets/spectral-vietnamese-600-normal-esmjhrFa.woff +0 -0
- package/web/dist/index.html +19 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-08-20
|
|
4
|
+
|
|
5
|
+
Initial implementation.
|
|
6
|
+
|
|
7
|
+
- The six LUD-25 endpoints: payRequest, mint address discovery, pay
|
|
8
|
+
callback, LUD-21 verify (mint and melt sides), informational GET, and
|
|
9
|
+
the mutating callback (melt, rotate, split, merge).
|
|
10
|
+
- Funding sources: cln (clnrest) and lnd (REST), both with
|
|
11
|
+
caller-supplied invoice preimages; a fake backend for development and
|
|
12
|
+
tests, refused outside `--dev`.
|
|
13
|
+
- SQLite store keyed by note id (sha256 of the secret) - secrets are never
|
|
14
|
+
persisted. Synchronous atomic mutations.
|
|
15
|
+
- In-process note signing with a dedicated mint key, agreeing byte for
|
|
16
|
+
byte with lnurlcash-kit's verifier.
|
|
17
|
+
- The melt discipline: respond-then-pay, confirm-before-restore,
|
|
18
|
+
pending-not-guessed, reconciliation at startup and on demand.
|
|
19
|
+
- Passes the lnurlcash-conformance grader, read-only and spending runs;
|
|
20
|
+
the grader executes inside the test suite.
|
|
21
|
+
- The LUD-25 fee algebra on mutations: base_fee_msat deducted from every
|
|
22
|
+
split's change (never the requested amount), (n-1) base fees refunded
|
|
23
|
+
into a merge of n notes, 'insufficient value' when change cannot cover
|
|
24
|
+
the fee or would land at zero.
|
|
25
|
+
- The mint's own website at GET /: mint a note in the browser (invoice
|
|
26
|
+
QR, LUD-21 countdown polling, claim-and-immediately-rotate so the
|
|
27
|
+
verify-exposed preimage dies before the note is shown, offline
|
|
28
|
+
signature verification in front of the user, scratch-off silver over the QR), check
|
|
29
|
+
a note (live/spent/unknown/pending in plain words), the terms and the
|
|
30
|
+
funding node. vite + anime.js + lnurlcash-kit, served from memory by
|
|
31
|
+
the mint process; falls back to a self-contained zero-script landing
|
|
32
|
+
page when no web build exists. `MONEYER_WALLET_URL` links minted notes
|
|
33
|
+
into a companion wallet's claim route.
|
|
34
|
+
- Node statistics (capacity, channels, peers) on the discovery endpoint,
|
|
35
|
+
best-effort from the funding source.
|
|
36
|
+
- Shared funding sources are a supported deployment: the melt path
|
|
37
|
+
pre-checks the node's payment history and surfaces the node's
|
|
38
|
+
"payment already exists" refusal as a distinct, note-restoring
|
|
39
|
+
outcome, closing the cross-mint melt replay a shared node otherwise
|
|
40
|
+
allows.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TheCryptoDonkey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# moneyer
|
|
2
|
+
|
|
3
|
+
> An LNURLcash (LUD-25) mint. A moneyer was the mediaeval craftsman licensed
|
|
4
|
+
> to strike coins; this one strikes Lightning bearer notes.
|
|
5
|
+
|
|
6
|
+
moneyer is an independent implementation of the LUD-25 draft: paying an
|
|
7
|
+
invoice it issues mints a bearer note whose spend secret is that invoice's
|
|
8
|
+
payment preimage, and a note's holder can rotate, split, merge and melt it
|
|
9
|
+
against the withdraw callback. It passes the full
|
|
10
|
+
[lnurlcash-conformance](https://github.com/TheCryptoDonkey/lnurlcash-conformance)
|
|
11
|
+
grader, including the spending checks, and the grader runs in this repo's
|
|
12
|
+
own test suite.
|
|
13
|
+
|
|
14
|
+
Reference stack, by dni (MIT): the [LUD-25 draft](https://github.com/lnurl/luds/pull/301),
|
|
15
|
+
[lnurl-mint](https://github.com/dni/lnurl-mint), [lnurl-wallet](https://github.com/dni/lnurl-wallet).
|
|
16
|
+
moneyer shares no code with them; where their behaviour encodes a safety
|
|
17
|
+
lesson, that behaviour is kept deliberately and tested.
|
|
18
|
+
|
|
19
|
+
## What it is
|
|
20
|
+
|
|
21
|
+
- TypeScript, ESM, Node 24+. `node:http` and `node:sqlite`; no web
|
|
22
|
+
framework, no ORM.
|
|
23
|
+
- Funding sources: **cln** (clnrest) and **lnd** (REST), both of which
|
|
24
|
+
accept a caller-supplied invoice preimage - the capability a LUD-25 mint
|
|
25
|
+
cannot exist without. phoenixd and NIP-47 `make_invoice` do not offer it,
|
|
26
|
+
which is why neither can back a mint. A **fake** backend exists for
|
|
27
|
+
development and tests and refuses to run outside `--dev`.
|
|
28
|
+
- Notes are stored by id, `sha256(k1)` - the store never holds a spend
|
|
29
|
+
secret. A freshly minted note's preimage lives only with the payer and
|
|
30
|
+
the funding source.
|
|
31
|
+
- Signs every note it mints with its own mint key (secp256k1, the standard
|
|
32
|
+
`Lightning Signed Message` construction) for LUD-25 offline verification.
|
|
33
|
+
- The melt discipline: reply OK when the note is reserved, pay in the
|
|
34
|
+
background, burn only on confirmed payment, restore only on confirmed
|
|
35
|
+
non-payment, and park everything else as pending for reconciliation -
|
|
36
|
+
which also runs at startup, so a crash mid-melt never guesses.
|
|
37
|
+
|
|
38
|
+
## Run
|
|
39
|
+
|
|
40
|
+
Not on npm yet - build from source, with the sibling repos it links
|
|
41
|
+
against until those publish:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/TheCryptoDonkey/lnurlcash-kit
|
|
45
|
+
git clone https://github.com/TheCryptoDonkey/lnurlcash-conformance
|
|
46
|
+
git clone https://github.com/forgesworn/moneyer
|
|
47
|
+
(cd lnurlcash-kit && npm install && npm run build)
|
|
48
|
+
cd moneyer && npm install && npm run build && npm run web:build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# development: fake funding source, in-memory store, a funded note printed
|
|
53
|
+
node dist/cli.js --dev
|
|
54
|
+
|
|
55
|
+
# production shape
|
|
56
|
+
MONEYER_BACKEND=cln \
|
|
57
|
+
MONEYER_BACKEND_URL=https://127.0.0.1:3010 \
|
|
58
|
+
MONEYER_BACKEND_RUNE=... \
|
|
59
|
+
MONEYER_SIGNING_KEY=<32 bytes hex> \
|
|
60
|
+
MONEYER_PUBLIC_ORIGIN=https://mint.example \
|
|
61
|
+
MONEYER_DB=/var/lib/moneyer/mint.sqlite \
|
|
62
|
+
node dist/cli.js
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The mint is then payable at `mint@mint.example` (and the bare-domain `_`
|
|
66
|
+
alias). Configuration is environment-only; see `src/config.ts` for the
|
|
67
|
+
full `MONEYER_*` set including fees (`MONEYER_BASE_FEE_MSAT`,
|
|
68
|
+
`MONEYER_FEE_PPM`), limits, and `MONEYER_SUNSET` for winding down without
|
|
69
|
+
stranding holders.
|
|
70
|
+
|
|
71
|
+
## The website
|
|
72
|
+
|
|
73
|
+
`GET /` serves the mint's own site (`web/`, vite + anime.js, built by
|
|
74
|
+
`npm run web:build` into memory-served static files). It is a wallet-grade
|
|
75
|
+
LNURLcash client in its own right, driven entirely by `lnurlcash-kit`
|
|
76
|
+
against the same endpoints every wallet uses:
|
|
77
|
+
|
|
78
|
+
- **Mint a note in the browser**: amount in, fee grossed up and shown
|
|
79
|
+
before the invoice exists, invoice QR (tap opens a wallet), LUD-21
|
|
80
|
+
polling with a countdown, and on settlement the note is claimed and
|
|
81
|
+
**immediately rotated** - the preimage any invoice-observer could poll
|
|
82
|
+
out of verify is dead before the note is shown. The rotated note's
|
|
83
|
+
signature is verified against the mint's advertised key in front of the
|
|
84
|
+
user, and the QR arrives under scratch-off silver foil, rubbed away like a scratch card.
|
|
85
|
+
- **Check a note**: live value, spent/unknown/pending classified in plain
|
|
86
|
+
words, offline signature verification.
|
|
87
|
+
- `MONEYER_WALLET_URL` (optional) links minted notes straight into a
|
|
88
|
+
companion web wallet's `#/claim` route.
|
|
89
|
+
|
|
90
|
+
Without a web build the server falls back to a self-contained, zero-script
|
|
91
|
+
landing page, so an npm-installed mint still has a face.
|
|
92
|
+
|
|
93
|
+
As a library:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import {createMoneyer, configFromEnv} from '@forgesworn/moneyer'
|
|
97
|
+
const mint = await createMoneyer(configFromEnv())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Endpoints
|
|
101
|
+
|
|
102
|
+
| | |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `/.well-known/lnurlp/<user>` | LUD-16 payRequest; paying mints a note |
|
|
105
|
+
| `/.well-known/lnurlw/<user>` | LUD-25 mint address discovery (experimental) |
|
|
106
|
+
| `/p/cb` | LUD-06 pay callback; issues the mint invoice |
|
|
107
|
+
| `/verify/<hash>` | LUD-21 verify, for mint invoices and melt payments |
|
|
108
|
+
| `/w` | LUD-03 informational GET |
|
|
109
|
+
| `/w/cb` | the mutating callback: melt, rotate, split, merge |
|
|
110
|
+
|
|
111
|
+
## Testing
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm test # vitest: protocol, signing, melt discipline, conformance
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The conformance grader runs inside the suite against an in-process mint,
|
|
118
|
+
read-only and spending paths both. The cln and lnd backends are direct
|
|
119
|
+
ports of the reference mint's semantics but have not yet been exercised
|
|
120
|
+
against live nodes; treat them as beta until they have.
|
|
121
|
+
|
|
122
|
+
## Dogfood
|
|
123
|
+
|
|
124
|
+
Part of the ForgeSworn LNURLcash stack: built on
|
|
125
|
+
[`lnurlcash-kit`](https://github.com/TheCryptoDonkey/lnurlcash-kit) (fee
|
|
126
|
+
maths, hashing, signature agreement) and
|
|
127
|
+
[`farrier-kit`](https://github.com/forgesworn/farrier-kit) (BOLT-11
|
|
128
|
+
decoding and preimage verification on the melt path). The companion wallet
|
|
129
|
+
is [`@forgesworn/notecase`](https://github.com/forgesworn/notecase).
|
|
130
|
+
|
|
131
|
+
## Licence
|
|
132
|
+
|
|
133
|
+
MIT.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
moneyer moves real money. If you find a way to make it move the wrong way,
|
|
4
|
+
please report it privately.
|
|
5
|
+
|
|
6
|
+
## Reporting
|
|
7
|
+
|
|
8
|
+
Open a GitHub security advisory on this repository (Security tab, "Report
|
|
9
|
+
a vulnerability"), or contact the maintainer privately. Please do not open
|
|
10
|
+
a public issue for anything that could cost an operator or a holder funds
|
|
11
|
+
before a fix exists.
|
|
12
|
+
|
|
13
|
+
Include what you can: the endpoint or flow, a reproduction against
|
|
14
|
+
`moneyer --dev` (never against someone's live mint), and what an attacker
|
|
15
|
+
gains.
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
|
|
19
|
+
In scope: anything that burns, mints, restores or reveals a note contrary
|
|
20
|
+
to the rules in THREAT-MODEL.md; anything that makes the melt discipline
|
|
21
|
+
guess; anything that lets a request cross the store's atomicity.
|
|
22
|
+
|
|
23
|
+
Out of scope: denial of service against an unproxied dev deployment,
|
|
24
|
+
vulnerabilities in the funding source itself, and social engineering.
|
|
25
|
+
|
|
26
|
+
## Supported versions
|
|
27
|
+
|
|
28
|
+
Pre-1.0: only the latest release is supported. Pin an exact version; the
|
|
29
|
+
LUD-25 spec is still a draft and wire behaviour may follow it.
|
package/THREAT-MODEL.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Threat model
|
|
2
|
+
|
|
3
|
+
moneyer holds other people's money as bearer liabilities. The store keeps
|
|
4
|
+
note ids (hashes), amounts and states; the spend secrets exist only as
|
|
5
|
+
invoice preimages at the funding source and in holders' wallets.
|
|
6
|
+
|
|
7
|
+
## Assets
|
|
8
|
+
|
|
9
|
+
- The outstanding note set: every `outstanding` row is money owed to
|
|
10
|
+
whoever holds its secret.
|
|
11
|
+
- The funding source credentials (rune or macaroon): full spending power
|
|
12
|
+
over the mint's liquidity.
|
|
13
|
+
- The mint signing key: whoever holds it can issue "verified" notes.
|
|
14
|
+
|
|
15
|
+
## Adversaries and defences
|
|
16
|
+
|
|
17
|
+
**A holder replaying or racing mutations.** Every mutation validates and
|
|
18
|
+
transitions state in one synchronous SQLite transaction with no await
|
|
19
|
+
inside it; two concurrent callbacks for the same k1 cannot both win. A
|
|
20
|
+
burned k1 answers `Invalid or already spent k1.` atomically. And every
|
|
21
|
+
LNURL endpoint answers GET only, so a preflight or any other non-GET
|
|
22
|
+
request carrying a callback's query string cannot mutate anything.
|
|
23
|
+
|
|
24
|
+
**A holder inflating a merge or split with a repeated k1.** Duplicated k1
|
|
25
|
+
parameters in one request are refused outright; they would otherwise count
|
|
26
|
+
one note's value twice into the output.
|
|
27
|
+
|
|
28
|
+
**A holder claiming an output id that already exists.** `h`/`h2` may not
|
|
29
|
+
collide with any existing note OR any mint invoice's payment hash, settled
|
|
30
|
+
or not. The invoice case is the subtle one: `/verify` hands a settled mint
|
|
31
|
+
invoice's preimage to its payer, and that preimage is the k1 of whatever
|
|
32
|
+
note carries the hash as its id - minting "over" such an id would point a
|
|
33
|
+
future payer's money at a stranger's note.
|
|
34
|
+
|
|
35
|
+
**A holder melting into the mint's own invoice.** Refused synchronously:
|
|
36
|
+
paying it would route the funding source's money at itself, which real
|
|
37
|
+
nodes handle inconsistently.
|
|
38
|
+
|
|
39
|
+
**A holder reusing an invoice across melts.** Refused: the funding source
|
|
40
|
+
dedupes payments by hash, so the second melt would be "confirmed" against
|
|
41
|
+
the first payment and burn a note without moving funds.
|
|
42
|
+
|
|
43
|
+
**A holder replaying a melt across mints sharing one funding source.**
|
|
44
|
+
Sharing a node between mints is a supported deployment, and it creates a
|
|
45
|
+
dedupe gap no mint's own tables can see: an invoice melted at mint A is
|
|
46
|
+
unknown to mint B, whose confirm-by-hash would read A's completed payment
|
|
47
|
+
as its own success and burn B's note with no funds moving. Closed twice
|
|
48
|
+
over: the melt callback synchronously asks the NODE whether it ever paid
|
|
49
|
+
(or is still paying) the hash and refuses if so, and if the foreign
|
|
50
|
+
payment lands in the race between that check and the send, the node's own
|
|
51
|
+
"payment already exists" refusal is surfaced as a distinct
|
|
52
|
+
PaymentAlreadyKnownError and the note restores - nothing went out on this
|
|
53
|
+
mint's behalf, so nothing is guessed. Do not share a funding source with
|
|
54
|
+
a mint implementation that lacks the equivalent guard: its side of the
|
|
55
|
+
same replay stays open.
|
|
56
|
+
|
|
57
|
+
**A malicious payee holding a hodl invoice.** A clean failure report from
|
|
58
|
+
the funding source is never trusted on its own: the note restores only
|
|
59
|
+
once `isPaymentComplete` returns a terminal false. A payment stuck
|
|
60
|
+
in-flight leaves the note pending rather than restorable - the alternative
|
|
61
|
+
is letting the holder melt the same value twice.
|
|
62
|
+
|
|
63
|
+
**A funding source lying about an invoice.** The invoice returned by
|
|
64
|
+
`createInvoice` is decoded and must commit to the preimage moneyer chose
|
|
65
|
+
and the amount requested, or it is never handed out - otherwise a payer's
|
|
66
|
+
money would mint an unclaimable note.
|
|
67
|
+
|
|
68
|
+
**Process death.** Melts are journalled before payment; reconciliation
|
|
69
|
+
runs at startup and on demand, resolving pending melts by asking the
|
|
70
|
+
funding source, never by assumption.
|
|
71
|
+
|
|
72
|
+
**The verify endpoint as an oracle.** `/verify` serves bearer material
|
|
73
|
+
(the preimage). It is only served once the funding source reports
|
|
74
|
+
settlement, is fetched live rather than cached, and has a real off switch
|
|
75
|
+
(`MONEYER_VERIFY=0` makes it a 404). Anyone who saw the unpaid invoice can
|
|
76
|
+
poll it, which is why wallets must rotate immediately on claim - stated in
|
|
77
|
+
the spec and enforced by the companion wallet.
|
|
78
|
+
|
|
79
|
+
## Out of scope
|
|
80
|
+
|
|
81
|
+
- Compromise of the host or the funding source credentials: at that point
|
|
82
|
+
the attacker IS the mint.
|
|
83
|
+
- Lightning-level attacks against the funding source (channel jamming,
|
|
84
|
+
probing) - the node's own concern.
|
|
85
|
+
- TLS termination: moneyer expects a reverse proxy in production and binds
|
|
86
|
+
to loopback by default.
|
|
87
|
+
|
|
88
|
+
## Known limitations
|
|
89
|
+
|
|
90
|
+
- The cln and lnd backends are unexercised against live nodes (direct
|
|
91
|
+
ports of the reference mint's logic). Run `--dev` traffic and the
|
|
92
|
+
conformance grader against a staging deployment before taking real money.
|
|
93
|
+
- No rate limiting is built in; put it at the proxy. This matters most for
|
|
94
|
+
`/p/cb`: each unauthenticated call creates a real invoice at the funding
|
|
95
|
+
source. moneyer sweeps its own unsettled invoices once their bolt11
|
|
96
|
+
expiry has passed, but the node's side of that growth is the operator's
|
|
97
|
+
to bound (cln's autoclean, or an equivalent cron) - and a proxy limit is
|
|
98
|
+
what keeps the RPC pile-up and the node database from growing at all.
|
|
99
|
+
- A fee-free configuration pays the melt routing-fee floor (0.5% of the
|
|
100
|
+
amount or 5000 msat) out of its own channel balance, and mint-and-melt
|
|
101
|
+
cycling costs a griefer nothing. Set `MONEYER_BASE_FEE_MSAT` to cover the
|
|
102
|
+
floor before taking real traffic.
|
|
103
|
+
- `node:sqlite` is a single-writer store; moneyer is a single-process
|
|
104
|
+
service by design. Do not run two instances against one database.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { bytesToHex, randomBytes } from '@noble/hashes/utils.js';
|
|
2
|
+
import { verifyPreimage } from 'farrier-kit/preimage';
|
|
3
|
+
import { PaymentAlreadyKnownError, PaymentFailedError, PaymentPendingError } from "./types.js";
|
|
4
|
+
// Core Lightning over the clnrest plugin. cln's `invoice` accepts a
|
|
5
|
+
// caller-supplied preimage, which is the whole reason it can back a mint.
|
|
6
|
+
//
|
|
7
|
+
// Not yet exercised against a live node - the semantics are a direct port
|
|
8
|
+
// of the reference mint's cln backend, which is.
|
|
9
|
+
//
|
|
10
|
+
// TLS: the node's certificate must be one this process trusts. For a
|
|
11
|
+
// self-signed clnrest cert, point NODE_EXTRA_CA_CERTS at it.
|
|
12
|
+
// xpay's documented failure codes.
|
|
13
|
+
const PAY_FAILURE_REASONS = {
|
|
14
|
+
203: "The invoice's destination permanently rejected this payment.",
|
|
15
|
+
205: 'Could not find a route to pay this invoice.',
|
|
16
|
+
207: 'This invoice has expired.',
|
|
17
|
+
219: 'This invoice has already been paid.'
|
|
18
|
+
};
|
|
19
|
+
export const createClnBackend = (config) => {
|
|
20
|
+
const call = async (path, body, timeoutMs = 15_000) => {
|
|
21
|
+
const res = await fetch(`${config.url}${path}`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'content-type': 'application/json', Rune: config.rune },
|
|
24
|
+
body: JSON.stringify(body),
|
|
25
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
26
|
+
});
|
|
27
|
+
const json = await res.json().catch(() => null);
|
|
28
|
+
return { ok: res.ok, status: res.status, json };
|
|
29
|
+
};
|
|
30
|
+
const mustCall = async (path, body, timeoutMs) => {
|
|
31
|
+
const res = await call(path, body, timeoutMs);
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`cln ${path} failed (${res.status}): ${JSON.stringify(res.json)?.slice(0, 200)}`);
|
|
34
|
+
}
|
|
35
|
+
return res.json;
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
name: 'cln',
|
|
39
|
+
async createInvoice({ amountMsat, preimageHex, memo }) {
|
|
40
|
+
const result = await mustCall('/v1/invoice', {
|
|
41
|
+
amount_msat: amountMsat,
|
|
42
|
+
label: bytesToHex(randomBytes(16)),
|
|
43
|
+
description: memo,
|
|
44
|
+
preimage: preimageHex
|
|
45
|
+
});
|
|
46
|
+
if (typeof result?.bolt11 !== 'string')
|
|
47
|
+
throw new Error('cln did not return a bolt11 invoice.');
|
|
48
|
+
return { pr: result.bolt11 };
|
|
49
|
+
},
|
|
50
|
+
async payInvoice({ pr, feeLimitMsat }) {
|
|
51
|
+
// xpay resolves once it stops retrying, which is NOT proof that no
|
|
52
|
+
// HTLC it already sent remains outstanding - the caller confirms via
|
|
53
|
+
// isPaymentComplete before restoring anything either way. The
|
|
54
|
+
// timeout sits above xpay's own default 60s retry_for so a clean
|
|
55
|
+
// failure response is not turned into an ambiguous one at the wire.
|
|
56
|
+
const res = await call('/v1/xpay', { invstring: pr, maxfee: feeLimitMsat }, 90_000);
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const code = res.json?.code;
|
|
59
|
+
// 219: this node already paid that hash. On a shared node that is
|
|
60
|
+
// somebody else's payment - nothing went out for THIS call, so it
|
|
61
|
+
// is a distinct, safely-restorable refusal rather than a failure.
|
|
62
|
+
if (code === 219) {
|
|
63
|
+
throw new PaymentAlreadyKnownError('cln already has a payment for this hash');
|
|
64
|
+
}
|
|
65
|
+
const reason = (typeof code === 'number' && PAY_FAILURE_REASONS[code]) ||
|
|
66
|
+
res.json?.message ||
|
|
67
|
+
`Payment failed (${res.status}).`;
|
|
68
|
+
throw new PaymentFailedError(reason);
|
|
69
|
+
}
|
|
70
|
+
const preimageHex = res.json?.payment_preimage;
|
|
71
|
+
if (typeof preimageHex !== 'string')
|
|
72
|
+
throw new Error('cln did not return a payment_preimage.');
|
|
73
|
+
const amountMsat = res.json?.amount_msat;
|
|
74
|
+
const amountSentMsat = res.json?.amount_sent_msat;
|
|
75
|
+
const feeMsat = typeof amountMsat === 'number' && typeof amountSentMsat === 'number'
|
|
76
|
+
? amountSentMsat - amountMsat
|
|
77
|
+
: null;
|
|
78
|
+
return { preimageHex, feeMsat };
|
|
79
|
+
},
|
|
80
|
+
async isPaymentComplete(paymentHashHex) {
|
|
81
|
+
const result = await mustCall('/v1/listpays', { payment_hash: paymentHashHex });
|
|
82
|
+
const pays = result?.pays ?? [];
|
|
83
|
+
// "pending" is not "confirmed not paid" - an HTLC may still be locked
|
|
84
|
+
// at the final hop.
|
|
85
|
+
if (pays.some(pay => pay?.status === 'pending')) {
|
|
86
|
+
throw new PaymentPendingError('cln reports the payment still pending');
|
|
87
|
+
}
|
|
88
|
+
return pays.some(pay => pay?.status === 'complete');
|
|
89
|
+
},
|
|
90
|
+
async isInvoiceSettled(paymentHashHex) {
|
|
91
|
+
const result = await mustCall('/v1/listinvoices', { payment_hash: paymentHashHex });
|
|
92
|
+
const invoices = result?.invoices ?? [];
|
|
93
|
+
return invoices.length > 0 && invoices[0]?.status === 'paid';
|
|
94
|
+
},
|
|
95
|
+
async invoicePreimage(paymentHashHex) {
|
|
96
|
+
const result = await mustCall('/v1/listinvoices', { payment_hash: paymentHashHex });
|
|
97
|
+
const invoice = (result?.invoices ?? [])[0];
|
|
98
|
+
if (!invoice || invoice.status !== 'paid')
|
|
99
|
+
return null;
|
|
100
|
+
const preimageHex = invoice.payment_preimage;
|
|
101
|
+
if (typeof preimageHex !== 'string' || !verifyPreimage(preimageHex, paymentHashHex))
|
|
102
|
+
return null;
|
|
103
|
+
return preimageHex;
|
|
104
|
+
},
|
|
105
|
+
async paymentPreimage(paymentHashHex) {
|
|
106
|
+
const result = await mustCall('/v1/listpays', { payment_hash: paymentHashHex });
|
|
107
|
+
for (const pay of result?.pays ?? []) {
|
|
108
|
+
if (pay?.status === 'complete' && typeof pay.preimage === 'string')
|
|
109
|
+
return pay.preimage;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
},
|
|
113
|
+
async nodeInfo() {
|
|
114
|
+
const info = await mustCall('/v1/getinfo', {});
|
|
115
|
+
const id = info?.id;
|
|
116
|
+
const address = (info?.address ?? [])[0];
|
|
117
|
+
const uri = id && address?.address && address?.port ? `${id}@${address.address}:${address.port}` : id;
|
|
118
|
+
const color = typeof info?.color === 'string' ? `#${info.color.replace(/^#/, '')}` : undefined;
|
|
119
|
+
const numChannels = Number(info?.num_active_channels);
|
|
120
|
+
const numPeers = Number(info?.num_peers);
|
|
121
|
+
return {
|
|
122
|
+
...(info?.alias ? { alias: info.alias } : {}),
|
|
123
|
+
...(uri ? { uri } : {}),
|
|
124
|
+
...(color && /^#[0-9a-fA-F]{6}$/.test(color) ? { color } : {}),
|
|
125
|
+
...(Number.isSafeInteger(numChannels) ? { numChannels } : {}),
|
|
126
|
+
...(Number.isSafeInteger(numPeers) ? { numPeers } : {})
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { bech32 } from '@scure/base';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
3
|
+
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
4
|
+
import { hexToBytes, utf8ToBytes, randomBytes } from '@noble/hashes/utils.js';
|
|
5
|
+
// A syntactically COMPLETE BOLT-11 invoice that nothing can ever pay.
|
|
6
|
+
//
|
|
7
|
+
// The conformance mock mint gets away with an HRP and filler because the
|
|
8
|
+
// reference wallet only reads the human-readable part. This stack holds
|
|
9
|
+
// itself to farrier-kit's full decode - checksum, tagged fields, signature
|
|
10
|
+
// layout - so the fake funding source has to emit the real grammar: a
|
|
11
|
+
// mainnet HRP with the exact amount, a p field carrying the true payment
|
|
12
|
+
// hash, s and 9 so modern parsers see a payment secret and its feature
|
|
13
|
+
// bits, d for the memo, and a recoverable signature by a fixed throwaway
|
|
14
|
+
// key that no Lightning node has ever announced. Unpayable by
|
|
15
|
+
// construction; decodable by anything.
|
|
16
|
+
const FAKE_NODE_KEY = hexToBytes('4242424242424242424242424242424242424242424242424242424242424242');
|
|
17
|
+
const fiveToEight = (words) => {
|
|
18
|
+
let acc = 0;
|
|
19
|
+
let bits = 0;
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const word of words) {
|
|
22
|
+
acc = (acc << 5) | word;
|
|
23
|
+
bits += 5;
|
|
24
|
+
while (bits >= 8) {
|
|
25
|
+
bits -= 8;
|
|
26
|
+
out.push((acc >> bits) & 0xff);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (bits > 0)
|
|
30
|
+
out.push((acc << (8 - bits)) & 0xff);
|
|
31
|
+
return new Uint8Array(out);
|
|
32
|
+
};
|
|
33
|
+
const tagged = (type, data) => [
|
|
34
|
+
type,
|
|
35
|
+
data.length >> 5,
|
|
36
|
+
data.length & 31,
|
|
37
|
+
...data
|
|
38
|
+
];
|
|
39
|
+
const amountHrp = (msat) => (msat % 100 === 0 ? `${msat / 100}n` : `${msat * 10}p`);
|
|
40
|
+
export const fakeBolt11 = (args) => {
|
|
41
|
+
const hrp = `lnbc${amountHrp(args.amountMsat)}`;
|
|
42
|
+
const words = [];
|
|
43
|
+
const timestamp = args.timestamp ?? Math.floor(Date.now() / 1000);
|
|
44
|
+
for (let i = 6; i >= 0; i--)
|
|
45
|
+
words.push((timestamp / 2 ** (5 * i)) & 31);
|
|
46
|
+
// p - payment hash
|
|
47
|
+
words.push(...tagged(1, Array.from(bech32.toWords(hexToBytes(args.paymentHashHex)))));
|
|
48
|
+
// d - description
|
|
49
|
+
words.push(...tagged(13, Array.from(bech32.toWords(utf8ToBytes(args.memo ?? 'moneyer fake invoice')))));
|
|
50
|
+
// s - payment secret (random; nothing will ever present it)
|
|
51
|
+
words.push(...tagged(16, Array.from(bech32.toWords(randomBytes(32)))));
|
|
52
|
+
// 9 - features: var_onion_optin (bit 9) and payment_secret (bit 15), both
|
|
53
|
+
// optional, as 20 bits of 5-bit words
|
|
54
|
+
words.push(...tagged(5, [1, 0, 16, 0]));
|
|
55
|
+
const signed = sha256(new Uint8Array([...utf8ToBytes(hrp), ...fiveToEight(words)]));
|
|
56
|
+
const lead = secp256k1.sign(signed, FAKE_NODE_KEY, { format: 'recovered', prehash: false });
|
|
57
|
+
const signature = new Uint8Array([...lead.subarray(1), lead[0]]);
|
|
58
|
+
words.push(...Array.from(bech32.toWords(signature)));
|
|
59
|
+
return bech32.encode(hrp, words, false);
|
|
60
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type LightningBackend } from './types.ts';
|
|
2
|
+
export type FakePayMode = 'succeed' | 'fail-clean' | 'fail-then-paid' | 'ambiguous-paid' | 'ambiguous-unpaid' | 'ambiguous-pending';
|
|
3
|
+
export type FakeBackend = LightningBackend & {
|
|
4
|
+
control: {
|
|
5
|
+
settleInvoice(paymentHashHex: string): void;
|
|
6
|
+
setPayMode(mode: FakePayMode): void;
|
|
7
|
+
resolvePayment(paymentHashHex: string, status: 'complete' | 'failed'): void;
|
|
8
|
+
registerPaymentPreimage(paymentHashHex: string, preimageHex: string): void;
|
|
9
|
+
seedForeignPayment(paymentHashHex: string, status?: 'complete' | 'pending'): void;
|
|
10
|
+
invoiceByHash(paymentHashHex: string): {
|
|
11
|
+
preimageHex: string;
|
|
12
|
+
amountMsat: number;
|
|
13
|
+
settled: boolean;
|
|
14
|
+
} | undefined;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare const createFakeBackend: () => FakeBackend;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
3
|
+
import { hexToBytes } from '@noble/hashes/utils.js';
|
|
4
|
+
import { bolt11PaymentHash } from 'farrier-kit/bolt11';
|
|
5
|
+
import { fakeBolt11 } from "./fake-bolt11.js";
|
|
6
|
+
import { PaymentAlreadyKnownError, PaymentFailedError, PaymentPendingError } from "./types.js";
|
|
7
|
+
export const createFakeBackend = () => {
|
|
8
|
+
const invoices = new Map();
|
|
9
|
+
const payments = new Map();
|
|
10
|
+
const knownPreimages = new Map();
|
|
11
|
+
let payMode = 'succeed';
|
|
12
|
+
return {
|
|
13
|
+
name: 'fake',
|
|
14
|
+
async createInvoice({ amountMsat, preimageHex, memo }) {
|
|
15
|
+
const paymentHashHex = bytesToHex(sha256(hexToBytes(preimageHex)));
|
|
16
|
+
const pr = fakeBolt11({ amountMsat, paymentHashHex, memo });
|
|
17
|
+
invoices.set(paymentHashHex, { preimageHex, amountMsat, settled: false });
|
|
18
|
+
return { pr };
|
|
19
|
+
},
|
|
20
|
+
async payInvoice({ pr }) {
|
|
21
|
+
const paymentHashHex = bolt11PaymentHash(pr);
|
|
22
|
+
if (!paymentHashHex)
|
|
23
|
+
throw new PaymentFailedError('That is not a decodable invoice.');
|
|
24
|
+
// Real nodes dedupe sends by payment hash: a hash this node already
|
|
25
|
+
// holds a payment for is refused, exactly as lnd and cln refuse it.
|
|
26
|
+
if (payments.has(paymentHashHex)) {
|
|
27
|
+
throw new PaymentAlreadyKnownError('this node already has a payment for that hash');
|
|
28
|
+
}
|
|
29
|
+
const preimageHex = knownPreimages.get(paymentHashHex) ?? null;
|
|
30
|
+
switch (payMode) {
|
|
31
|
+
case 'succeed':
|
|
32
|
+
payments.set(paymentHashHex, { status: 'complete', preimageHex });
|
|
33
|
+
return { preimageHex, feeMsat: 0 };
|
|
34
|
+
case 'fail-clean':
|
|
35
|
+
payments.set(paymentHashHex, { status: 'failed', preimageHex: null });
|
|
36
|
+
throw new PaymentFailedError('Could not find a route to pay this invoice.');
|
|
37
|
+
case 'fail-then-paid':
|
|
38
|
+
payments.set(paymentHashHex, { status: 'complete', preimageHex });
|
|
39
|
+
throw new PaymentFailedError('Timed out trying to find a route to pay this invoice.');
|
|
40
|
+
case 'ambiguous-paid':
|
|
41
|
+
payments.set(paymentHashHex, { status: 'complete', preimageHex });
|
|
42
|
+
throw new Error('connection reset mid-payment');
|
|
43
|
+
case 'ambiguous-unpaid':
|
|
44
|
+
payments.set(paymentHashHex, { status: 'failed', preimageHex: null });
|
|
45
|
+
throw new Error('connection reset mid-payment');
|
|
46
|
+
case 'ambiguous-pending':
|
|
47
|
+
payments.set(paymentHashHex, { status: 'pending', preimageHex });
|
|
48
|
+
throw new Error('connection reset mid-payment');
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async isPaymentComplete(paymentHashHex) {
|
|
52
|
+
const payment = payments.get(paymentHashHex);
|
|
53
|
+
if (!payment)
|
|
54
|
+
return false;
|
|
55
|
+
if (payment.status === 'pending') {
|
|
56
|
+
throw new PaymentPendingError('the payment has no terminal outcome yet');
|
|
57
|
+
}
|
|
58
|
+
return payment.status === 'complete';
|
|
59
|
+
},
|
|
60
|
+
async isInvoiceSettled(paymentHashHex) {
|
|
61
|
+
return invoices.get(paymentHashHex)?.settled ?? false;
|
|
62
|
+
},
|
|
63
|
+
async invoicePreimage(paymentHashHex) {
|
|
64
|
+
const invoice = invoices.get(paymentHashHex);
|
|
65
|
+
return invoice?.settled ? invoice.preimageHex : null;
|
|
66
|
+
},
|
|
67
|
+
async paymentPreimage(paymentHashHex) {
|
|
68
|
+
const payment = payments.get(paymentHashHex);
|
|
69
|
+
return payment?.status === 'complete' ? payment.preimageHex : null;
|
|
70
|
+
},
|
|
71
|
+
async nodeInfo() {
|
|
72
|
+
return { alias: 'moneyer (fake funding source)', color: '#c9ced8' };
|
|
73
|
+
},
|
|
74
|
+
control: {
|
|
75
|
+
settleInvoice(paymentHashHex) {
|
|
76
|
+
const invoice = invoices.get(paymentHashHex);
|
|
77
|
+
if (!invoice)
|
|
78
|
+
throw new Error(`no fake invoice for ${paymentHashHex}`);
|
|
79
|
+
invoice.settled = true;
|
|
80
|
+
},
|
|
81
|
+
setPayMode(mode) {
|
|
82
|
+
payMode = mode;
|
|
83
|
+
},
|
|
84
|
+
resolvePayment(paymentHashHex, status) {
|
|
85
|
+
const payment = payments.get(paymentHashHex);
|
|
86
|
+
if (!payment)
|
|
87
|
+
throw new Error(`no fake payment for ${paymentHashHex}`);
|
|
88
|
+
payment.status = status;
|
|
89
|
+
},
|
|
90
|
+
registerPaymentPreimage(paymentHashHex, preimageHex) {
|
|
91
|
+
knownPreimages.set(paymentHashHex, preimageHex);
|
|
92
|
+
const payment = payments.get(paymentHashHex);
|
|
93
|
+
if (payment?.status === 'complete')
|
|
94
|
+
payment.preimageHex = preimageHex;
|
|
95
|
+
},
|
|
96
|
+
seedForeignPayment(paymentHashHex, status = 'complete') {
|
|
97
|
+
payments.set(paymentHashHex, { status, preimageHex: null });
|
|
98
|
+
},
|
|
99
|
+
invoiceByHash(paymentHashHex) {
|
|
100
|
+
return invoices.get(paymentHashHex);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|