diff options
Diffstat (limited to 'lib/ecash.ts')
| -rw-r--r-- | lib/ecash.ts | 83 |
1 files changed, 64 insertions, 19 deletions
diff --git a/lib/ecash.ts b/lib/ecash.ts index 9ce8b4e5..b1d8b7fb 100644 --- a/lib/ecash.ts +++ b/lib/ecash.ts @@ -1,6 +1,7 @@ import config from "$config"; -import { g, s } from "$lib/db"; +import { db, g } from "$lib/db"; import { lnb } from "$lib/ln"; +import { warn } from "$lib/logging"; import { safeGot } from "$lib/safe-fetch"; import { fail, wait } from "$lib/utils"; import { @@ -11,13 +12,39 @@ import { PaymentRequestTransportType, getDecodedToken, getEncodedToken, - getEncodedTokenV4, } from "@cashu/cashu-ts"; const { URL } = process.env; const m = new CashuMint(config.mintUrl); -const w = new CashuWallet(m); + +// The mint issues NUT-02 v1 keyset ids (`01…`, 33 bytes). cashu-ts 2.9.0 only +// derives v0 ids, so CashuWallet.getKeys() rejects every keyset with "Couldn't +// verify keyset ID" and receive()/send() can never run — ecash claims have been +// failing since the July keyset cutover. (4.x derives v1 ids, but not the way +// this nutshell build does, so upgrading wouldn't help either.) config.mintUrl +// is our own mint, so skip the derivation check: CashuMint.getKeys() returns +// the keys unverified, and a wallet constructed with them preloaded uses them +// as-is. Re-fetched periodically so a keyset rotation is picked up. +const KEYS_TTL = 10 * 60 * 1000; +let cached: { w: CashuWallet; keysets: any[]; at: number } | undefined; +const wallet = async () => { + if (cached && Date.now() - cached.at < KEYS_TTL) return cached; + const [{ keysets: keys }, { keysets }] = await Promise.all([ + m.getKeys(), + m.getKeySets(), + ]); + const w = new CashuWallet(m, { keys, keysets }); + cached = { w, keysets, at: Date.now() }; + return cached; +}; + +// 2.9.0 writes v1 keyset ids into V4 tokens in their 8-byte short form, and +// refuses to decode a short id unless it's handed the mint's keysets to expand +// it against — so every token we encode (the house pool included) has to be +// decoded through here rather than with a bare getDecodedToken(). +const decode = async (token) => + getDecodedToken(token, (await wallet()).keysets); const enc = (proofs) => getEncodedToken({ @@ -25,6 +52,29 @@ const enc = (proofs) => proofs, }); +// The house wallet is a single encoded token under the `cash` key. g() JSON- +// parses whatever is stored there, and the key has been found holding a bare +// integer — getDecodedToken(number) then threw "n.startsWith is not a function" +// on every claim. Only trust the value when it decodes; otherwise start from +// no proofs and let the next write replace it. +export const pool = async () => { + const v = await g("cash"); + if (typeof v === "string") { + try { + return (await decode(v)).proofs; + } catch (e: any) { + warn("cash pool token unreadable, treating as empty:", e.message); + } + } else { + warn("cash pool key is not a token, treating as empty:", typeof v); + } + return []; +}; + +// s() is fire-and-forget; a dropped write here would silently strand the +// proofs we just swapped in, so await the set directly. +const setPool = (proofs) => db.set("cash", JSON.stringify(enc(proofs))); + // Serialize every read-modify-write of the shared `cash` house-wallet token. The // mint swap sits between reading the current proofs and writing the merged set, // so two concurrent claim()/mint() calls would each read the same `current` and @@ -68,37 +118,37 @@ export async function get(id) { } export async function claim(token) { - const { mint } = getDecodedToken(token); + const { mint } = await decode(token); if (await ext(mint)) fail("Unable to receive from other mints"); + const { w } = await wallet(); return withCashLock(async () => { - const { proofs: current } = getDecodedToken(await g("cash")); + const current = await pool(); const rcvd = await w.receive(token); - await s("cash", enc([...current, ...rcvd])); + await setPool([...current, ...rcvd]); return rcvd.reduce((a, b) => a + b.amount, 0); }); } export async function mint(amount) { - const { keysets } = await m.getKeySets(); - const w = new CashuWallet(m, { keysets }); + const { w } = await wallet(); return withCashLock(async () => { - const { proofs } = getDecodedToken(await g("cash")); + const proofs = await pool(); const { send, keep } = await w.send(amount, proofs); const rcvd = await w.receive(enc(send)); - const change = enc(keep); - await s("cash", change); + await setPool(keep); return enc(rcvd); }); } export async function check(token) { - const { mint, proofs } = getDecodedToken(token); + const { mint, proofs } = await decode(token); const total = proofs.reduce((a, b) => a + b.amount, 0); const external = await ext(mint); + const { w } = await wallet(); let spent = 0; for (const [i, p] of (await w.checkProofsStates(proofs)).entries()) { if (p.state === "SPENT") spent += proofs[i].amount; @@ -110,6 +160,7 @@ export async function check(token) { export async function init(amount = 100000) { try { await new Promise((r) => setTimeout(r, 2000)); + const { w } = await wallet(); const { quote, request } = await w.createMintQuote(amount); await lnb.pay(request); @@ -119,13 +170,7 @@ export async function init(amount = 100000) { }); const proofs = await w.mintProofs(amount, quote); - - const cash = getEncodedTokenV4({ - mint: config.mintUrl, - proofs, - }); - - await s("cash", cash); + await setPool(proofs); } catch (e) {} } |
