From 11d9a2b1f7e45ee7b19b3542752b5801107eb9a8 Mon Sep 17 00:00:00 2001 From: Om Santoshwar Date: Mon, 1 Jun 2026 11:34:12 +0530 Subject: [PATCH 01/32] feat(wallet): replace cocod IPC with coco-core library --- bun.lock | 94 +++++++++++--------- package.json | 3 + src/daemon/index.ts | 4 +- src/daemon/wallet/coco-client.ts | 142 +++++++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 42 deletions(-) create mode 100644 src/daemon/wallet/coco-client.ts diff --git a/bun.lock b/bun.lock index b56af5b..440ce1e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,11 +1,15 @@ { "lockfileVersion": 1, + "configVersion": 1, "workspaces": { "": { "name": "routstrd", "dependencies": { "@cashu/cashu-ts": "^4.3.0", + "@cashu/coco-core": "^1.0.1", + "@cashu/coco-sqlite-bun": "^1.0.1", "@routstr/sdk": "^0.3.17", + "@scure/bip39": "^2.2.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", @@ -26,23 +30,27 @@ }, }, "packages": { - "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.30" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A6qfyaVs4lxmRxRux6U3ViOa8mMbsSd0OaHghpei2MpiBT6791J4zFH5MN7kaW1tLfQ246rSC2DVTMOavsycjQ=="], + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.59", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CFsUizO+jL+jSlN13rW2nQE9EbWx+8PSFBdB3TtD0UGYOxtefCVa5hsrNo5NUOJJGX5f4xHiXE1i2m5nQ80QPg=="], - "@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], - "@cashu/cashu-ts": ["@cashu/cashu-ts@4.5.1", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-AGB7wh1SC0iANRSsdwM091eoS1uVhSf/Qf2VM3plw8EzlYjiFkwSa9X0Nh6NpsE1VVeAuPpNInvpznvElp2Q0A=="], + "@cashu/cashu-ts": ["@cashu/cashu-ts@4.7.0", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-u/R9nHCBFugQeTKf1NCZscKp6Zblo3ubX8mFA0Ar8+lpGaHSndWmsQUsSQ7vrNGmdGtqOTMiR7hErEqVCVOY+A=="], - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@cashu/coco-core": ["@cashu/coco-core@1.0.1", "", { "dependencies": { "@cashu/cashu-ts": "^3.5.0", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "@scure/bip32": "^2.0.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-Nv0T9+K49X+jqnrqn65Vgn/4Id+271JyHTgjwP/URdgShIJX3O8aGH4uidNphKnuBEa0EZKWPiFRn5K4mXh9jA=="], - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@cashu/coco-sqlite-bun": ["@cashu/coco-sqlite-bun@1.0.1", "", { "peerDependencies": { "@cashu/coco-core": "1.0.1", "typescript": "^5" } }, "sha512-u6lnC3c8+qopD1CuLkcScbxcU1O+STrTz9+fwN09filD4PS+vYPCQNgygIfzKWnCaF8gXFRUuJsXSMqHDImJTw=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@freedomofpress/crypto-browser": ["@freedomofpress/crypto-browser@0.1.7", "", { "dependencies": { "@noble/curves": "^1.6.0" } }, "sha512-zjWmZDKdAu8g0Zq1IjBQ+sKQ/NpfzStBDFjy/qHUSMVEL4wNlNGtA7lhtw8v8asXa0yqF2QTYQ3rq6xCTQeADw=="], - "@freedomofpress/sigstore-browser": ["@freedomofpress/sigstore-browser@0.1.13", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/tuf-browser": "^0.1.11", "@noble/curves": "^2.0.1" } }, "sha512-3YfmP9JQ5h8CAO/vKJEGYrFdzss6xWxYi5ZkbR4KoHlxIaLGrnu+5TQDoZVWhuhyfDBdLQqj6ypdN0W6PsH3Jw=="], + "@freedomofpress/sigstore-browser": ["@freedomofpress/sigstore-browser@0.1.14", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/tuf-browser": "^0.1.11", "@noble/curves": "^2.0.1" } }, "sha512-1dqc7HojiBcr/sJSAXjBwNz4+WGeeAJP8ENQnDxm+idVV0/YIxpfwXRNSJdNw6EXJuHviq/5kSPEBCxamPcrpQ=="], "@freedomofpress/tuf-browser": ["@freedomofpress/tuf-browser@0.1.11", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7" } }, "sha512-d76ohB/AS5+zI+lnbiFMX/BIK3nT18hZ8q3Na6X4vyYaSYjXkeknzhAnbx1da+8ObWLwsaZLue46haEch28qtQ=="], @@ -74,7 +82,7 @@ "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.29", "", { "os": "win32", "cpu": "x64" }, "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], @@ -86,7 +94,7 @@ "@noble/post-quantum": ["@noble/post-quantum@0.6.1", "", { "dependencies": { "@noble/ciphers": "~2.2.0", "@noble/curves": "~2.2.0", "@noble/hashes": "~2.2.0" } }, "sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A=="], - "@panva/hpke-noble": ["@panva/hpke-noble@1.1.2", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-Gr/jKr/ErpRow8aHK+SAp0sAaWy7iJGHlFsXsdTjD8ULcN4vvsVVWR3fiD6Ogf+VoSB28XY/xWRDUEIGXw+9aA=="], + "@panva/hpke-noble": ["@panva/hpke-noble@1.1.3", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-zPG7MR9x7QE7+KdYsKBO9H0vp3AdYt9/4AT3ab7T7W6SL0fdRqhgNRu8q4OGTJNLeKpdbkkRb6LhBDaA9+9xWQ=="], "@routstr/sdk": ["@routstr/sdk@0.3.17", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-3rLsVo8bo3pQG+KzACNfRo3dz6XqfsCoFLpvuqkVNgXUyxSQEQPFRiDtUeACf+a6Xp3f9LSLknVoTeL78s28Tg=="], @@ -94,11 +102,11 @@ "@scure/bip32": ["@scure/bip32@2.2.0", "", { "dependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg=="], - "@scure/bip39": ["@scure/bip39@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg=="], + "@scure/bip39": ["@scure/bip39@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@tinfoilsh/verifier": ["@tinfoilsh/verifier@1.1.6", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/sigstore-browser": "^0.1.13", "@freedomofpress/tuf-browser": "^0.1.11" } }, "sha512-egZlLFU98kh48GKV5B8Gh1abGHCUGEcSijWmTv3hn5lqAMXdLsqyehKDaUen8/98CrNxiOP+mzeqVbKSyAs5ZQ=="], + "@tinfoilsh/verifier": ["@tinfoilsh/verifier@1.1.8", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/sigstore-browser": "^0.1.13", "@freedomofpress/tuf-browser": "^0.1.11" } }, "sha512-4L09Xr0SqVGzByzs3bnDqL4+U9kwgX8nmv2JMt4xydeTKjKtHZUosAt0jphtN7jT9h6mhfXhX0Yim5bCyEJc/Q=="], "@tursodatabase/database": ["@tursodatabase/database@0.2.2", "", { "dependencies": { "@tursodatabase/database-common": "^0.2.2" }, "optionalDependencies": { "@tursodatabase/database-darwin-arm64": "0.2.2", "@tursodatabase/database-linux-arm64-gnu": "0.2.2", "@tursodatabase/database-linux-x64-gnu": "0.2.2", "@tursodatabase/database-win32-x64-msvc": "0.2.2" } }, "sha512-BlvoyiwIWIIQA65KEYlCqLDpRKIOM1AN99tBMjz1B+ydO9h0cDb+lCbfBf6f7+Lk696KB1ODpePiF5EfGm9qpw=="], @@ -116,11 +124,11 @@ "@tursodatabase/database-win32-x64-msvc": ["@tursodatabase/database-win32-x64-msvc@0.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-C1Z1xOUSln1/qtzR60xZYHgmfrQIhNm0glgqsnZt2wQI354dDtQGZH/oG/tKWCHrvtFvWCbnd9DEZif/VCK56A=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], @@ -130,7 +138,7 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "applesauce-common": ["applesauce-common@6.1.0", "", { "dependencies": { "@scure/base": "^1.2.4", "applesauce-core": "^6.1.0", "hash-sum": "^2.0.0", "light-bolt11-decoder": "^3.2.0", "nanoid": "^5.0.9", "rxjs": "^7.8.1" } }, "sha512-z5azXCIrSknGw57hzUN8F30Xd4clvLCfRQYM0asyyIQJtwxuEl0N/zrXX7RqdiJN/9yFCuLzBro2XoFezoeOMA=="], + "applesauce-common": ["applesauce-common@6.2.0", "", { "dependencies": { "@scure/base": "^2.2.0", "applesauce-core": "^6.2.0", "hash-sum": "^2.0.0", "light-bolt11-decoder": "^3.2.0", "nanoid": "^5.0.9", "rxjs": "^7.8.1" } }, "sha512-meXbDCdqA1LKy1tr04BW7wMfnBBkHetLv+wlXYjAj5iR4Sji5KMxLFdAjUe6cSvrxrk9LxFcP+0P9BcZQxjpaA=="], "applesauce-core": ["applesauce-core@5.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-aSuM6q6/Gs2FGUqytlHDjKZpSst2xKaT0vMXUQFWUctECNIxvwy6/hTDDInukMuI9mrQdjnO781ZJJgghI7RNw=="], @@ -138,11 +146,11 @@ "applesauce-sqlite": ["applesauce-sqlite@6.0.0", "", { "dependencies": { "applesauce-core": "^6.0.0" }, "peerDependencies": { "@libsql/client": "^0.15.15", "@tursodatabase/database": "^0.2.2", "@tursodatabase/database-wasm": "^0.2.2", "better-sqlite3": "^12.8.0" } }, "sha512-OwuJ0ch8UW2T2JmcW0FHImdh5zjpDKrIb5BrDXSpLhzN3mexT9M2IXhRZlq1xU4wtzQoMkmsgYX2r7QmgeLArw=="], - "applesauce-wallet-connect": ["applesauce-wallet-connect@6.0.0", "", { "dependencies": { "@noble/hashes": "^1.7.1", "applesauce-common": "^6.0.0", "applesauce-core": "^6.0.0", "rxjs": "^7.8.1" } }, "sha512-rdVvc2NIa6VqnJh2JucyMjhA+S9bohPfXxChJtM4TkWwTFxkhkao9W+7Fuoe9ctelubGKXBbmtOSX3WowjgUOg=="], + "applesauce-wallet-connect": ["applesauce-wallet-connect@6.2.0", "", { "dependencies": { "@noble/hashes": "^2.2.0", "applesauce-common": "^6.2.0", "applesauce-core": "^6.2.0", "rxjs": "^7.8.1" } }, "sha512-vI3FD3Oyil5QtEM1IRx1Q/lWEfmXMHHO4YsHRUtUx3fcid4uG4xe2KIKsMpduGH9t46Bxz4k+vjp2WBHtkgT1g=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="], + "better-sqlite3": ["better-sqlite3@12.11.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA=="], "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], @@ -178,7 +186,7 @@ "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], - "ehbp": ["ehbp@0.2.3", "", { "dependencies": { "@panva/hpke-noble": "^1.0.3", "hpke": "^1.0.1" } }, "sha512-f7iLlhGpeE5xNOYASR3d529PzmBD7Pq5Yb/z0R7vQcxalwNuVTHhX6i4wDJRIVai18MznwoOQ3OWyT/EWaDJ3w=="], + "ehbp": ["ehbp@0.2.5", "", { "dependencies": { "@panva/hpke-noble": "^1.0.3", "hpke": "^1.0.1" } }, "sha512-Fjos3ct9f1Fxf2xckV+dXUez9vRa+k+pn3gqkivg0WaJ4WIQwXBCdT8iphr/+DmAQE9JKo4FdedwR3gwlA2kvA=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -206,7 +214,7 @@ "hash-sum": ["hash-sum@2.0.0", "", {}, "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg=="], - "hpke": ["hpke@1.1.2", "", {}, "sha512-6Z8JoG/zYXly+MfsgQ5aNibBx/HDq2F74254Tv8VK4DR3/Mmf6zVrGakqsodfy9pF0XGKSYh5MA9x6D30gbhbg=="], + "hpke": ["hpke@1.1.3", "", {}, "sha512-nViXp6Au3UmGSIHlKIz8GQSuHJsscLTqZciHnssYMyuxHJvj3M4jBYpN3CbffTNPhhz2NQuUiC+TIaDxxP7NCw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -216,7 +224,7 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-base64": ["js-base64@3.8.1", "", {}, "sha512-5xVjhUZlHHeuO2W7w2rDFj/Kl1xLX+HjZxdOQwCsUOifl6UaoH1o1wsbsTMz+r0aeC7gCijvru02j6TfKZWzKg=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], @@ -234,23 +242,23 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], + "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - "node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="], + "node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "nostr-tools": ["nostr-tools@2.23.5", "", { "dependencies": { "@noble/ciphers": "2.1.1", "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0", "@scure/bip32": "2.0.1", "@scure/bip39": "2.0.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-Fa7ZlUdjfUW1P4E7H3yBexhOHYi18XNyvd2n7eNHkYR085xADX6Y8V8Vm7nT/XQajaFOBrptXmVIGkJ2E4vfVw=="], + "nostr-tools": ["nostr-tools@2.23.9", "", { "dependencies": { "@noble/ciphers": "2.1.1", "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0", "@scure/bip32": "2.0.1", "@scure/bip39": "2.0.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-PBN3TpGh+EszlTZWpQdvcqEMqhcKcY1vTw0Xkkccg+TyP7y/icu9/iXJw72aFZ/ETm35ehU0hneGTXLaTSkImw=="], "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "openai": ["openai@6.44.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"] }, "sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA=="], + "openai": ["openai@6.46.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-DFg6jEPT2RO+oAyXtddeUJU8zkGy1OQ1AjGzNIJUMQG03TTqvCpy9tBpQ+2VVVnvrl3E56F8GEin2JYtWpITtA=="], "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -282,7 +290,7 @@ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], @@ -298,11 +306,11 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "tinfoil": ["tinfoil@1.1.6", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.41", "@freedomofpress/sigstore-browser": "^0.1.13", "@tinfoilsh/verifier": "1.1.6", "@types/ws": "^8.18.1", "ehbp": "^0.2.0", "openai": "^6.34.0", "ws": "^8.21.0" }, "peerDependencies": { "ai": "^6.0.168" }, "optionalPeers": ["ai"] }, "sha512-kAgVPuvdydLSF72GH8I5Awnc0835MQCNHb9cS/J2TxGgMF2LFgqaJhe9l1iPf7Si64h3s+Gebz0XfTJ80QZ8wQ=="], + "tinfoil": ["tinfoil@1.1.8", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.41", "@freedomofpress/sigstore-browser": "^0.1.13", "@tinfoilsh/verifier": "1.1.8", "@types/ws": "^8.18.1", "ehbp": "^0.2.0", "openai": "^6.34.0", "ws": "^8.21.0" }, "peerDependencies": { "ai": "^6.0.168" }, "optionalPeers": ["ai"] }, "sha512-wD6DlDwnzquRqQcmfUNVYdQWM1kdzESwt/ksta2R78+9Zuk7NNQb2HinCkROJ0AZCHJZN0vHpQr/n98SxS3H7A=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -310,7 +318,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -336,21 +344,17 @@ "zustand": ["zustand@5.0.14", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g=="], + "@cashu/coco-core/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.1", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-EdUdjR6D0eY1YT6irNh/d5SBRc/ug4WNpnlLBXbRI9iOb8UNnQeGfk9CJQWL9ElQMjQ/4g582SehKXzEAZ2vnw=="], + "@freedomofpress/crypto-browser/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "@noble/post-quantum/@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], "@panva/hpke-noble/@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], - "@routstr/sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.0", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-Jy4Hek+Wouh94xS4InmTJRLpBPXA6fddVOAHR5lfNyTsCk9r3desY0vnwUGvOZBZWsSKZXedqdD/YpjWb3zngQ=="], + "@routstr/sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.1", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-EdUdjR6D0eY1YT6irNh/d5SBRc/ug4WNpnlLBXbRI9iOb8UNnQeGfk9CJQWL9ElQMjQ/4g582SehKXzEAZ2vnw=="], - "@scure/bip39/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], - - "@scure/bip39/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], - - "applesauce-common/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - - "applesauce-common/applesauce-core": ["applesauce-core@6.1.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-+tOFNP/54Zz3Z2tKeCqQZRdFQp0bwwPRiYO4A/PKOmx9iHO2XOFMrPEuRGtNqaHeBmDQQn0pFNB9mif0t94g8w=="], + "applesauce-common/applesauce-core": ["applesauce-core@6.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-O6AlVyzqcuIhTOIuexm6UWmx7mRIa2D98gJP7K7vFGf90YdtvSH78AsKQWZXJ0Pk/K14at+9zkwfCkFr7ghgNw=="], "applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], @@ -358,11 +362,9 @@ "applesauce-relay/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], - "applesauce-sqlite/applesauce-core": ["applesauce-core@6.1.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-+tOFNP/54Zz3Z2tKeCqQZRdFQp0bwwPRiYO4A/PKOmx9iHO2XOFMrPEuRGtNqaHeBmDQQn0pFNB9mif0t94g8w=="], + "applesauce-sqlite/applesauce-core": ["applesauce-core@6.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-O6AlVyzqcuIhTOIuexm6UWmx7mRIa2D98gJP7K7vFGf90YdtvSH78AsKQWZXJ0Pk/K14at+9zkwfCkFr7ghgNw=="], - "applesauce-wallet-connect/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "applesauce-wallet-connect/applesauce-core": ["applesauce-core@6.1.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-+tOFNP/54Zz3Z2tKeCqQZRdFQp0bwwPRiYO4A/PKOmx9iHO2XOFMrPEuRGtNqaHeBmDQQn0pFNB9mif0t94g8w=="], + "applesauce-wallet-connect/applesauce-core": ["applesauce-core@6.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-O6AlVyzqcuIhTOIuexm6UWmx7mRIa2D98gJP7K7vFGf90YdtvSH78AsKQWZXJ0Pk/K14at+9zkwfCkFr7ghgNw=="], "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], @@ -376,6 +378,8 @@ "nostr-tools/@scure/bip32": ["@scure/bip32@2.0.1", "", { "dependencies": { "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA=="], + "nostr-tools/@scure/bip39": ["@scure/bip39@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg=="], + "@freedomofpress/crypto-browser/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "applesauce-common/applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], @@ -424,10 +428,14 @@ "applesauce-core/nostr-tools/@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="], + "applesauce-core/nostr-tools/@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + "applesauce-relay/nostr-tools/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], "applesauce-relay/nostr-tools/@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="], + "applesauce-relay/nostr-tools/@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + "applesauce-sqlite/applesauce-core/nostr-tools/@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="], "applesauce-sqlite/applesauce-core/nostr-tools/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], @@ -456,12 +464,18 @@ "applesauce-common/applesauce-core/nostr-tools/@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="], + "applesauce-common/applesauce-core/nostr-tools/@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + "applesauce-sqlite/applesauce-core/nostr-tools/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], "applesauce-sqlite/applesauce-core/nostr-tools/@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="], + "applesauce-sqlite/applesauce-core/nostr-tools/@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + "applesauce-wallet-connect/applesauce-core/nostr-tools/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], "applesauce-wallet-connect/applesauce-core/nostr-tools/@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="], + + "applesauce-wallet-connect/applesauce-core/nostr-tools/@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], } } diff --git a/package.json b/package.json index c5b40d1..cbac95f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,10 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", + "@cashu/coco-core": "^1.0.1", + "@cashu/coco-sqlite-bun": "^1.0.1", "@routstr/sdk": "^0.3.17", + "@scure/bip39": "^2.2.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", diff --git a/src/daemon/index.ts b/src/daemon/index.ts index c44e605..306805a 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -42,12 +42,12 @@ import { } from "@routstr/sdk/storage/bun"; import { createWalletAdapter } from "./wallet"; import type { AutoRefillConfig } from "./wallet/auto-refill"; -import { createCocodClient } from "./wallet/cocod-client"; import { createModelService } from "./models"; import { createDaemonRequestHandler } from "./http"; import { FileRequestResponseLogSink } from "./request-response-log-sink"; import { refreshModelsAndIntegrations } from "../integrations"; import { RoutstrClient } from "@routstr/sdk"; +import { createCocoClient } from "./wallet/coco-client"; // Global error handlers — the daemon is spawned detached with stdout/stderr // redirected to a file, so without these, uncaught async errors would kill @@ -104,7 +104,7 @@ async function main(): Promise { const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } = createModelService(modelManager, store); - const walletClient = createCocodClient({ cocodPath: config.cocodPath }); + const walletClient = await createCocoClient(); // ── Auto-refill configuration ──────────────────────────────── // Uses a getter that reads config from disk each cycle, so diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts new file mode 100644 index 0000000..d515a03 --- /dev/null +++ b/src/daemon/wallet/coco-client.ts @@ -0,0 +1,142 @@ +import { initializeCoco, getEncodedToken } from "@cashu/coco-core"; +import { SqliteRepositories } from "@cashu/coco-sqlite-bun"; +import { Database } from "bun:sqlite"; +import { existsSync, readFileSync } from "fs"; +import { join } from "path"; +import { mnemonicToSeedSync } from "@scure/bip39"; +import type { CocodClient, CocodState } from "./cocod-client"; +import { logger } from "../../utils/logger"; + +const CONFIG_DIR = + process.env.COCOD_DIR || + `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`; + +const CONFIG_FILE = join(CONFIG_DIR, "config.json"); +const DB_PATH = join(CONFIG_DIR, "coco.db"); + +interface CocodConfig { + mnemonic: string; + encrypted: boolean; +} + +function loadMnemonic(): string { + if (!existsSync(CONFIG_FILE)) { + throw new Error( + `Config file not found at ${CONFIG_FILE}. Run 'routstrd onboard' first.`, + ); + } + const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as CocodConfig; + if (config.encrypted) { + throw new Error( + "Encrypted wallets are not supported yet. Please use an unencrypted wallet.", + ); + } + return config.mnemonic; +} + +async function seedGetter(): Promise { + const mnemonic = loadMnemonic(); + return mnemonicToSeedSync(mnemonic); +} + +export async function createCocoClient(): Promise { + const database = new Database(DB_PATH); + const repo = new SqliteRepositories({ database }); + await repo.init(); + + const coco = await initializeCoco({ + repo, + seedGetter, + }); + + return { + async ping(): Promise { + try { + await coco.wallet.balances.total(); + return true; + } catch { + return false; + } + }, + + async getStatus(): Promise { + return "UNLOCKED"; + }, + + async unlock(_passphrase: string): Promise { + return "already unlocked"; + }, + + async getBalances(): Promise> { + const byMint = await coco.wallet.balances.byMint(); + return Object.fromEntries( + Object.entries(byMint).map(([mintUrl, snapshot]) => [ + mintUrl, + snapshot.spendable, + ]), + ); + }, + + async receiveCashu(token: string): Promise { + await coco.wallet.receive(token); + return "Token received successfully"; + }, + + async receiveBolt11(amount: number, mintUrl?: string): Promise { + const mints = await coco.mint.getAllTrustedMints(); + const targetMint = mintUrl || mints[0]?.mintUrl; + if (!targetMint) { + throw new Error("No trusted mint available for Lightning invoice"); + } + const op = await coco.ops.mint.prepare({ + mintUrl: targetMint, + amount, + method: "bolt11", + }); + // PendingMintOperation extends MintQuoteSnapshot which has `request` + return (op as unknown as { request: string }).request; + }, + + async sendCashu(amount: number, mintUrl?: string): Promise { + const mints = await coco.mint.getAllTrustedMints(); + const targetMint = mintUrl || mints[0]?.mintUrl; + if (!targetMint) { + throw new Error("No trusted mint available for sending"); + } + const prepared = await coco.ops.send.prepare({ + mintUrl: targetMint, + amount, + }); + const { token } = await coco.ops.send.execute(prepared); + return getEncodedToken(token); + }, + + async sendBolt11(invoice: string, mintUrl?: string): Promise { + const mints = await coco.mint.getAllTrustedMints(); + const targetMint = mintUrl || mints[0]?.mintUrl; + if (!targetMint) { + throw new Error("No trusted mint available for Lightning payment"); + } + await coco.ops.melt.prepare({ + mintUrl: targetMint, + method: "bolt11", + methodData: { invoice }, + }); + return "Payment initiated successfully"; + }, + + async listMints(): Promise { + const mints = await coco.mint.getAllMints(); + return mints.map((m) => m.mintUrl); + }, + + async addMint(url: string): Promise { + await coco.mint.addMint(url, { trusted: true }); + return `Mint ${url} added successfully`; + }, + + async getMintInfo(url: string): Promise { + return coco.mint.getMintInfo(url); + }, + }; +} From 9ca937d4ad0abbd0696f57dbb011da610a2bf834 Mon Sep 17 00:00:00 2001 From: Om Santoshwar Date: Mon, 1 Jun 2026 11:46:43 +0530 Subject: [PATCH 02/32] feat(cli): remove cocod dependency from onboard and start commands --- src/cli.ts | 371 +++++++++++++++++++++++++---------------------------- 1 file changed, 175 insertions(+), 196 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 06cdd90..aeb72af 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,7 @@ import { deleteClientAction, addClientAction, } from "./utils/clients"; -import { existsSync, mkdirSync } from "fs"; +import { existsSync, mkdirSync, writeFileSync } from "fs"; import { execSync } from "child_process"; import { CONFIG_DIR, @@ -29,12 +29,14 @@ import { logger } from "./utils/logger"; import { setupIntegration, runIntegrationsForClients } from "./integrations"; import { getClientsList } from "./utils/clients"; import * as QRCode from "qrcode"; -import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98"; -import { generateSecretKey, nip19 } from "nostr-tools"; import { - isCocodInstalled, - resolveCocodExecutable, -} from "./daemon/wallet/cocod-client"; + normalizeNostrPubkey, + npubFromPubkey, + npubFromSecretKey, +} from "./utils/nip98"; +import { generateSecretKey, nip19 } from "nostr-tools"; +import { generateMnemonic } from "@scure/bip39"; +import { wordlist } from "@scure/bip39/wordlists/english.js"; import packageJson from "../package.json" with { type: "json" }; type RoutstrModel = { @@ -81,26 +83,28 @@ async function printLightningInvoice(invoice: string): Promise { console.log(`${qr}\nInvoice:\n${invoice}`); } -async function installCocodOrExit(): Promise { - console.log("cocod not found. Installing globally with bun..."); +function initializeWallet(): void { + const walletDir = + process.env.COCOD_DIR || + `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`; + const walletConfig = `${walletDir}/config.json`; - const installProc = Bun.spawn( - ["bun", "install", "--global", "@routstr/cocod"], - { - stdout: "inherit", - stderr: "inherit", - }, - ); - - const installCode = await installProc.exited; - if (installCode !== 0 || !(await isCocodInstalled())) { - console.error( - "Failed to install cocod. Please run 'bun install --global @routstr/cocod' manually.", - ); - throw new Error("cocod installation failed"); + if (existsSync(walletConfig)) { + console.log("Wallet already initialized."); + return; } - console.log("cocod installed successfully."); + mkdirSync(walletDir, { recursive: true }); + const mnemonic = generateMnemonic(wordlist); + const config = { + version: 1, + mnemonic, + encrypted: false, + createdAt: new Date().toISOString(), + }; + writeFileSync(walletConfig, JSON.stringify(config, null, 2)); + console.log("Initialized. Mnemonic:", mnemonic); + console.log("IMPORTANT: Write down this mnemonic and keep it safe!"); } /** @@ -268,75 +272,9 @@ async function initDaemon(): Promise { console.log(`Your npub: ${npub}`); console.log(`You can view it in the config file at: ${CONFIG_FILE}\n`); } - - if (!(await isCocodInstalled(config.cocodPath))) { - if (config.cocodPath) { - logger.error( - `Configured cocod executable was not found: ${config.cocodPath}`, - ); - return; - } - - await installCocodOrExit(); - } - - const cocodExecutable = resolveCocodExecutable(config.cocodPath); - + console.log(`Database will be stored at: ${DB_PATH}`); - console.log("\nInitializing cocod..."); - - const initProc = Bun.spawn([cocodExecutable, "init"], { - stdout: "pipe", - stderr: "pipe", - }); - - let initStdout = ""; - let initStderr = ""; - - const stdoutDone = initProc.stdout - ? initProc.stdout.pipeTo( - new WritableStream({ - write(chunk) { - const text = new TextDecoder().decode(chunk); - initStdout += text; - process.stdout.write(text); - }, - }), - ) - : Promise.resolve(); - - const stderrDone = initProc.stderr - ? initProc.stderr.pipeTo( - new WritableStream({ - write(chunk) { - const text = new TextDecoder().decode(chunk); - initStderr += text; - process.stderr.write(text); - }, - }), - ) - : Promise.resolve(); - - const [initCode] = await Promise.all([ - initProc.exited, - stdoutDone, - stderrDone, - ]); - const combinedOutput = `${initStdout}\n${initStderr}`.toLowerCase(); - const alreadyInitialized = combinedOutput.includes("already initialized"); - - if (initCode !== 0 && !alreadyInitialized) { - console.error( - "Failed to initialize cocod. Please run 'cocod init' manually.", - ); - return; - } - - if (alreadyInitialized) { - console.log("cocod is already initialized."); - } else { - console.log("cocod initialized successfully."); - } + initializeWallet(); await startDaemon({ port: String(config.port || 8008) }); @@ -401,36 +339,76 @@ program "Mint URL to refund to (defaults to first mint in wallet)", ) .option("-y, --yes", "Skip confirmation prompt", false) - .option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false) - .action(async (options: { mintUrl?: string; yes: boolean; xcashu: boolean }) => { - await ensureDaemonRunning(); + .option( + "--xcashu", + "Refund xcashu tokens only (uses refundXcashuTokens)", + false, + ) + .action( + async (options: { mintUrl?: string; yes: boolean; xcashu: boolean }) => { + await ensureDaemonRunning(); - let mintUrl = options.mintUrl; - if (!mintUrl) { - const balanceResult = await callDaemon("/balance"); - if (balanceResult.error) { - console.log(balanceResult.error); - process.exit(1); + let mintUrl = options.mintUrl; + if (!mintUrl) { + const balanceResult = await callDaemon("/balance"); + if (balanceResult.error) { + console.log(balanceResult.error); + process.exit(1); + } + const balances = ( + balanceResult.output as + | { + balances?: Record; + } + | undefined + )?.balances; + if (!balances || Object.keys(balances).length === 0) { + console.log("No mint URLs found in wallet balance"); + process.exit(1); + } + mintUrl = Object.keys(balances)[0]; + console.log(`Using mint URL: ${mintUrl}`); } - const balances = ( - balanceResult.output as - | { - balances?: Record; + + try { + if (options.xcashu) { + // xcashu path: only refund xcashu tokens + const result = await callDaemon("/refund/xcashu", { + method: "POST", + body: { mintUrl }, + }); + + if (result.error) { + console.log(result.error); + process.exit(1); + } + + const output = result.output as + | { + message: string; + results: Array<{ + baseUrl: string; + token: string; + success: boolean; + error?: string; + }>; + } + | undefined; + + if (output) { + console.log(output.message); + console.log("\nResults:"); + for (const r of output.results) { + const status = r.success + ? "success" + : `failed: ${r.error || "unknown"}`; + console.log(` - ${r.baseUrl}: ${status}`); } - | undefined - )?.balances; - if (!balances || Object.keys(balances).length === 0) { - console.log("No mint URLs found in wallet balance"); - process.exit(1); - } - mintUrl = Object.keys(balances)[0]; - console.log(`Using mint URL: ${mintUrl}`); - } + } + return; + } - try { - if (options.xcashu) { - // xcashu path: only refund xcashu tokens - const result = await callDaemon("/refund/xcashu", { + const result = await callDaemon("/refund", { method: "POST", body: { mintUrl }, }); @@ -443,68 +421,46 @@ program const output = result.output as | { message: string; - results: Array<{ baseUrl: string; token: string; success: boolean; error?: string }>; + pendingTokens: number; + apiKeys: number; + results: Array<{ baseUrl: string; success: boolean }>; } | undefined; if (output) { console.log(output.message); + console.log(`\nPending tokens: ${output.pendingTokens}`); + console.log(`API keys: ${output.apiKeys}`); console.log("\nResults:"); for (const r of output.results) { - const status = r.success ? "success" : `failed: ${r.error || "unknown"}`; - console.log(` - ${r.baseUrl}: ${status}`); + console.log( + ` - ${r.baseUrl}: ${r.success ? "success" : "failed"}`, + ); } } - return; - } - - const result = await callDaemon("/refund", { - method: "POST", - body: { mintUrl }, - }); - - if (result.error) { - console.log(result.error); - process.exit(1); - } - - const output = result.output as - | { - message: string; - pendingTokens: number; - apiKeys: number; - results: Array<{ baseUrl: string; success: boolean }>; - } - | undefined; - - if (output) { - console.log(output.message); - console.log(`\nPending tokens: ${output.pendingTokens}`); - console.log(`API keys: ${output.apiKeys}`); - console.log("\nResults:"); - for (const r of output.results) { - console.log(` - ${r.baseUrl}: ${r.success ? "success" : "failed"}`); + } catch (error) { + const message = (error as Error).message; + if ( + message?.includes("fetch failed") || + message?.includes("Connection refused") + ) { + console.error("Daemon is not running"); + process.exit(1); } - } - } catch (error) { - const message = (error as Error).message; - if ( - message?.includes("fetch failed") || - message?.includes("Connection refused") - ) { - console.error("Daemon is not running"); + console.error(message); process.exit(1); } - console.error(message); - process.exit(1); - } - }); + }, + ); // Remote - configure a remote daemon URL program .command("remote ") .description("Configure a remote daemon URL") - .option("--auth-url ", "URL of the auth proxy for management commands (npubs, clients, usage)") + .option( + "--auth-url ", + "URL of the auth proxy for management commands (npubs, clients, usage)", + ) .action(async (url: string, options: { authUrl?: string }) => { try { new URL(url); @@ -557,9 +513,7 @@ program `\nA new Nostr identity has been generated for remote authentication.`, ); console.log(`Your npub: ${generatedNpub}`); - console.log( - `You can view it in the config file at: ${CONFIG_FILE}`, - ); + console.log(`You can view it in the config file at: ${CONFIG_FILE}`); } }); @@ -583,13 +537,6 @@ program .action(async (options: { port?: string; provider?: string }) => { await requireLocalDaemon(); const config = await loadConfig(); - if (!(await isCocodInstalled(config.cocodPath))) { - const installHint = config.cocodPath - ? `Configured cocod executable was not found: ${config.cocodPath}` - : "cocod is not installed. Run 'routstrd onboard' first to install cocod."; - logger.error(installHint); - process.exit(1); - } await startDaemon({ port: options.port || String(config.port || 8008), provider: options.provider, @@ -1081,7 +1028,9 @@ npubsCmd : result; const npubs = (data as { npubs?: NpubEntry[] } | undefined)?.npubs ?? []; if (npubs.length === 0) { - console.log("No admin npubs configured. Run 'routstrd npubs register' to register yourself as the first admin."); + console.log( + "No admin npubs configured. Run 'routstrd npubs register' to register yourself as the first admin.", + ); return; } console.log(`Npubs (${npubs.length}):`); @@ -1102,7 +1051,9 @@ npubsCmd npubsCmd .command("register") - .description("Register yourself as the first admin (only when no admins exist)") + .description( + "Register yourself as the first admin (only when no admins exist)", + ) .action(async () => { await ensureDaemonRunning(); const config = await loadConfig(); @@ -1123,7 +1074,9 @@ npubsCmd : result; const npubs = (data as { npubs?: NpubEntry[] } | undefined)?.npubs ?? []; if (npubs.length > 0) { - console.log(`Admin npubs already configured (${npubs.length}). Ask your admin to add your npub. \n Your npub: ${userNpub}`); + console.log( + `Admin npubs already configured (${npubs.length}). Ask your admin to add your npub. \n Your npub: ${userNpub}`, + ); return; } const normalized = normalizeNostrPubkey(userNpub); @@ -1143,7 +1096,9 @@ npubsCmd | { npub?: string; added?: boolean; error?: string } | undefined; if (output?.npub) { - console.log(`Successfully registered as first admin npub: ${output.npub}`); + console.log( + `Successfully registered as first admin npub: ${output.npub}`, + ); } else { console.log(`Successfully registered as first admin npub: ${userNpub}`); } @@ -1151,8 +1106,14 @@ npubsCmd npubsCmd .command("add ") - .description("Add a npub (hex pubkey or npub1...). Defaults to 'user' role unless --role is specified.") - .option("-r, --role ", "Role for the npub: 'admin' or 'user' (default: 'user')", "user") + .description( + "Add a npub (hex pubkey or npub1...). Defaults to 'user' role unless --role is specified.", + ) + .option( + "-r, --role ", + "Role for the npub: 'admin' or 'user' (default: 'user')", + "user", + ) .action(async (npubArg: string, options: { role: string }) => { await ensureDaemonRunning(); const normalized = normalizeNostrPubkey(npubArg); @@ -1164,7 +1125,10 @@ npubsCmd console.error("Invalid role. Expected 'admin' or 'user'."); process.exit(1); } - const body: Record = { npub: npubFromPubkey(normalized), role: options.role }; + const body: Record = { + npub: npubFromPubkey(normalized), + role: options.role, + }; const result = await callAuth("/npubs", { method: "POST", body, @@ -1481,7 +1445,10 @@ const nwcCmd = program nwcCmd .command("connect") .description("Connect to a Lightning wallet via NWC") - .argument("[connection-string]", "NWC connection string (nostr+walletconnect://...)") + .argument( + "[connection-string]", + "NWC connection string (nostr+walletconnect://...)", + ) .action(async (connectionString?: string) => { if (!connectionString) { // Interactive mode: prompt for connection string @@ -1498,8 +1465,14 @@ nwcCmd } // Quick validation: must be nostr+walletconnect:// with a 64-char hex pubkey - if (!/^nostr\+walletconnect:\/\/[0-9a-fA-F]{64}\?relay=/.test(connectionString)) { - console.error("Invalid NWC connection string: expected nostr+walletconnect://<64-char-hex>?relay=..."); + if ( + !/^nostr\+walletconnect:\/\/[0-9a-fA-F]{64}\?relay=/.test( + connectionString, + ) + ) { + console.error( + "Invalid NWC connection string: expected nostr+walletconnect://<64-char-hex>?relay=...", + ); process.exit(1); } @@ -1554,21 +1527,27 @@ autoRefillCmd "Minimum time between refills in seconds", "300", ) - .action(async (options: { threshold: string; amount: string; cooldown: string }) => { - const threshold = parsePositiveIntOrExit(options.threshold, "threshold"); - const amount = parsePositiveIntOrExit(options.amount, "amount"); - const cooldownSec = parsePositiveIntOrExit(options.cooldown, "cooldown"); + .action( + async (options: { + threshold: string; + amount: string; + cooldown: string; + }) => { + const threshold = parsePositiveIntOrExit(options.threshold, "threshold"); + const amount = parsePositiveIntOrExit(options.amount, "amount"); + const cooldownSec = parsePositiveIntOrExit(options.cooldown, "cooldown"); - await handleDaemonCommand("/nwc/auto-refill", { - method: "POST", - body: { - enabled: true, - threshold, - amount, - cooldownMs: cooldownSec * 1000, - }, - }); - }); + await handleDaemonCommand("/nwc/auto-refill", { + method: "POST", + body: { + enabled: true, + threshold, + amount, + cooldownMs: cooldownSec * 1000, + }, + }); + }, + ); autoRefillCmd .command("off") From a2219e43a9dcc7959bf448ee56626eea3c58cced Mon Sep 17 00:00:00 2001 From: Om Santoshwar Date: Tue, 2 Jun 2026 22:41:18 +0530 Subject: [PATCH 03/32] fix: revert cosmetic formatting and document coco-core wallet status --- src/cli.ts | 258 ++++++++++++------------------- src/daemon/wallet/coco-client.ts | 6 +- 2 files changed, 107 insertions(+), 157 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index aeb72af..4e9cde6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -29,11 +29,7 @@ import { logger } from "./utils/logger"; import { setupIntegration, runIntegrationsForClients } from "./integrations"; import { getClientsList } from "./utils/clients"; import * as QRCode from "qrcode"; -import { - normalizeNostrPubkey, - npubFromPubkey, - npubFromSecretKey, -} from "./utils/nip98"; +import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98"; import { generateSecretKey, nip19 } from "nostr-tools"; import { generateMnemonic } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english.js"; @@ -88,12 +84,10 @@ function initializeWallet(): void { process.env.COCOD_DIR || `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`; const walletConfig = `${walletDir}/config.json`; - if (existsSync(walletConfig)) { console.log("Wallet already initialized."); return; } - mkdirSync(walletDir, { recursive: true }); const mnemonic = generateMnemonic(wordlist); const config = { @@ -272,7 +266,7 @@ async function initDaemon(): Promise { console.log(`Your npub: ${npub}`); console.log(`You can view it in the config file at: ${CONFIG_FILE}\n`); } - + console.log(`Database will be stored at: ${DB_PATH}`); initializeWallet(); @@ -339,76 +333,36 @@ program "Mint URL to refund to (defaults to first mint in wallet)", ) .option("-y, --yes", "Skip confirmation prompt", false) - .option( - "--xcashu", - "Refund xcashu tokens only (uses refundXcashuTokens)", - false, - ) - .action( - async (options: { mintUrl?: string; yes: boolean; xcashu: boolean }) => { - await ensureDaemonRunning(); + .option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false) + .action(async (options: { mintUrl?: string; yes: boolean; xcashu: boolean }) => { + await ensureDaemonRunning(); - let mintUrl = options.mintUrl; - if (!mintUrl) { - const balanceResult = await callDaemon("/balance"); - if (balanceResult.error) { - console.log(balanceResult.error); - process.exit(1); - } - const balances = ( - balanceResult.output as - | { - balances?: Record; - } - | undefined - )?.balances; - if (!balances || Object.keys(balances).length === 0) { - console.log("No mint URLs found in wallet balance"); - process.exit(1); - } - mintUrl = Object.keys(balances)[0]; - console.log(`Using mint URL: ${mintUrl}`); + let mintUrl = options.mintUrl; + if (!mintUrl) { + const balanceResult = await callDaemon("/balance"); + if (balanceResult.error) { + console.log(balanceResult.error); + process.exit(1); } - - try { - if (options.xcashu) { - // xcashu path: only refund xcashu tokens - const result = await callDaemon("/refund/xcashu", { - method: "POST", - body: { mintUrl }, - }); - - if (result.error) { - console.log(result.error); - process.exit(1); - } - - const output = result.output as - | { - message: string; - results: Array<{ - baseUrl: string; - token: string; - success: boolean; - error?: string; - }>; - } - | undefined; - - if (output) { - console.log(output.message); - console.log("\nResults:"); - for (const r of output.results) { - const status = r.success - ? "success" - : `failed: ${r.error || "unknown"}`; - console.log(` - ${r.baseUrl}: ${status}`); + const balances = ( + balanceResult.output as + | { + balances?: Record; } - } - return; - } + | undefined + )?.balances; + if (!balances || Object.keys(balances).length === 0) { + console.log("No mint URLs found in wallet balance"); + process.exit(1); + } + mintUrl = Object.keys(balances)[0]; + console.log(`Using mint URL: ${mintUrl}`); + } - const result = await callDaemon("/refund", { + try { + if (options.xcashu) { + // xcashu path: only refund xcashu tokens + const result = await callDaemon("/refund/xcashu", { method: "POST", body: { mintUrl }, }); @@ -421,46 +375,68 @@ program const output = result.output as | { message: string; - pendingTokens: number; - apiKeys: number; - results: Array<{ baseUrl: string; success: boolean }>; + results: Array<{ baseUrl: string; token: string; success: boolean; error?: string }>; } | undefined; if (output) { console.log(output.message); - console.log(`\nPending tokens: ${output.pendingTokens}`); - console.log(`API keys: ${output.apiKeys}`); console.log("\nResults:"); for (const r of output.results) { - console.log( - ` - ${r.baseUrl}: ${r.success ? "success" : "failed"}`, - ); + const status = r.success ? "success" : `failed: ${r.error || "unknown"}`; + console.log(` - ${r.baseUrl}: ${status}`); } } - } catch (error) { - const message = (error as Error).message; - if ( - message?.includes("fetch failed") || - message?.includes("Connection refused") - ) { - console.error("Daemon is not running"); - process.exit(1); - } - console.error(message); + return; + } + + const result = await callDaemon("/refund", { + method: "POST", + body: { mintUrl }, + }); + + if (result.error) { + console.log(result.error); process.exit(1); } - }, - ); + + const output = result.output as + | { + message: string; + pendingTokens: number; + apiKeys: number; + results: Array<{ baseUrl: string; success: boolean }>; + } + | undefined; + + if (output) { + console.log(output.message); + console.log(`\nPending tokens: ${output.pendingTokens}`); + console.log(`API keys: ${output.apiKeys}`); + console.log("\nResults:"); + for (const r of output.results) { + console.log(` - ${r.baseUrl}: ${r.success ? "success" : "failed"}`); + } + } + } catch (error) { + const message = (error as Error).message; + if ( + message?.includes("fetch failed") || + message?.includes("Connection refused") + ) { + console.error("Daemon is not running"); + process.exit(1); + } + console.error(message); + process.exit(1); + } + }); // Remote - configure a remote daemon URL program .command("remote ") .description("Configure a remote daemon URL") - .option( - "--auth-url ", - "URL of the auth proxy for management commands (npubs, clients, usage)", - ) + .option("--auth-url ", "URL of the auth proxy for management commands (npubs, clients, usage)") .action(async (url: string, options: { authUrl?: string }) => { try { new URL(url); @@ -513,7 +489,9 @@ program `\nA new Nostr identity has been generated for remote authentication.`, ); console.log(`Your npub: ${generatedNpub}`); - console.log(`You can view it in the config file at: ${CONFIG_FILE}`); + console.log( + `You can view it in the config file at: ${CONFIG_FILE}`, + ); } }); @@ -1028,9 +1006,7 @@ npubsCmd : result; const npubs = (data as { npubs?: NpubEntry[] } | undefined)?.npubs ?? []; if (npubs.length === 0) { - console.log( - "No admin npubs configured. Run 'routstrd npubs register' to register yourself as the first admin.", - ); + console.log("No admin npubs configured. Run 'routstrd npubs register' to register yourself as the first admin."); return; } console.log(`Npubs (${npubs.length}):`); @@ -1051,9 +1027,7 @@ npubsCmd npubsCmd .command("register") - .description( - "Register yourself as the first admin (only when no admins exist)", - ) + .description("Register yourself as the first admin (only when no admins exist)") .action(async () => { await ensureDaemonRunning(); const config = await loadConfig(); @@ -1074,9 +1048,7 @@ npubsCmd : result; const npubs = (data as { npubs?: NpubEntry[] } | undefined)?.npubs ?? []; if (npubs.length > 0) { - console.log( - `Admin npubs already configured (${npubs.length}). Ask your admin to add your npub. \n Your npub: ${userNpub}`, - ); + console.log(`Admin npubs already configured (${npubs.length}). Ask your admin to add your npub. \n Your npub: ${userNpub}`); return; } const normalized = normalizeNostrPubkey(userNpub); @@ -1096,9 +1068,7 @@ npubsCmd | { npub?: string; added?: boolean; error?: string } | undefined; if (output?.npub) { - console.log( - `Successfully registered as first admin npub: ${output.npub}`, - ); + console.log(`Successfully registered as first admin npub: ${output.npub}`); } else { console.log(`Successfully registered as first admin npub: ${userNpub}`); } @@ -1106,14 +1076,8 @@ npubsCmd npubsCmd .command("add ") - .description( - "Add a npub (hex pubkey or npub1...). Defaults to 'user' role unless --role is specified.", - ) - .option( - "-r, --role ", - "Role for the npub: 'admin' or 'user' (default: 'user')", - "user", - ) + .description("Add a npub (hex pubkey or npub1...). Defaults to 'user' role unless --role is specified.") + .option("-r, --role ", "Role for the npub: 'admin' or 'user' (default: 'user')", "user") .action(async (npubArg: string, options: { role: string }) => { await ensureDaemonRunning(); const normalized = normalizeNostrPubkey(npubArg); @@ -1125,10 +1089,7 @@ npubsCmd console.error("Invalid role. Expected 'admin' or 'user'."); process.exit(1); } - const body: Record = { - npub: npubFromPubkey(normalized), - role: options.role, - }; + const body: Record = { npub: npubFromPubkey(normalized), role: options.role }; const result = await callAuth("/npubs", { method: "POST", body, @@ -1445,10 +1406,7 @@ const nwcCmd = program nwcCmd .command("connect") .description("Connect to a Lightning wallet via NWC") - .argument( - "[connection-string]", - "NWC connection string (nostr+walletconnect://...)", - ) + .argument("[connection-string]", "NWC connection string (nostr+walletconnect://...)") .action(async (connectionString?: string) => { if (!connectionString) { // Interactive mode: prompt for connection string @@ -1465,14 +1423,8 @@ nwcCmd } // Quick validation: must be nostr+walletconnect:// with a 64-char hex pubkey - if ( - !/^nostr\+walletconnect:\/\/[0-9a-fA-F]{64}\?relay=/.test( - connectionString, - ) - ) { - console.error( - "Invalid NWC connection string: expected nostr+walletconnect://<64-char-hex>?relay=...", - ); + if (!/^nostr\+walletconnect:\/\/[0-9a-fA-F]{64}\?relay=/.test(connectionString)) { + console.error("Invalid NWC connection string: expected nostr+walletconnect://<64-char-hex>?relay=..."); process.exit(1); } @@ -1527,27 +1479,21 @@ autoRefillCmd "Minimum time between refills in seconds", "300", ) - .action( - async (options: { - threshold: string; - amount: string; - cooldown: string; - }) => { - const threshold = parsePositiveIntOrExit(options.threshold, "threshold"); - const amount = parsePositiveIntOrExit(options.amount, "amount"); - const cooldownSec = parsePositiveIntOrExit(options.cooldown, "cooldown"); + .action(async (options: { threshold: string; amount: string; cooldown: string }) => { + const threshold = parsePositiveIntOrExit(options.threshold, "threshold"); + const amount = parsePositiveIntOrExit(options.amount, "amount"); + const cooldownSec = parsePositiveIntOrExit(options.cooldown, "cooldown"); - await handleDaemonCommand("/nwc/auto-refill", { - method: "POST", - body: { - enabled: true, - threshold, - amount, - cooldownMs: cooldownSec * 1000, - }, - }); - }, - ); + await handleDaemonCommand("/nwc/auto-refill", { + method: "POST", + body: { + enabled: true, + threshold, + amount, + cooldownMs: cooldownSec * 1000, + }, + }); + }); autoRefillCmd .command("off") @@ -1836,4 +1782,4 @@ program export function cli(args: string[]) { program.parse(args); -} +} \ No newline at end of file diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index d515a03..12bc4fe 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -60,11 +60,15 @@ export async function createCocoClient(): Promise { }, async getStatus(): Promise { + // coco-core has no lock/unlock concept, the wallet is always + // ready once initialized. Return UNLOCKED to satisfy the interface. return "UNLOCKED"; }, async unlock(_passphrase: string): Promise { - return "already unlocked"; + // coco-core does not support passphrase locking. + // Wallet access is controlled via the mnemonic in ~/.cocod/config.json. + return "wallet does not require unlocking"; }, async getBalances(): Promise> { From 4afc50500d3850261482bb07ad02042994ac8dfe Mon Sep 17 00:00:00 2001 From: Om Santoshwar Date: Thu, 4 Jun 2026 23:30:22 +0530 Subject: [PATCH 04/32] fix(wallet): add cancel on send/melt failure, use trusted mints for listMints, improve receiveBolt11 type safety --- src/daemon/wallet/coco-client.ts | 36 +++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 12bc4fe..302bfab 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -60,9 +60,12 @@ export async function createCocoClient(): Promise { }, async getStatus(): Promise { - // coco-core has no lock/unlock concept, the wallet is always - // ready once initialized. Return UNLOCKED to satisfy the interface. - return "UNLOCKED"; + try { + await coco.wallet.balances.total(); + return "UNLOCKED"; + } catch { + return "ERROR"; + } }, async unlock(_passphrase: string): Promise { @@ -97,8 +100,10 @@ export async function createCocoClient(): Promise { amount, method: "bolt11", }); - // PendingMintOperation extends MintQuoteSnapshot which has `request` - return (op as unknown as { request: string }).request; + if (!("request" in op)) { + throw new Error("mint prepare did not return a payment request"); + } + return op.request as string; }, async sendCashu(amount: number, mintUrl?: string): Promise { @@ -111,8 +116,13 @@ export async function createCocoClient(): Promise { mintUrl: targetMint, amount, }); - const { token } = await coco.ops.send.execute(prepared); - return getEncodedToken(token); + try { + const { token } = await coco.ops.send.execute(prepared.id); + return getEncodedToken(token); + } catch (error) { + await coco.ops.send.cancel(prepared.id); + throw error; + } }, async sendBolt11(invoice: string, mintUrl?: string): Promise { @@ -121,16 +131,22 @@ export async function createCocoClient(): Promise { if (!targetMint) { throw new Error("No trusted mint available for Lightning payment"); } - await coco.ops.melt.prepare({ + const prepared = await coco.ops.melt.prepare({ mintUrl: targetMint, method: "bolt11", methodData: { invoice }, }); - return "Payment initiated successfully"; + try { + await coco.ops.melt.execute(prepared.id); + return "Payment sent successfully"; + } catch (error) { + await coco.ops.melt.cancel(prepared.id); + throw error; + } }, async listMints(): Promise { - const mints = await coco.mint.getAllMints(); + const mints = await coco.mint.getAllTrustedMints(); return mints.map((m) => m.mintUrl); }, From ca2c53f8cd08e10e59b648e555909b15ddc74948 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:04:49 +0800 Subject: [PATCH 05/32] fix(cli): remove cocod restart/update from update command after coco-core migration After rebasing onto main, the update command and restartDaemonsAfterUpdate still referenced resolveCocodExecutable and @routstr/cocod. Since cocod is no longer a separate process, remove all cocod daemon management from the update flow. --- src/cli.ts | 98 ++++++++---------------------------------------------- 1 file changed, 14 insertions(+), 84 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 4e9cde6..eb47b59 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -102,9 +102,12 @@ function initializeWallet(): void { } /** - * Restart the routstrd and cocod daemons after an update so the new - * binaries take effect immediately. Failures are collected and reported - * but never roll back the update itself. + * Restart the routstrd daemon after an update so the new binary takes + * effect immediately. Failures are collected and reported but never + * roll back the update itself. + * + * Note: cocod is no longer a separate process — the wallet now runs + * in-process via coco-core, so there is nothing else to restart. */ async function restartDaemonsAfterUpdate(): Promise { const config = await loadConfig(); @@ -149,80 +152,19 @@ async function restartDaemonsAfterUpdate(): Promise { } } - // --- cocod daemon --- - try { - const cocodExecutable = resolveCocodExecutable(config.cocodPath); - - // Check whether cocod is currently running. - const pingProc = Bun.spawn([cocodExecutable, "ping"], { - stdout: "ignore", - stderr: "ignore", - }); - const cocodWasRunning = (await pingProc.exited) === 0; - - if (!cocodWasRunning) { - console.log("\ncocod daemon was not running — skipping restart."); - } else { - console.log("\nRestarting cocod daemon..."); - - const stopProc = Bun.spawn([cocodExecutable, "stop"], { - stdout: "inherit", - stderr: "inherit", - }); - const stopCode = await stopProc.exited; - if (stopCode !== 0) { - throw new Error(`cocod stop exited with code ${stopCode}`); - } - console.log("cocod daemon stopped."); - - // Start cocod in the background (detached, like the wallet client does). - const env = { ...process.env }; - const startProc = Bun.spawn([cocodExecutable, "daemon"], { - stdout: "ignore", - stderr: "ignore", - stdin: "ignore", - detached: true, - env, - }); - startProc.unref?.(); - - // Poll until cocod responds to ping (max ~10 s). - let started = false; - for (let i = 0; i < 100; i++) { - await new Promise((resolve) => setTimeout(resolve, 100)); - const poll = Bun.spawn([cocodExecutable, "ping"], { - stdout: "ignore", - stderr: "ignore", - }); - if ((await poll.exited) === 0) { - started = true; - break; - } - } - - if (!started) { - throw new Error("cocod did not come back up within 10 seconds"); - } - console.log("cocod daemon restarted."); - } - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - failures.push(`cocod daemon: ${msg}`); - } - // --- report --- if (failures.length > 0) { - console.error("\n⚠ Some daemons failed to restart:"); + console.error("\n⚠ Failed to restart daemon:"); for (const f of failures) { console.error(` - ${f}`); } console.error( - "The update was applied but may not take effect until daemons are manually restarted.", + "The update was applied but may not take effect until the daemon is manually restarted.", ); process.exit(1); } - console.log("\n✓ All daemons restarted successfully."); + console.log("\n✓ Daemon restarted successfully."); } async function requireLocalDaemon(): Promise { @@ -293,7 +235,7 @@ program program .command("update") - .description("Update routstrd and cocod to the latest versions") + .description("Update routstrd to the latest version") .action(async () => { console.log("Updating routstrd..."); const routstrdProc = Bun.spawn(["bun", "install", "-g", "routstrd"], { @@ -307,21 +249,9 @@ program } console.log("routstrd updated successfully.\n"); - console.log("Updating cocod..."); - const cocodProc = Bun.spawn(["bun", "install", "-g", "@routstr/cocod"], { - stdout: "inherit", - stderr: "inherit", - }); - const cocodCode = await cocodProc.exited; - if (cocodCode !== 0) { - console.error("Failed to update cocod."); - process.exit(1); - } - console.log("cocod updated successfully.\n"); + console.log("routstrd has been updated!"); - console.log("Both routstrd and cocod have been updated!"); - - // Restart daemons so the new binaries take effect immediately. + // Restart daemon so the new binary takes effect immediately. await restartDaemonsAfterUpdate(); }); @@ -499,7 +429,7 @@ program program .command("onboard") .description( - "Initialize routstrd (creates config directory and initializes cocod)", + "Initialize routstrd (creates config directory and initializes wallet)", ) .action(async () => { await requireLocalDaemon(); @@ -1782,4 +1712,4 @@ program export function cli(args: string[]) { program.parse(args); -} \ No newline at end of file +} From 19ae28c382116a63d680fd3316e094d34dae6b07 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:16:15 +0800 Subject: [PATCH 06/32] fix(wallet): block coco-core while legacy cocod is running Check the legacy cocod PID file and Unix socket before opening coco.db. Refuse startup when cocod is active or its state cannot be verified, while permitting confirmed stale socket files. Add focused coverage for PID, live socket, stale socket, and fail-closed behavior. --- src/daemon/wallet/coco-client.test.ts | 131 ++++++++++++++++++++++++ src/daemon/wallet/coco-client.ts | 137 ++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 src/daemon/wallet/coco-client.test.ts diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts new file mode 100644 index 0000000..2124e65 --- /dev/null +++ b/src/daemon/wallet/coco-client.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it, mock } from "bun:test"; +import { assertLegacyCocodNotRunning } from "./coco-client"; + +type GuardOptions = NonNullable< + Parameters[0] +>; +type LegacyFetch = NonNullable; + +const SOCKET_PATH = "/tmp/routstrd-test/cocod.sock"; +const PID_FILE_PATH = "/tmp/routstrd-test/cocod.pid"; + +function socketOnly(path: string): boolean { + return path === SOCKET_PATH; +} + +describe("assertLegacyCocodNotRunning", () => { + it("does not probe when the legacy socket and PID file do not exist", async () => { + const fetchImpl = mock(async () => new Response("pong")); + + await assertLegacyCocodNotRunning({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: () => false, + fetchImpl, + }); + + expect(fetchImpl).not.toHaveBeenCalled(); + }); + + it("refuses to continue when cocod responds on the legacy socket", async () => { + const fetchImpl = mock(async () => + Response.json({ output: "pong" }), + ); + + await expect( + assertLegacyCocodNotRunning({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: socketOnly, + fetchImpl, + }), + ).rejects.toThrow("Legacy cocod daemon is still running"); + + expect(fetchImpl).toHaveBeenCalledTimes(1); + expect(fetchImpl.mock.calls[0]?.[0]).toBe("http://localhost/ping"); + expect(fetchImpl.mock.calls[0]?.[1]).toMatchObject({ unix: SOCKET_PATH }); + }); + + it.each(["ENOENT", "ECONNREFUSED", "FailedToOpenSocket"])( + "allows startup for a stale socket that fails with %s", + async (code) => { + const fetchImpl = mock(async () => { + throw Object.assign(new Error("socket unavailable"), { code }); + }); + + await expect( + assertLegacyCocodNotRunning({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: socketOnly, + fetchImpl, + }), + ).resolves.toBeUndefined(); + }, + ); + + it("recognizes stale socket errors nested under cause", async () => { + const fetchImpl = mock(async () => { + throw new TypeError("fetch failed", { + cause: Object.assign(new Error("connection refused"), { + code: "ECONNREFUSED", + }), + }); + }); + + await expect( + assertLegacyCocodNotRunning({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: socketOnly, + fetchImpl, + }), + ).resolves.toBeUndefined(); + }); + + it("refuses to continue when the legacy PID is still running", async () => { + const fetchImpl = mock(async () => + Response.json({ output: "pong" }), + ); + + await expect( + assertLegacyCocodNotRunning({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: (path) => path === PID_FILE_PATH, + readFile: () => "4242\n", + isProcessRunning: (pid) => pid === 4242, + fetchImpl, + }), + ).rejects.toThrow("Legacy cocod daemon is still running with PID 4242"); + + expect(fetchImpl).not.toHaveBeenCalled(); + }); + + it("ignores a stale PID file when no socket exists", async () => { + await expect( + assertLegacyCocodNotRunning({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: (path) => path === PID_FILE_PATH, + readFile: () => "4242\n", + isProcessRunning: () => false, + }), + ).resolves.toBeUndefined(); + }); + + it("fails closed when the socket cannot be probed safely", async () => { + const fetchImpl = mock(async () => { + throw Object.assign(new Error("permission denied"), { code: "EACCES" }); + }); + + await expect( + assertLegacyCocodNotRunning({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: socketOnly, + fetchImpl, + }), + ).rejects.toThrow("Cannot verify whether the legacy cocod daemon has stopped"); + }); +}); diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 302bfab..122e528 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -13,6 +13,32 @@ const CONFIG_DIR = const CONFIG_FILE = join(CONFIG_DIR, "config.json"); const DB_PATH = join(CONFIG_DIR, "coco.db"); +const LEGACY_COCOD_SOCKET = + process.env.COCOD_SOCKET || join(CONFIG_DIR, "cocod.sock"); +const LEGACY_COCOD_PID_FILE = join(CONFIG_DIR, "cocod.pid"); + +const STALE_SOCKET_ERROR_CODES = new Set([ + "ECONNREFUSED", + "ENOENT", + // Bun's Unix-socket fetch error for an abandoned socket inode. + "FailedToOpenSocket", +]); + +type UnixRequestInit = RequestInit & { unix: string }; +type LegacyCocodFetch = ( + input: string | URL | Request, + init: UnixRequestInit, +) => Promise; + +export interface LegacyCocodGuardOptions { + socketPath?: string; + pidFilePath?: string; + pathExists?: (path: string) => boolean; + readFile?: (path: string) => string; + isProcessRunning?: (pid: number) => boolean; + fetchImpl?: LegacyCocodFetch; + timeoutMs?: number; +} interface CocodConfig { mnemonic: string; @@ -39,7 +65,118 @@ async function seedGetter(): Promise { return mnemonicToSeedSync(mnemonic); } +function defaultIsProcessRunning(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return (error as NodeJS.ErrnoException).code === "EPERM"; + } +} + +function hasErrorCode(error: unknown, codes: Set): boolean { + let current: unknown = error; + const visited = new Set(); + + while (current && typeof current === "object" && !visited.has(current)) { + visited.add(current); + const candidate = current as { code?: unknown; cause?: unknown }; + if (typeof candidate.code === "string" && codes.has(candidate.code)) { + return true; + } + current = candidate.cause; + } + + return false; +} + +/** + * Refuse to open coco.db while the legacy cocod daemon owns its Unix socket. + * Two independent wallet engines must never operate on the same proof database. + * + * A socket left behind after a crash is safe to ignore only when connecting + * fails with ENOENT or ECONNREFUSED. Other probe failures are treated as unsafe + * because they do not prove that cocod has stopped. + */ +export async function assertLegacyCocodNotRunning( + options: LegacyCocodGuardOptions = {}, +): Promise { + const socketPath = options.socketPath || LEGACY_COCOD_SOCKET; + const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + const pathExists = options.pathExists || existsSync; + const readFile = options.readFile || ((path) => readFileSync(path, "utf-8")); + const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning; + + const getRunningLegacyPid = (): number | null => { + if (!pathExists(pidFilePath)) return null; + + try { + const pid = Number.parseInt(readFile(pidFilePath).trim(), 10); + return Number.isInteger(pid) && pid > 0 && isProcessRunning(pid) + ? pid + : null; + } catch { + // An unreadable or malformed PID file does not prove that cocod is alive; + // the socket probe below remains the authoritative fallback. + return null; + } + }; + + const runningPid = getRunningLegacyPid(); + if (runningPid !== null) { + throw new Error( + `Legacy cocod daemon is still running with PID ${runningPid}. ` + + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + + "Run 'cocod stop' and try again.", + ); + } + + if (!pathExists(socketPath)) return; + + const fetchImpl = options.fetchImpl || (fetch as LegacyCocodFetch); + const timeoutMs = options.timeoutMs ?? 1_000; + + try { + const response = await fetchImpl("http://localhost/ping", { + unix: socketPath, + signal: AbortSignal.timeout(timeoutMs), + }); + await response.body?.cancel(); + } catch (error) { + if (hasErrorCode(error, STALE_SOCKET_ERROR_CODES)) { + // Recheck after the failed probe in case cocod started concurrently. + const newlyRunningPid = getRunningLegacyPid(); + if (newlyRunningPid === null) { + logger.debug(`Ignoring stale legacy cocod socket at ${socketPath}`); + return; + } + + throw new Error( + `Legacy cocod daemon is still running with PID ${newlyRunningPid}. ` + + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + + "Run 'cocod stop' and try again.", + { cause: error }, + ); + } + + throw new Error( + `Cannot verify whether the legacy cocod daemon has stopped at ${socketPath}. ` + + "Refusing to open the wallet database to prevent concurrent access. " + + "Run 'cocod stop', verify the daemon has exited, and try again.", + { cause: error }, + ); + } + + throw new Error( + `Legacy cocod daemon is still running at ${socketPath}. ` + + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + + "Run 'cocod stop' and try again.", + ); +} + export async function createCocoClient(): Promise { + await assertLegacyCocodNotRunning(); + const database = new Database(DB_PATH); const repo = new SqliteRepositories({ database }); await repo.init(); From 52e5d9b1d6aa6d4e7326103a2167e0f0ac10ca96 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:35:04 +0800 Subject: [PATCH 07/32] fix(wallet): retain legacy cocod PID lock --- src/daemon/http/index.ts | 10 +- src/daemon/index.ts | 28 ++++- src/daemon/wallet/coco-client.test.ts | 99 ++++++++++++++++- src/daemon/wallet/coco-client.ts | 151 ++++++++++++++++++++++++-- src/daemon/wallet/cocod-client.ts | 2 + 5 files changed, 276 insertions(+), 14 deletions(-) diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 96b4d59..83337a1 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -33,6 +33,7 @@ type WalletStatusOutput = { type DaemonDeps = { provider: string | null; server: { close(cb?: () => void): void }; + shutdown?: () => void; store: any; walletClient: CocodClient; walletAdapter: any; @@ -287,6 +288,7 @@ const sdkLogger: SdkLogger = makeSdkLogger(); export function createDaemonRequestHandler(deps: { provider: string | null; server: { close(cb?: () => void): void }; + shutdown?: () => void; store: any; walletClient: CocodClient; walletAdapter: any; @@ -627,9 +629,11 @@ export function createDaemonRequestHandler(deps: { if (req.method === "POST" && url.pathname === "/stop") { sendJson(res, 200, { output: "stopping" }); setTimeout(() => { - deps.server.close(() => { - process.exit(0); - }); + if (deps.shutdown) { + deps.shutdown(); + } else { + deps.server.close(() => process.exit(0)); + } }, 50); return; } diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 306805a..37d3515 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -139,11 +139,15 @@ async function main(): Promise { ); const server = createServer(); + let shutdownDaemon: () => void = () => { + server.close(() => process.exit(0)); + }; server.on( "request", createDaemonRequestHandler({ provider, server, + shutdown: () => shutdownDaemon(), store, walletClient, walletAdapter, @@ -272,12 +276,34 @@ async function main(): Promise { } }; + let walletDisposePromise: Promise | undefined; + const disposeWallet = (): Promise => { + walletDisposePromise ??= walletClient.dispose?.() || Promise.resolve(); + return walletDisposePromise; + }; + server.on("close", () => { stopModelRefreshJob(); stopRefundJob(); - + void disposeWallet().catch((error) => { + logger.error("Failed to dispose wallet:", error); + }); }); + shutdownDaemon = () => { + server.close(() => { + void disposeWallet().finally(() => process.exit(0)); + }); + }; + + const shutdownForSignal = (signal: NodeJS.Signals) => { + logger.log(`Received ${signal}; shutting down...`); + shutdownDaemon(); + }; + + process.once("SIGINT", shutdownForSignal); + process.once("SIGTERM", shutdownForSignal); + server.listen(port, async () => { logger.log(`Routstr daemon listening on http://localhost:${port}/v1`); if (requestResponseLogDir) { diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts index 2124e65..38f9143 100644 --- a/src/daemon/wallet/coco-client.test.ts +++ b/src/daemon/wallet/coco-client.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it, mock } from "bun:test"; -import { assertLegacyCocodNotRunning } from "./coco-client"; +import { + assertLegacyCocodNotRunning, + claimLegacyCocodPidFile, +} from "./coco-client"; type GuardOptions = NonNullable< Parameters[0] @@ -129,3 +132,97 @@ describe("assertLegacyCocodNotRunning", () => { ).rejects.toThrow("Cannot verify whether the legacy cocod daemon has stopped"); }); }); + +describe("claimLegacyCocodPidFile", () => { + it("claims the PID file exclusively and releases its own claim", () => { + let storedPid: string | undefined; + let removed = false; + const opened: Array<{ path: string; fd: number }> = []; + + const release = claimLegacyCocodPidFile({ + pidFilePath: PID_FILE_PATH, + pid: 4242, + openExclusive: (path) => { + opened.push({ path, fd: 7 }); + return 7; + }, + writePid: (fd, pid) => { + expect(fd).toBe(7); + storedPid = String(pid); + }, + closeFile: (fd) => expect(fd).toBe(7), + readFile: () => storedPid || "", + removeFile: () => { + removed = true; + }, + }); + + expect(opened).toEqual([{ path: PID_FILE_PATH, fd: 7 }]); + expect(storedPid).toBe("4242"); + release(); + expect(removed).toBe(true); + }); + + it("refuses startup when another process wins the atomic claim", () => { + expect(() => + claimLegacyCocodPidFile({ + pidFilePath: PID_FILE_PATH, + openExclusive: () => { + throw Object.assign(new Error("exists"), { code: "EEXIST" }); + }, + readFile: () => "4242", + isProcessRunning: () => true, + }), + ).toThrow("Another cocod or routstrd process may be starting"); + }); + + it("replaces a confirmed stale PID file before claiming it", () => { + let openAttempts = 0; + let removed = false; + let storedPid = "4242"; + + const release = claimLegacyCocodPidFile({ + pidFilePath: PID_FILE_PATH, + pid: 9001, + openExclusive: () => { + openAttempts++; + if (openAttempts === 1) { + throw Object.assign(new Error("exists"), { code: "EEXIST" }); + } + return 7; + }, + readFile: () => storedPid, + isProcessRunning: () => false, + removeFile: () => { + removed = true; + }, + writePid: (_fd, pid) => { + storedPid = String(pid); + }, + closeFile: () => {}, + }); + + expect(openAttempts).toBe(2); + expect(removed).toBe(true); + expect(storedPid).toBe("9001"); + release(); + }); + + it("does not remove a PID file that no longer belongs to this process", () => { + let removed = false; + const release = claimLegacyCocodPidFile({ + pidFilePath: PID_FILE_PATH, + pid: 4242, + openExclusive: () => 7, + writePid: () => {}, + closeFile: () => {}, + readFile: () => "9001", + removeFile: () => { + removed = true; + }, + }); + + release(); + expect(removed).toBe(false); + }); +}); diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 122e528..cac507f 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -1,7 +1,14 @@ import { initializeCoco, getEncodedToken } from "@cashu/coco-core"; import { SqliteRepositories } from "@cashu/coco-sqlite-bun"; import { Database } from "bun:sqlite"; -import { existsSync, readFileSync } from "fs"; +import { + closeSync, + existsSync, + openSync, + readFileSync, + unlinkSync, + writeFileSync, +} from "fs"; import { join } from "path"; import { mnemonicToSeedSync } from "@scure/bip39"; import type { CocodClient, CocodState } from "./cocod-client"; @@ -15,7 +22,8 @@ const CONFIG_FILE = join(CONFIG_DIR, "config.json"); const DB_PATH = join(CONFIG_DIR, "coco.db"); const LEGACY_COCOD_SOCKET = process.env.COCOD_SOCKET || join(CONFIG_DIR, "cocod.sock"); -const LEGACY_COCOD_PID_FILE = join(CONFIG_DIR, "cocod.pid"); +const LEGACY_COCOD_PID_FILE = + process.env.COCOD_PID || join(CONFIG_DIR, "cocod.pid"); const STALE_SOCKET_ERROR_CODES = new Set([ "ECONNREFUSED", @@ -40,6 +48,17 @@ export interface LegacyCocodGuardOptions { timeoutMs?: number; } +export interface LegacyCocodPidClaimOptions { + pidFilePath?: string; + pid?: number; + openExclusive?: (path: string) => number; + writePid?: (fd: number, pid: number) => void; + closeFile?: (fd: number) => void; + readFile?: (path: string) => string; + removeFile?: (path: string) => void; + isProcessRunning?: (pid: number) => boolean; +} + interface CocodConfig { mnemonic: string; encrypted: boolean; @@ -174,18 +193,118 @@ export async function assertLegacyCocodNotRunning( ); } +/** + * Atomically claim cocod's PID file for the lifetime of the in-process wallet. + * Legacy cocod checks this same file before opening coco.db, so a live routstrd + * owner prevents cocod from starting after the initial socket/PID probe. + */ +export function claimLegacyCocodPidFile( + options: LegacyCocodPidClaimOptions = {}, +): () => void { + const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + const pid = options.pid ?? process.pid; + const openExclusive = + options.openExclusive || ((path: string) => openSync(path, "wx", 0o600)); + const writePid = + options.writePid || ((fd: number, ownerPid: number) => writeFileSync(fd, String(ownerPid))); + const closeFile = options.closeFile || closeSync; + const readFile = options.readFile || ((path: string) => readFileSync(path, "utf-8")); + const removeFile = options.removeFile || unlinkSync; + const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning; + + let fd: number; + try { + fd = openExclusive(pidFilePath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; + + // The earlier guard permits a dead PID file. Remove only a parseable, + // confirmed-dead owner; an empty/malformed file may belong to a process + // that has created the file but has not written its PID yet. + let stalePid: number; + try { + stalePid = Number.parseInt(readFile(pidFilePath).trim(), 10); + } catch { + throw new Error( + `Cannot claim the wallet process lock at ${pidFilePath}. ` + + "Another cocod or routstrd process may be starting. Stop it and try again.", + { cause: error }, + ); + } + + if (!Number.isInteger(stalePid) || stalePid <= 0 || isProcessRunning(stalePid)) { + throw new Error( + `Cannot claim the wallet process lock at ${pidFilePath}. ` + + "Another cocod or routstrd process may be starting. Stop it and try again.", + { cause: error }, + ); + } + + try { + removeFile(pidFilePath); + fd = openExclusive(pidFilePath); + } catch (retryError) { + throw new Error( + `Cannot claim the wallet process lock at ${pidFilePath}. ` + + "Another cocod or routstrd process may be starting. Stop it and try again.", + { cause: retryError }, + ); + } + } + + try { + writePid(fd, pid); + } catch (error) { + try { + removeFile(pidFilePath); + } catch { + // Preserve the original write failure. + } + throw error; + } finally { + closeFile(fd); + } + + let released = false; + return () => { + if (released) return; + released = true; + + try { + if (readFile(pidFilePath).trim() === String(pid)) { + removeFile(pidFilePath); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + logger.warn(`Failed to release wallet process lock at ${pidFilePath}:`, error); + } + } + }; +} + export async function createCocoClient(): Promise { await assertLegacyCocodNotRunning(); + const releaseLegacyPidClaim = claimLegacyCocodPidFile(); - const database = new Database(DB_PATH); - const repo = new SqliteRepositories({ database }); - await repo.init(); + let database: Database | undefined; + let coco: Awaited> | undefined; - const coco = await initializeCoco({ - repo, - seedGetter, - }); + try { + database = new Database(DB_PATH); + const repo = new SqliteRepositories({ database }); + await repo.init(); + coco = await initializeCoco({ + repo, + seedGetter, + }); + } catch (error) { + database?.close(); + releaseLegacyPidClaim(); + throw error; + } + + let disposed = false; return { async ping(): Promise { try { @@ -295,5 +414,19 @@ export async function createCocoClient(): Promise { async getMintInfo(url: string): Promise { return coco.mint.getMintInfo(url); }, + + async dispose(): Promise { + if (disposed) return; + disposed = true; + try { + await coco.dispose(); + } finally { + try { + database.close(); + } finally { + releaseLegacyPidClaim(); + } + } + }, }; } diff --git a/src/daemon/wallet/cocod-client.ts b/src/daemon/wallet/cocod-client.ts index 1d85568..e4cc782 100644 --- a/src/daemon/wallet/cocod-client.ts +++ b/src/daemon/wallet/cocod-client.ts @@ -56,6 +56,8 @@ export interface CocodClient { listMints(): Promise; addMint(url: string): Promise; getMintInfo(url: string): Promise; + /** Release resources held by in-process wallet implementations. */ + dispose?(): Promise; } export function resolveCocodExecutable(cocodPath?: string | null): string { From 0b63ad31bfc5a72567c73c9c79655c1db84aea5b Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:57:54 +0800 Subject: [PATCH 08/32] fix(wallet): protect mnemonic file permissions --- src/cli.test.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ src/cli.ts | 23 +++++++++++++++------ 2 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 src/cli.test.ts diff --git a/src/cli.test.ts b/src/cli.test.ts new file mode 100644 index 0000000..a617dfb --- /dev/null +++ b/src/cli.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { initializeWallet } from "./cli"; + +const tempDirs: string[] = []; + +function makeTempDir(): string { + const dir = mkdtempSync(join(tmpdir(), "routstrd-wallet-test-")); + tempDirs.push(dir); + return dir; +} + +function permissions(path: string): number { + return statSync(path).mode & 0o777; +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +describe("initializeWallet", () => { + test("creates the wallet directory and config with restrictive permissions", () => { + const walletDir = join(makeTempDir(), ".cocod"); + + initializeWallet(walletDir); + + const walletConfig = join(walletDir, "config.json"); + expect(permissions(walletDir)).toBe(0o700); + expect(permissions(walletConfig)).toBe(0o600); + + const config = JSON.parse(readFileSync(walletConfig, "utf8")); + expect(config.encrypted).toBe(false); + expect(typeof config.mnemonic).toBe("string"); + expect(config.mnemonic.length).toBeGreaterThan(0); + }); + + test("repairs permissions without replacing an existing wallet", () => { + const walletDir = join(makeTempDir(), ".cocod"); + const walletConfig = join(walletDir, "config.json"); + const existingConfig = JSON.stringify({ mnemonic: "existing seed" }); + + mkdirSync(walletDir, { mode: 0o755 }); + writeFileSync(walletConfig, existingConfig, { mode: 0o644 }); + + initializeWallet(walletDir); + + expect(readFileSync(walletConfig, "utf8")).toBe(existingConfig); + expect(permissions(walletDir)).toBe(0o700); + expect(permissions(walletConfig)).toBe(0o600); + }); +}); diff --git a/src/cli.ts b/src/cli.ts index eb47b59..26e7411 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,7 @@ import { deleteClientAction, addClientAction, } from "./utils/clients"; -import { existsSync, mkdirSync, writeFileSync } from "fs"; +import { chmodSync, existsSync, mkdirSync, writeFileSync } from "fs"; import { execSync } from "child_process"; import { CONFIG_DIR, @@ -79,16 +79,24 @@ async function printLightningInvoice(invoice: string): Promise { console.log(`${qr}\nInvoice:\n${invoice}`); } -function initializeWallet(): void { - const walletDir = +export function initializeWallet( + walletDir = process.env.COCOD_DIR || - `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`; + `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`, +): void { const walletConfig = `${walletDir}/config.json`; + + // The wallet directory and config contain the plaintext seed phrase. Correct + // permissions on existing installations as well as newly created ones. + mkdirSync(walletDir, { recursive: true, mode: 0o700 }); + chmodSync(walletDir, 0o700); + if (existsSync(walletConfig)) { + chmodSync(walletConfig, 0o600); console.log("Wallet already initialized."); return; } - mkdirSync(walletDir, { recursive: true }); + const mnemonic = generateMnemonic(wordlist); const config = { version: 1, @@ -96,7 +104,10 @@ function initializeWallet(): void { encrypted: false, createdAt: new Date().toISOString(), }; - writeFileSync(walletConfig, JSON.stringify(config, null, 2)); + writeFileSync(walletConfig, JSON.stringify(config, null, 2), { + mode: 0o600, + flag: "wx", + }); console.log("Initialized. Mnemonic:", mnemonic); console.log("IMPORTANT: Write down this mnemonic and keep it safe!"); } From 2d7c7e9b24bc4364afc5e48ad91dbadd290d7372 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:17:14 +0800 Subject: [PATCH 09/32] test(wallet): verify legacy cocod database migration --- src/daemon/wallet/coco-client.test.ts | 63 +++++++++++++++++- src/daemon/wallet/coco-client.ts | 43 ++++++++---- .../wallet/fixtures/cocod-0.0.24-wallet.db.gz | Bin 0 -> 6021 bytes 3 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts index 38f9143..49d2667 100644 --- a/src/daemon/wallet/coco-client.test.ts +++ b/src/daemon/wallet/coco-client.test.ts @@ -1,7 +1,12 @@ -import { describe, expect, it, mock } from "bun:test"; +import { afterEach, describe, expect, it, mock } from "bun:test"; +import { gunzipSync } from "bun"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; import { assertLegacyCocodNotRunning, claimLegacyCocodPidFile, + createCocoClient, } from "./coco-client"; type GuardOptions = NonNullable< @@ -11,11 +16,67 @@ type LegacyFetch = NonNullable; const SOCKET_PATH = "/tmp/routstrd-test/cocod.sock"; const PID_FILE_PATH = "/tmp/routstrd-test/cocod.pid"; +const tempDirs: string[] = []; + +function makeTempDir(): string { + const dir = mkdtempSync(join(tmpdir(), "routstrd-coco-migration-test-")); + tempDirs.push(dir); + return dir; +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); function socketOnly(path: string): boolean { return path === SOCKET_PATH; } +describe("legacy cocod wallet migration", () => { + it("opens an existing unencrypted config and preserves database balances", async () => { + const walletDir = join(makeTempDir(), ".cocod"); + const mintUrl = "https://mint.example.com"; + mkdirSync(walletDir, { recursive: true }); + writeFileSync( + join(walletDir, "config.json"), + JSON.stringify({ + version: 1, + mnemonic: + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + encrypted: false, + }), + ); + + // This fixture was generated with @routstr/cocod 0.0.24 using its + // coco-cashu-sqlite-bun 1.1.2-rc.50 adapter. Keeping it frozen prevents + // this test from accidentally creating its "legacy" database with the + // same current adapter that createCocoClient uses to read it. + const fixture = readFileSync( + join(import.meta.dir, "fixtures", "cocod-0.0.24-wallet.db.gz"), + ); + writeFileSync(join(walletDir, "coco.db"), gunzipSync(fixture)); + + const client = await createCocoClient({ configDir: walletDir }); + try { + expect(await client.getStatus()).toBe("UNLOCKED"); + expect(await client.getBalances()).toEqual({ [mintUrl]: 10 }); + } finally { + await client.dispose?.(); + } + + // Prove the migrated schema remains reopenable and the pre-existing + // proofs survive a complete in-process wallet restart. + const reopenedClient = await createCocoClient({ configDir: walletDir }); + try { + expect(await reopenedClient.getBalances()).toEqual({ [mintUrl]: 10 }); + } finally { + await reopenedClient.dispose?.(); + } + }); +}); + describe("assertLegacyCocodNotRunning", () => { it("does not probe when the legacy socket and PID file do not exist", async () => { const fetchImpl = mock(async () => new Response("pong")); diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index cac507f..d16f130 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -64,13 +64,13 @@ interface CocodConfig { encrypted: boolean; } -function loadMnemonic(): string { - if (!existsSync(CONFIG_FILE)) { +function loadMnemonic(configFile: string = CONFIG_FILE): string { + if (!existsSync(configFile)) { throw new Error( - `Config file not found at ${CONFIG_FILE}. Run 'routstrd onboard' first.`, + `Config file not found at ${configFile}. Run 'routstrd onboard' first.`, ); } - const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as CocodConfig; + const config = JSON.parse(readFileSync(configFile, "utf-8")) as CocodConfig; if (config.encrypted) { throw new Error( "Encrypted wallets are not supported yet. Please use an unencrypted wallet.", @@ -79,11 +79,6 @@ function loadMnemonic(): string { return config.mnemonic; } -async function seedGetter(): Promise { - const mnemonic = loadMnemonic(); - return mnemonicToSeedSync(mnemonic); -} - function defaultIsProcessRunning(pid: number): boolean { try { process.kill(pid, 0); @@ -282,21 +277,41 @@ export function claimLegacyCocodPidFile( }; } -export async function createCocoClient(): Promise { - await assertLegacyCocodNotRunning(); - const releaseLegacyPidClaim = claimLegacyCocodPidFile(); +export interface CreateCocoClientOptions { + /** Override the wallet directory, primarily for migration tests and tooling. */ + configDir?: string; + socketPath?: string; + pidFilePath?: string; +} + +export async function createCocoClient( + options: CreateCocoClientOptions = {}, +): Promise { + const configDir = options.configDir || CONFIG_DIR; + const configFile = join(configDir, "config.json"); + const dbPath = join(configDir, "coco.db"); + const socketPath = options.socketPath || + (options.configDir ? join(configDir, "cocod.sock") : LEGACY_COCOD_SOCKET); + const pidFilePath = options.pidFilePath || + (options.configDir ? join(configDir, "cocod.pid") : LEGACY_COCOD_PID_FILE); + + await assertLegacyCocodNotRunning({ socketPath, pidFilePath }); + const releaseLegacyPidClaim = claimLegacyCocodPidFile({ pidFilePath }); let database: Database | undefined; let coco: Awaited> | undefined; try { - database = new Database(DB_PATH); + // Read and validate the existing cocod config during startup rather than + // deferring failure until coco-core first needs wallet key material. + const seed = mnemonicToSeedSync(loadMnemonic(configFile)); + database = new Database(dbPath); const repo = new SqliteRepositories({ database }); await repo.init(); coco = await initializeCoco({ repo, - seedGetter, + seedGetter: async () => seed, }); } catch (error) { database?.close(); diff --git a/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz b/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz new file mode 100644 index 0000000000000000000000000000000000000000..a7fc1aa68cd2ff8d38a4e51ee22493c39d5a5566 GIT binary patch literal 6021 zcmcIoXH-*bvqnV`P!Xkyh|+r%0U<^K>Am+zZ=pBoD1!9fk)ZU1CWIm-AVrYgJ0Wxk zkRS%6lh1R&^PP3qUH9MJKlYk6^X{41?|x^VXEsCR_3H!^T6UMOfGw>}&AoZdoy?sq z{=V~Cm=SCtL0~%B03zoyc9*@*Wrdn#;dfuCZfII5a4(9wil0`LhoLpTGG9eQ8+b&tX78w7W1Zg=bLPExTGq z=3PgntnU7ctZU_LWLlm#277#oIZ+(g-{0GR!^`S!!^sgW@2;t#`o6SrT5NRZwQFX4 zq>|iJbRU60b39Es>8>^Zcw2kSliR*nYP@w=Q>X2TtCNGe= znu#Q7wOZ&XP&3E{CXbDQMW&((1Fta~R=co}Dwb2F=nxijR=s=&bnk2X|fDMzDV`r_7WyUA|`&)8MZ2zDz5KVl#IpDinGIAIa_q8)6=FF zs!p%bk7`%sbFChKge2mC0rda@@+Sp0#BV#a_|O(M`yQ4HSOmhNGwmK3oo37pfH^Gn z33;_zlX~Nw=4zUcE-ngcQi50KbynIlYx*i+d8*Ex$`$h7K~C6E#3^2C;YG)F zYL6&UAIX0F!RuqCgq0q+(s2YR5Dbf`blAhdwD#TQxx^^P=}ujpJ>{5 zK)~BuR0dyW)kAQ6`hs>{ITSsd!!zdG&C-gGX*C$}tslbF(&AhvrK%sd{)EGOeYvoo z>_WV6MP0k8#x-RPzV^!&I_K@tUPQe4>mC%!3|{6Ymi>@F_wcyfHe&0+Q!i#?)(k6_ zy5XlnokMQ#nVm%PvWx*)l*63lo9{bFOXZu#k5zlZOvs=xyk5<{kK$>#T;e7{ zI#d1`0P~vISoo^?S&8}nbA#BynMzjdze)+^l<}2kVT$;?T8Gs$xukmHO zX2W9Ep|sWcEh-~+&>+iTw_0~?{yQ@HQOdwdn%7S(6}-R5JC^zFbdL&NiRexroI_#v z9$`#X1}~Czfhl?pUMk`g(piNBDTUSU9@MQmw(^n zM+1=y%HJl~bf5kab(?y+$xTa_*movmOo=&-rG(d{({H_{2nn{|=8VV}#G z&JOfSFTnJT?e=Rr7g$D&h+9S!sye-qr`C z_7r_GZ@Tp=y1cVsy=ow-=pMUlM?=#CILVvkMw&G+U@cmrY?=tZ z!C<~g2R&g(+@MB$s5a(B?zG#0AuZZYQK%cK{ERjhsGPVae)UNVK=5{UK_{4zt264M2QS3>?f#7f>&3_@d_W6v@e>H*ZAIjc@alK} zx9YgYH@<{SR;zQTZ%lD^hub$lFYQD|xq%+;==r_bLEfbiEM93~;Q#(D+v>jWr5(*3 z+m327{|A&_*QCqWEcy##-Lq6lID5_qC>z3ADG6NYnf{-6yPcrFGl zkq!K_7`SwZf4<=>P$nOE@w}gWb>fw-9$Tw=0D0t2aK&UO#MO(ONR#OsCHn4H=nhVm(xer%Fb%y0RgL5Ev`c4^XO}OsBMMzQ2|MgIz~fD$j;)VK&&>o6o~Yh5S-pbKB(P5pgoH-D3)S1igQ8smmH)iZ zS87wob=M4Q##5F{;}L=52VdoO$>ve`E9V}5Z_@Uk?Ra%ccHvg84wdYr(-Tp>K4Mxb!dOv|mJe=(8qb_qk^UF!&nA`TCiG;Py7aU@t79h9(xZ}x{lzgFQ#ej)4-*fIEFADz0AHqH=V(KQ zwbEYBNv!J|a~L)5Y7EC_tXZ8AhXhy(HdXH(d7p$#P^O<*ze%E}s{CM2cC)z8E)a@E zC+N6xmWUj!%x}c$$dw0a0PKyklzdMJ>A9<%>vBofK$RAw2`@QPwDW2^^#a0EYy|V9 z&qBn=Kk+|W`K*%7p!_3w>blfeKmTtO*P&7QVTdEIETToG6rHBK--@a7(of0Gw#Lr6 z#Nt%K20kr(B%dSx(GW>OLs`{kFJ0Sw;6IGvZVQdZ6QC_->6sM62?GJtpT3o-{t`*^ zk3t-G-0oR(?G>LuVk~7}|3-C#zfj#t%od`oo}>HF`Cc)4*x^oqszUShL}qQPxj~=L zkDJ*7E%#yqWLVe&=2uRNQl1)%-Wsdv?)m;$2pQX(r^JB(AA^ zH%C1%d~iA~=XP+qkUAB6Coua9zq^}S!6j5ILa<(3RKsdV*^|03yAo41y2c}T*uWR@gY-#lxCrp6Y=1%SM;8U`re?Fdco z>;WMLBR4M^T|eh;chH14r|1ukP8!c&IdP_2_qSeL8LNYl(H8f!c)k;^u_!A?GeS9F zd5(>&M0w-GEZ&_;AGP5iN5#s?xF*}HP=(wVvARwX)rML7d5#E=x#_2Cse_XH1!;r) zIk1T7;!GiV*1WRh?XW(_Tm4!`eZ$`9M9!|h6i97`2)j4(%PB`D9l`i`Xaj+QVxehI z4zFio161EMcD*5D(<)b7tYFi^5G)*JZ~AR-W$`Y)wAQw$Y?jeggmYw0749e8Ia`{s z^m$Cg-IGOGcT{TAvwB9iuWU2gXB@kctg*AFV9hPlUv7v#dt)1zvrk{L`vfG2954#3 z{;Ym`w~dvYm(-T2p(>7#M?5&htZs%8!!_{vWSZMegw_>^nZ)ieTQSaRi$B{Abi&kT zCshFt1oZ%N=gLQ0)rt|h{I()R8m>bWE@CB~u zZuBEwRjX*BYxZ22dfArIpGgQ;mSFO1;BNXFBX&Ly*piBy&T2fHEn9~5H0h^gbic*3 zaMhPE87JNwMbG)Za{DY8wdd>X-w?|VUYnV*F3nF_g}d`h`Q0LZ6iLMOs=LmAFwTx^ znSYt9fBGq=Lt-+zXpPYz9@!U`#inPT?@{YFdc(%iEX}ucE{tv4?bqfCrN^oeP`$*u z&zsi9zXq35H7^soMb*teR|Eh@U@K2 zT$^>9Og=7GR;!cS?|9bSJ>}z7j!??H36;5awj7GW)E565mjVE;Xo_umjEQtzBzfvm ztSqmENMg}dk3Ppuzqi>CtD0W>|hjCL|J0#pP6Cf8w1Ay zLsOPJ4;F$8#Ke&TJ$u+?@w-WGPLDuSMqDX2juRyLec;__O1ImDsFKXCRYbj*Lgjc> zTs1bT(l&WC%sq1#>E`7b3>+J2VTZd|SA_zXa1seq&R{R`=}+5M<3mBx)ac>o6dy!J z{Y7GbA-AOtZr{hB{uhFq|6nY^X5a-Fe?4NjkUpPXa36wy2+=(5eKY>T{x2Hz-v$4t zpP&JTv$KmhK}!QWCwST;xA-%o=fxKh@6YT3FU0?iK1h)S{t5nX`p^k--Rhg1{Q6!1 zTPyZNo9@7wTi~cV9z}bGYX`ApNHsI~GVwZRLQ!8(Mkqx6*Pn+HxEAyJ*YFp1|IF`+ z76{nmaL$bcM*9{HQboFu6RQ`Kz0H*dx8E+kgQ~Z%j)nBKMk|~8=f37?u3#ylsHsFs zFPT$To4!mMUstvCs7`JCJ^yP|39i$RxvR6CGLWa3@Nk!2*VNIdU{=a!(H|bse}rUq zHBRt~N?i^bHK;$eGEDdux~r@qf(p|JZtAx1v4%q5t!fW!K5jS%|3ibmqj&*nZOSq2 zU_j0`=iWyHL+J0`(Nw_Us+RPdC@f>c=`C~iJhN0-j(KwGViST3@*emI+0;1BF}^RG z4=qC5i=f0ag?$J4C?o)3kKg*J1$GO)0TcnTLK`pkr*yT3DYT0fcRi?N*)}#sV8b?p zOJkX47@ai$)X=F}DXS~+<5iCe2T2&`P?%5V>$}zBW^e_jMh?M89;Hf&2>FCrF#0fP zBt3i+mIF>Dc91jP9B&``+K|&IHE!_qCZ-r1*y4A2YqumTr1 z457ci-kgNMo*c6eJ)GB62gPVui@jAKN*0^^m^Se;P z;D>Mzj+Sj>OEW8I^*_Yxl$@l!fGwFAPkORhl_0{$Mq;IaZ0@LaGDST_`zamgY`&{bIKf#mE z`#l?Huw+|ni1}}LvKLU$M~)paye+b=5Ck!L^$WdpuLWx{(H?nj9!T!&_p67#9a_6H4;K8a(8)}>u`0t&Rq z#AL41)>De%dwL7`RcuhDcKL4q2qR|$s(+0n|5WauVm`ooV#^90xNgk9j;bn%9>oq% zkCn^?3401alTtWljD9=}Uf?9oEy~a^&we}Bek%a_$_rYPIhVW*FR#c%I)Tin#YI`; zsM#*n*KXkxxxYPAjTxw5Si9dNPjIrogUA0}zrD{O=cNDZMYEYdJMS7N$2-U89l(m= zBF#T0=ZjwS+A-=i?&P#KM2H*gK zD{Jl$)8CCj4%?!Wa)XBGkowlSbJWF*3NI#tkhz=L(-Wb4PWY%zt5`u7G1A8*n@YHRA9gHa$ zSYB{*B1w2_WGd(*&uVRo=~g!iY6f{=V0%&=WcwQ~a@x=Eplprd1A?NnRelk?lD{^i zyiK%Fz*RGmRPKIi#;3w=VO&Puv$3&SuTMYJl!sA?A~&UZcLiV*e!%aH^Fi_$!_AK~ z*?-`2wshhS5cD9s#I60WSV@yueP5Q*yg%9B z)gI&uGC^h21ZESmwcw`-@hT29-I;s<6lEvGcUE+utDh8}M&oKG8$ll|gpTQ5T zyFgPWLfN*Ox|5sls{vNbdf&@}zTu1}e79$i{KZz=cZx=R#0G~ZBJBFqo@S1C3N#-K zX^VSRJEH0RtBbJTYjg^1?R^ecQ~5yo6}HBjHBX|)>{T71M4=jThWF@&5qgiem~Rd5 zxilPcH*eu2O|hx)sh5SP$-Rp9THPi>eDHzk{WyrG-$xLeD(Te~-D)e2tZgSqp}x3d zQkm|00q@LNMLpBZ*$H6TLFc${*ehe)3#Vt5nW#7MeaTUHX;_T(u+`_aqDQypmZYNu zSeh{+lsBm<;k{EzYJ^=~$w|=-75$^dpEZ0d%8kc4ynPD&Y!9Fc*YgN8*yt?)}0X*xr6T^69e^_ZByR9p<~KVC|(< z5ZX#UXD_fSNf+~)pI6n5v-j{LeGJyv)bAYJg77zA@~&J)otMEY)>GoI?uq3D^H&J| E13yy00RR91 literal 0 HcmV?d00001 From aaafe91a4a2b438f3f52472492503601d05f2546 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:32:02 +0800 Subject: [PATCH 10/32] fix(wallet): let coco recover failed operations --- src/daemon/wallet/coco-client.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index d16f130..c0e63b9 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -387,13 +387,8 @@ export async function createCocoClient( mintUrl: targetMint, amount, }); - try { - const { token } = await coco.ops.send.execute(prepared.id); - return getEncodedToken(token); - } catch (error) { - await coco.ops.send.cancel(prepared.id); - throw error; - } + const { token } = await coco.ops.send.execute(prepared.id); + return getEncodedToken(token); }, async sendBolt11(invoice: string, mintUrl?: string): Promise { @@ -407,13 +402,8 @@ export async function createCocoClient( method: "bolt11", methodData: { invoice }, }); - try { - await coco.ops.melt.execute(prepared.id); - return "Payment sent successfully"; - } catch (error) { - await coco.ops.melt.cancel(prepared.id); - throw error; - } + await coco.ops.melt.execute(prepared.id); + return "Payment sent successfully"; }, async listMints(): Promise { From 4694804a403d57a9dcb49e50975d89cb32bb14e6 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Fri, 12 Jun 2026 08:24:05 +0800 Subject: [PATCH 11/32] feat: add history command and /wallet/history endpoint - Add 'history' CLI command with --limit, --offset, --verbose, --json options - Add GET /wallet/history daemon endpoint - Implement getHistory() in coco-client using coco.history.getPaginatedHistory() - Add getHistory stub (returns []) to cocod-client interface --- src/cli.ts | 102 ++++++++++++++++++++++++++++++ src/daemon/http/index.ts | 27 ++++++++ src/daemon/wallet/coco-client.ts | 5 ++ src/daemon/wallet/cocod-client.ts | 5 ++ 4 files changed, 139 insertions(+) diff --git a/src/cli.ts b/src/cli.ts index 26e7411..615acb9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1113,6 +1113,108 @@ npubsCmd ); }); +// History - show transaction history +program + .command("history") + .description("Show wallet transaction history") + .option("-n, --limit ", "Number of entries to show", "50") + .option("--offset ", "Number of entries to skip", "0") + .option("-v, --verbose", "Show full details including encoded Cashu tokens") + .option("--json", "Output raw JSON with token objects (no encoding)") + .action(async (options: { limit: string; offset: string; verbose: boolean; json: boolean }) => { + await ensureDaemonRunning(); + + const limit = Math.min(parseInt(options.limit, 10) || 50, 1000); + const offset = parseInt(options.offset, 10) || 0; + + const result = await callDaemon( + `/wallet/history?offset=${offset}&limit=${limit}`, + ); + + if (result.error) { + console.log(result.error); + process.exit(1); + } + + const data = result.output as + | { entries?: Record[]; offset?: number; limit?: number } + | undefined; + const entries = data?.entries || []; + + if (entries.length === 0) { + console.log("No transaction history yet."); + return; + } + + if (options.json) { + // Raw JSON output with token as JSON object + const jsonOutput = entries.map((e: Record) => { + const entry = { ...e }; + delete entry.encodedToken; + return entry; + }); + console.log(JSON.stringify(jsonOutput, null, 2)); + return; + } + + if (options.verbose) { + // Verbose: JSON-like with encoded token + for (const entry of entries) { + const e = entry as Record; + const display: Record = {}; + for (const key of Object.keys(e).sort()) { + display[key] = e[key]; + } + // Replace raw token with encoded one if present + if (e.encodedToken && e.token) { + display.token = e.encodedToken; + delete display.encodedToken; + } + console.log(JSON.stringify(display, null, 2)); + } + return; + } + + // Default: table format + const idCol = "ID"; + const timeCol = "Date/Time"; + const typeCol = "Type"; + const mintCol = "Mint"; + const amtCol = "Amount"; + + const rows = entries.map((entry: Record) => { + const id = String(entry.id ?? ""); + const time = new Date(Number(entry.createdAt)).toISOString().replace("T", " ").slice(0, 19); + const type = String(entry.type ?? "").toUpperCase(); + const mint = String(entry.mintUrl ?? ""); + const unit = String(entry.unit ?? "sat"); + const amount = `${entry.amount} ${unit}`; + return { id, time, type, mint, amount }; + }); + + const widths = { + id: Math.max(idCol.length, ...rows.map((r) => r.id.length)), + time: Math.max(timeCol.length, ...rows.map((r) => r.time.length)), + type: Math.max(typeCol.length, ...rows.map((r) => r.type.length)), + mint: Math.max(mintCol.length, ...rows.map((r) => r.mint.length)), + amount: Math.max(amtCol.length, ...rows.map((r) => r.amount.length)), + }; + + const pad = (s: string, w: number) => s.padEnd(w); + const sep = Object.values(widths).map((w) => "-".repeat(w)).join(" | "); + + console.log( + `${pad(idCol, widths.id)} | ${pad(timeCol, widths.time)} | ${pad(typeCol, widths.type)} | ${pad(mintCol, widths.mint)} | ${pad(amtCol, widths.amount)}`, + ); + console.log(sep); + + for (const row of rows) { + console.log( + `${pad(row.id, widths.id)} | ${pad(row.time, widths.time)} | ${pad(row.type, widths.type)} | ${pad(row.mint, widths.mint)} | ${pad(row.amount, widths.amount)}`, + ); + } + }); + // Monitor - interactive TUI program .command("monitor") diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 83337a1..46dc119 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -8,6 +8,8 @@ import { } from "@routstr/sdk"; import type { UsageTrackingDriver, SdkLogger } from "@routstr/sdk"; import type { RequestResponseLogSink } from "../request-response-log-sink"; +import { getEncodedToken } from "@cashu/coco-core"; +import type { HistoryEntry } from "@cashu/coco-core"; import { logger } from "../../utils/logger"; import { loadDaemonConfig, saveDaemonConfig } from "../config-store"; import { @@ -437,6 +439,31 @@ export function createDaemonRequestHandler(deps: { return; } + if (req.method === "GET" && url.pathname === "/wallet/history") { + await respond(res, async () => { + const offsetParam = url.searchParams.get("offset"); + const limitParam = url.searchParams.get("limit"); + const offset = offsetParam ? parseInt(offsetParam, 10) || 0 : 0; + const limit = limitParam ? parseInt(limitParam, 10) || 50 : 50; + const entries = await deps.walletClient.getHistory(offset, limit); + + // Encode tokens for send/receive entries + const encoded = entries.map((entry: HistoryEntry) => { + const base = { ...entry } as Record; + if ( + (entry.type === "send" || entry.type === "receive") && + entry.token + ) { + base.encodedToken = getEncodedToken(entry.token); + } + return base; + }); + + return { output: { entries: encoded, offset, limit } }; + }); + return; + } + // ── NWC endpoints ───────────────────────────────────────────── if (req.method === "GET" && url.pathname === "/nwc/status") { diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index c0e63b9..f4c10c5 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -1,4 +1,5 @@ import { initializeCoco, getEncodedToken } from "@cashu/coco-core"; +import type { HistoryEntry } from "@cashu/coco-core"; import { SqliteRepositories } from "@cashu/coco-sqlite-bun"; import { Database } from "bun:sqlite"; import { @@ -433,5 +434,9 @@ export async function createCocoClient( } } }, + + async getHistory(offset?: number, limit?: number): Promise { + return coco.history.getPaginatedHistory(offset, limit); + }, }; } diff --git a/src/daemon/wallet/cocod-client.ts b/src/daemon/wallet/cocod-client.ts index e4cc782..5256d99 100644 --- a/src/daemon/wallet/cocod-client.ts +++ b/src/daemon/wallet/cocod-client.ts @@ -2,6 +2,7 @@ import { existsSync } from "fs"; import { createHash } from "crypto"; import { logger } from "../../utils/logger"; import { withCrossProcessLock } from "../../utils/process-lock"; +import type { HistoryEntry } from "@cashu/coco-core"; const DEFAULT_CONFIG_DIR = process.env.COCOD_DIR || `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`; @@ -58,6 +59,7 @@ export interface CocodClient { getMintInfo(url: string): Promise; /** Release resources held by in-process wallet implementations. */ dispose?(): Promise; + getHistory(offset?: number, limit?: number): Promise; } export function resolveCocodExecutable(cocodPath?: string | null): string { @@ -355,5 +357,8 @@ export function createCocodClient( async getMintInfo(url: string): Promise { return post("/mints/info", { url }); }, + async getHistory(_offset?: number, _limit?: number): Promise { + return []; + }, }; } From a583aaa4825194d6df825e5daf60d2f835c3e0d3 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:31:55 +0100 Subject: [PATCH 12/32] fix logging --- src/utils/logger.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index cc317e0..2a4311c 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,5 +1,4 @@ -import { appendFile, mkdir } from "fs/promises"; -import { existsSync } from "fs"; +import { appendFileSync, existsSync, mkdirSync } from "fs"; import { join } from "path"; const HOME = process.env.HOME || process.env.USERPROFILE || ""; @@ -13,14 +12,17 @@ function getLogFileForDate(date: Date = new Date()): string { return join(LOGS_DIR, `${year}-${month}-${day}.log`); } -async function ensureLogDir() { +function ensureLogDir() { if (!existsSync(LOGS_DIR)) { - await mkdir(LOGS_DIR, { recursive: true }); + mkdirSync(LOGS_DIR, { recursive: true }); } } -async function writeLog(level: string, ...args: unknown[]) { - await ensureLogDir(); +// NOTE: writes are synchronous on purpose — the daemon calls process.exit() +// right after logger.error(...) on fatal paths, and async writes were being +// silently dropped, making startup failures invisible. +function writeLog(level: string, ...args: unknown[]) { + ensureLogDir(); const timestamp = new Date().toISOString(); const message = args .map((a) => { @@ -40,7 +42,7 @@ async function writeLog(level: string, ...args: unknown[]) { const line = `[${timestamp}] [${level}] ${message}\n`; const logFile = getLogFileForDate(new Date(timestamp)); try { - await appendFile(logFile, line); + appendFileSync(logFile, line); } catch (error) { console.error("Failed to write log:", error); } From e46c3fa37f5e97c086ef48a2a5fd881edfdd4002 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:25:56 +0100 Subject: [PATCH 13/32] fix: surface real daemon error on startup failure and hint kill PID The CLI spawned the daemon detached with stdout/stderr redirected to debug.log, so when the daemon exited early the user only saw a useless 'Check logs in ~/.routstrd/logs' message while the actual error sat silently in the log file. - daemon/index.ts: print fatal startup error to stderr (captured by the spawn redirect) in addition to the dated logger. - start-daemon.ts: record debug.log offset before spawning; on early exit read the newly appended output and include it in the thrown error. - coco-client.ts: add 'kill ' alternative to the legacy-cocod guard error message alongside 'cocod stop'. --- src/daemon/index.ts | 6 ++++++ src/daemon/wallet/coco-client.ts | 4 ++-- src/start-daemon.ts | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 37d3515..713f204 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -335,6 +335,12 @@ async function main(): Promise { if (import.meta.main) { main().catch((error) => { logger.error("Failed to start Routstr daemon:", error); + // Also write to stderr so the spawning CLI can surface the real error + // (stdout/stderr are redirected to debug.log by start-daemon.ts). + console.error( + "Failed to start Routstr daemon:", + error instanceof Error ? error.message : error, + ); process.exit(1); }); } diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index f4c10c5..303c00a 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -142,7 +142,7 @@ export async function assertLegacyCocodNotRunning( throw new Error( `Legacy cocod daemon is still running with PID ${runningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + - "Run 'cocod stop' and try again.", + `Run 'cocod stop' or 'kill ${runningPid}' and try again.`, ); } @@ -169,7 +169,7 @@ export async function assertLegacyCocodNotRunning( throw new Error( `Legacy cocod daemon is still running with PID ${newlyRunningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + - "Run 'cocod stop' and try again.", + `Run 'cocod stop' or 'kill ${newlyRunningPid}' and try again.`, { cause: error }, ); } diff --git a/src/start-daemon.ts b/src/start-daemon.ts index f807886..b20d150 100644 --- a/src/start-daemon.ts +++ b/src/start-daemon.ts @@ -1,4 +1,4 @@ -import { openSync } from "fs"; +import { openSync, existsSync, readFileSync } from "fs"; import { logger } from "./utils/logger"; import { CONFIG_DIR, LOGS_DIR } from "./utils/config"; import { withCrossProcessLock } from "./utils/process-lock"; @@ -6,6 +6,27 @@ import { withCrossProcessLock } from "./utils/process-lock"; const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`; const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`; +/** + * The spawned daemon's stdout/stderr is redirected to DEBUG_LOG_PATH, so when + * it exits early we can surface its actual error from there instead of just + * telling the user to go read logs. + */ +function readDaemonOutput(offset: number): string { + try { + if (!existsSync(DEBUG_LOG_PATH)) return ""; + const content = readFileSync(DEBUG_LOG_PATH, "utf-8"); + return content + .slice(offset) + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .slice(-30) + .join("\n"); + } catch { + return ""; + } +} + async function isDaemonHealthy(port: string): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2000); @@ -44,6 +65,9 @@ async function startDaemonUnlocked( const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname; const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`; + const debugLogOffset = existsSync(DEBUG_LOG_PATH) + ? readFileSync(DEBUG_LOG_PATH, "utf-8").length + : 0; const debugLogFd = openSync(DEBUG_LOG_PATH, "a"); const proc = Bun.spawn(["sh", "-c", shellCmd], { @@ -65,8 +89,12 @@ async function startDaemonUnlocked( await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); if (exitCode !== null) { + const daemonOutput = readDaemonOutput(debugLogOffset); throw new Error( - `Daemon process exited early with code ${exitCode}. Check logs in ${LOGS_DIR}`, + `Daemon process exited early with code ${exitCode}.` + + (daemonOutput + ? `\n\nDaemon output:\n${daemonOutput}` + : ` Check logs in ${LOGS_DIR}`), ); } From eb2d217c243308e6ef380eed0b1a1f7dc15202b4 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:00:05 +0100 Subject: [PATCH 14/32] feat: auto-stop legacy cocod on daemon start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When upgrading from an older routstrd that ran cocod as a separate process, the new in-process coco wallet refused to start because cocod still held coco.db. Users had to manually 'cocod stop' (which ironically could respawn the daemon) or kill the PID. - coco-client.ts: add stopLegacyCocod() — reads cocod's PID file, sends SIGTERM, polls until the process exits and the PID file is removed. Refuses (no SIGKILL) on timeout to avoid corrupting coco.db mid-recovery. - cli.ts: call stopLegacyCocod() in the 'start' action before spawning the daemon, so the upgrade path is automatic. assertLegacyCocodNotRunning stays as a safety net for any caller that bypasses the CLI. --- src/cli.ts | 4 +++ src/daemon/wallet/coco-client.ts | 61 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/cli.ts b/src/cli.ts index 615acb9..e501744 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -27,6 +27,7 @@ import { } from "./utils/config"; import { logger } from "./utils/logger"; import { setupIntegration, runIntegrationsForClients } from "./integrations"; +import { stopLegacyCocod } from "./daemon/wallet/coco-client"; import { getClientsList } from "./utils/clients"; import * as QRCode from "qrcode"; import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98"; @@ -456,6 +457,9 @@ program .action(async (options: { port?: string; provider?: string }) => { await requireLocalDaemon(); const config = await loadConfig(); + // Stop a legacy cocod daemon (from older routstrd versions) before + // starting the new in-process coco wallet — both cannot share coco.db. + await stopLegacyCocod(); await startDaemon({ port: options.port || String(config.port || 8008), provider: options.provider, diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 303c00a..9c9af6c 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -189,6 +189,67 @@ export async function assertLegacyCocodNotRunning( ); } +/** + * Gracefully stop a legacy cocod daemon that is still running, so the new + * in-process coco wallet can safely open the shared database. + * + * Sends SIGTERM to the PID recorded in cocod's PID file, then polls until the + * process exits and the PID file is removed (cocod cleans up both on graceful + * shutdown). On timeout it refuses rather than escalating to SIGKILL, because + * killing a wallet engine mid-proof-recovery risks corrupting coco.db — the + * exact failure the guard exists to prevent. + */ +export async function stopLegacyCocod( + options: { + pidFilePath?: string; + isProcessRunning?: (pid: number) => boolean; + /** Total time to wait for cocod to exit after SIGTERM. */ + timeoutMs?: number; + /** Interval between exit checks. */ + pollIntervalMs?: number; + } = {}, +): Promise { + const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning; + const timeoutMs = options.timeoutMs ?? 30_000; + const pollIntervalMs = options.pollIntervalMs ?? 500; + + const readPid = (): number | null => { + if (!existsSync(pidFilePath)) return null; + try { + const pid = Number.parseInt(readFileSync(pidFilePath, "utf-8").trim(), 10); + return Number.isInteger(pid) && pid > 0 && isProcessRunning(pid) ? pid : null; + } catch { + return null; + } + }; + + const pid = readPid(); + if (pid === null) { + logger.debug("stopLegacyCocod: no running legacy cocod found, nothing to stop."); + return; + } + + logger.log(`Stopping legacy cocod daemon (PID ${pid})…`); + process.kill(pid, "SIGTERM"); + + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + if (readPid() === null) { + logger.log(`Legacy cocod daemon (PID ${pid}) stopped.`); + return; + } + } + + throw new Error( + `Legacy cocod daemon (PID ${pid}) did not stop within ${Math.round( + timeoutMs / 1000, + )}s of SIGTERM. ` + + `Run 'kill ${pid}' and try again.`, + ); +} + /** * Atomically claim cocod's PID file for the lifetime of the in-process wallet. * Legacy cocod checks this same file before opening coco.db, so a live routstrd From 280bc06dc4dc8cf45e7b770a4c95ef782964c4b2 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:54:00 +0100 Subject: [PATCH 15/32] bumped sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbac95f..f7cce47 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@cashu/cashu-ts": "^4.3.0", "@cashu/coco-core": "^1.0.1", "@cashu/coco-sqlite-bun": "^1.0.1", - "@routstr/sdk": "^0.3.17", + "@routstr/sdk": "^0.3.18", "@scure/bip39": "^2.2.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", From 154f36d8d2875eaa317ef331b2257d39671f4c52 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:05:05 +0100 Subject: [PATCH 16/32] fix: call stopLegacyCocod in restart/update/service paths and wait for pidfile release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'restart' command failed when the old daemon still held the cocod pidfile during wallet disposal — assertLegacyCocodNotRunning detected the PID as 'cocod running' and refused to start. - Call stopLegacyCocod() in restart, update restart, and service install (previously only in 'start'). - After /stop, poll for both health-down AND pidfile-released before spawning the new daemon. The pidfile is released during wallet disposal, which happens after server.close(), so health check alone was insufficient. --- src/cli.ts | 126 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 8 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 76b3f5d..ec5da58 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -146,16 +146,25 @@ async function restartDaemonsAfterUpdate(): Promise { // (draining active connections) then exits. await callDaemon("/stop", { method: "POST" }); - for (let i = 0; i < 50; i++) { + // Wait for daemon to fully stop — both the HTTP health check to fail + // AND the legacy cocod pidfile to be released. + const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; + for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); - if (!(await isDaemonRunning())) break; + const healthDown = !(await isDaemonRunning()); + const pidFileReleased = !existsSync(pidFilePath); + if (healthDown && pidFileReleased) break; } if (await isDaemonRunning()) { - throw new Error("routstrd did not stop within 5 seconds"); + throw new Error("routstrd did not stop within 10 seconds"); } console.log("routstrd daemon stopped."); + // Stop a legacy cocod daemon (from older routstrd versions) before + // starting the new in-process coco wallet. + await stopLegacyCocod(); + console.log("Starting routstrd daemon..."); await startDaemon({ port: String(config.port || 8008), @@ -536,9 +545,97 @@ program program .command("balance") .description("Get wallet and API key balances") - .action(async () => { + .option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false) + .option( + "--delete-api-keys ", + "Delete the API key stored for the given provider base URL (refunds balance first)", + ) + .option( + "--mint-url ", + "Mint URL to refund the deleted API key balance to (defaults to first mint in wallet)", + ) + .action(async (options: { apiKeys: boolean; deleteApiKeys?: string; mintUrl?: string }) => { await ensureDaemonRunning(); + // --delete-api-keys : refund then remove the API key for a + // single provider. + if (options.deleteApiKeys) { + const baseUrl = options.deleteApiKeys; + const queryParts = [`baseUrl=${encodeURIComponent(baseUrl)}`]; + if (options.mintUrl) { + queryParts.push(`mintUrl=${encodeURIComponent(options.mintUrl)}`); + } + const result = await callDaemon( + `/keys/api/delete?${queryParts.join("&")}`, + { method: "DELETE" }, + ); + if (result.error) { + console.log(result.error); + process.exit(1); + } + const out = result.output as + | { + baseUrl?: string; + removed?: boolean; + refunded?: boolean; + refundedAmount?: number; + refundMessage?: string; + message?: string; + } + | undefined; + if (out) { + console.log(out.message ?? `Removed API key for ${baseUrl}`); + if (out.refunded && out.refundedAmount !== undefined) { + console.log(` Refunded: ${out.refundedAmount} sats`); + } else if (out.refundMessage) { + console.log(` Refund: ${out.refundMessage}`); + } + } + return; + } + + // --api-keys: list every stored API key with full details. + if (options.apiKeys) { + const result = await callDaemon("/keys/api"); + if (result.error) { + console.log(result.error); + process.exit(1); + } + + const data = result.output as + | { + apiKeys: Array<{ + baseUrl: string; + key: string; + balance: number; + lastUsed: number | null; + }>; + count: number; + total: number; + unit: string; + } + | undefined; + + console.log("=== API Keys ===\n"); + if (!data || data.apiKeys.length === 0) { + console.log(" No API keys stored."); + return; + } + for (const k of data.apiKeys) { + const lastUsed = k.lastUsed + ? new Date(k.lastUsed).toISOString() + : "never"; + console.log(` ${k.baseUrl}`); + console.log(` key: ${k.key}`); + console.log(` balance: ${k.balance} ${data.unit}`); + console.log(` lastUsed: ${lastUsed}`); + console.log(""); + } + console.log(` Total: ${data.apiKeys.length} key(s), ${data.total} ${data.unit}`); + return; + } + + // Default: show the full wallet + API key balance summary. const [walletResult, keysResult] = await Promise.all([ callDaemon("/balance"), callDaemon("/keys/balance"), @@ -1645,6 +1742,10 @@ serviceCmd console.log("Starting routstrd via PM2..."); try { + // Stop a legacy cocod daemon (from older routstrd versions) before + // starting the new in-process coco wallet. + await stopLegacyCocod(); + // Use --interpreter bun to ensure it runs with bun execSync(`pm2 start "${daemonPath}" --name routstrd --interpreter bun`, { stdio: "inherit", @@ -1702,16 +1803,21 @@ program console.log("Stopping daemon..."); await callDaemon("/stop", { method: "POST" }); - // Wait for daemon to fully stop - for (let i = 0; i < 50; i++) { + // Wait for daemon to fully stop — both the HTTP health check to fail + // AND the legacy cocod pidfile to be released (the daemon releases + // it during wallet disposal, which happens after server.close()). + const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; + for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); - if (!(await isDaemonRunning())) { + const healthDown = !(await isDaemonRunning()); + const pidFileReleased = !existsSync(pidFilePath); + if (healthDown && pidFileReleased) { break; } } if (await isDaemonRunning()) { - logger.error("Daemon failed to stop within 5 seconds"); + logger.error("Daemon failed to stop within 10 seconds"); process.exit(1); } console.log("Daemon stopped."); @@ -1719,6 +1825,10 @@ program console.log("Daemon was not running."); } + // Stop a legacy cocod daemon (from older routstrd versions) before + // starting the new in-process coco wallet — both cannot share coco.db. + await stopLegacyCocod(); + console.log("Starting daemon..."); await startDaemon({ port: options.port || String(config.port || 8008), From df226dfa901508aceaccf49446967a7cdc01e542 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:58:05 +0100 Subject: [PATCH 17/32] fix: bind to 127.0.0.1 by default instead of 0.0.0.0 The daemon was binding to all network interfaces (0.0.0.0) by default because server.listen(port) without a host argument listens on all interfaces in Node.js. This exposed unauthenticated endpoints (/balance, /status, /providers, /wallet/balance, etc.) to anyone who could reach the port. Changes: - Add 'host' field to RoutstrdConfig (default: 127.0.0.1) - Add --host CLI flag to 'start' and 'restart' commands - Add ROUTSTRD_HOST env var support in parseArgs - Pass host to server.listen(port, host, callback) - Add startup warning when bound to 0.0.0.0 - Update getDaemonBaseUrl to use config.host (with 0.0.0.0 -> localhost fallback for client connections since 0.0.0.0 is not connectable) - Update start-daemon.ts health checks to use the correct host - Pass host through all startDaemon() call sites in cli.ts - Document --host flag, config field, and security implications in README Closes ROUTSTRD-BIND-ADDRESS --- README.md | 12 ++++++++++++ src/cli.ts | 12 +++++++++--- src/daemon/args.ts | 10 +++++++++- src/daemon/index.ts | 20 +++++++++++++++++--- src/start-daemon.ts | 32 +++++++++++++++++++++++--------- src/utils/config.ts | 4 ++++ src/utils/daemon-client.ts | 14 +++++++++++--- 7 files changed, 85 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 094f30a..9433e01 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,16 @@ With custom port: routstrd start --port 9000 ``` +With a specific bind address (default is `127.0.0.1` for security): +```sh +routstrd start --host 0.0.0.0 +``` + +> ⚠️ **Security note:** By default, routstrd binds to `127.0.0.1` (localhost only). +> Several endpoints (e.g. `/balance`, `/status`, `/providers`) do not require +> authentication and will leak sensitive information if exposed. Only bind to +> `0.0.0.0` if you have a firewall or reverse proxy in place. + With specific provider: ```sh routstrd start --provider https://your-provider.com @@ -139,6 +149,7 @@ Configuration is stored in `~/.routstrd/config.json`: ```json { "port": 8008, + "host": "127.0.0.1", "provider": null, "cocodPath": null } @@ -149,6 +160,7 @@ Configuration is stored in `~/.routstrd/config.json`: - `ROUTSTRD_DIR` - Config directory (default: `~/.routstrd`) - `ROUTSTRD_SOCKET` - Socket path (default: `~/.routstrd/routstrd.sock`) - `ROUTSTRD_PID` - PID file path (default: `~/.routstrd/routstrd.pid`) +- `ROUTSTRD_HOST` - Bind address override (default: `127.0.0.1`) ## Development diff --git a/src/cli.ts b/src/cli.ts index ec5da58..a2f85d2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -168,6 +168,7 @@ async function restartDaemonsAfterUpdate(): Promise { console.log("Starting routstrd daemon..."); await startDaemon({ port: String(config.port || 8008), + host: config.host || undefined, provider: config.provider || undefined, }); console.log("routstrd daemon restarted."); @@ -238,7 +239,7 @@ async function initDaemon(): Promise { console.log(`Database will be stored at: ${DB_PATH}`); initializeWallet(); - await startDaemon({ port: String(config.port || 8008) }); + await startDaemon({ port: String(config.port || 8008), host: config.host || undefined }); await setupIntegration(config); @@ -497,8 +498,9 @@ program .command("start") .description("Start the background daemon") .option("--port ", "Port to listen on") + .option("--host ", "Bind address (default: 127.0.0.1)") .option("-p, --provider ", "Default provider to use") - .action(async (options: { port?: string; provider?: string }) => { + .action(async (options: { port?: string; host?: string; provider?: string }) => { await requireLocalDaemon(); const config = await loadConfig(); // Stop a legacy cocod daemon (from older routstrd versions) before @@ -506,6 +508,7 @@ program await stopLegacyCocod(); await startDaemon({ port: options.port || String(config.port || 8008), + host: options.host || config.host || undefined, provider: options.provider, }); }); @@ -1793,8 +1796,9 @@ program .command("restart") .description("Restart the background daemon") .option("--port ", "Port to listen on") + .option("--host ", "Bind address (default: 127.0.0.1)") .option("-p, --provider ", "Default provider to use") - .action(async (options: { port?: string; provider?: string }) => { + .action(async (options: { port?: string; host?: string; provider?: string }) => { await requireLocalDaemon(); const config = await loadConfig(); const wasRunning = await isDaemonRunning(); @@ -1832,6 +1836,7 @@ program console.log("Starting daemon..."); await startDaemon({ port: options.port || String(config.port || 8008), + host: options.host || config.host || undefined, provider: options.provider, }); console.log("Daemon restarted."); @@ -1914,6 +1919,7 @@ program console.log("Starting daemon..."); await startDaemon({ port: String(config.port || 8008), + host: config.host || undefined, provider: config.provider || undefined, }); console.log(`Daemon restarted with mode '${selectedMode}'.`); diff --git a/src/daemon/args.ts b/src/daemon/args.ts index 659f35d..77226f6 100644 --- a/src/daemon/args.ts +++ b/src/daemon/args.ts @@ -1,8 +1,10 @@ export function parseArgs(argv: string[]): { port: number; + host: string | null; provider: string | null; } { const portFlagIndex = argv.findIndex((arg) => arg === "--port"); + const hostFlagIndex = argv.findIndex((arg) => arg === "--host"); const providerFlagIndex = argv.findIndex( (arg) => arg === "--provider" || arg === "-p", ); @@ -11,9 +13,15 @@ export function parseArgs(argv: string[]): { portFlagIndex !== -1 ? Number.parseInt(argv[portFlagIndex + 1] || "8008", 10) : 8008; + + // --host flag takes precedence, then ROUTSTRD_HOST env var + const hostValue = + hostFlagIndex !== -1 ? argv[hostFlagIndex + 1] : undefined; + const host = hostValue ? hostValue.trim() : (process.env.ROUTSTRD_HOST || null); + const providerValue = providerFlagIndex !== -1 ? argv[providerFlagIndex + 1] : undefined; const provider = providerValue ? providerValue.trim() : null; - return { port, provider }; + return { port, host, provider }; } diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 713f204..02470bc 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -65,6 +65,7 @@ async function main(): Promise { const config = await loadDaemonConfig(); const port = args.port; + const host = args.host || config.host || "127.0.0.1"; const provider = args.provider || config.provider; const requestResponseLogDir = process.env.ROUTSTRD_REQUEST_RESPONSE_LOG_DIR || @@ -80,7 +81,7 @@ async function main(): Promise { await ensureDirs(); - const updatedConfig = { ...config, port, provider }; + const updatedConfig = { ...config, port, host, provider }; saveDaemonConfig(updatedConfig); const sqliteDriver = await createBunSqliteDriver(DB_PATH, { logger: daemonSdkLogger }); @@ -304,8 +305,21 @@ async function main(): Promise { process.once("SIGINT", shutdownForSignal); process.once("SIGTERM", shutdownForSignal); - server.listen(port, async () => { - logger.log(`Routstr daemon listening on http://localhost:${port}/v1`); + // Warn when binding to all interfaces — unauthenticated endpoints expose + // balance info, provider lists, and internal state to anyone who can reach + // the port. + if (host === "0.0.0.0") { + logger.warn( + "⚠️ WARNING: Daemon is bound to 0.0.0.0 (all network interfaces). " + + "Several endpoints (e.g. /balance, /status, /providers) do not require " + + "authentication and will leak sensitive information to anyone on the " + + "network. Consider binding to 127.0.0.1 unless you have a firewall or " + + "reverse proxy in place.", + ); + } + + server.listen(port, host, async () => { + logger.log(`Routstr daemon listening on http://${host}:${port}/v1`); if (requestResponseLogDir) { logger.log(`Raw request/response logs: ${requestResponseLogDir}`); } diff --git a/src/start-daemon.ts b/src/start-daemon.ts index b20d150..2bfcf46 100644 --- a/src/start-daemon.ts +++ b/src/start-daemon.ts @@ -27,11 +27,11 @@ function readDaemonOutput(offset: number): string { } } -async function isDaemonHealthy(port: string): Promise { +async function isDaemonHealthy(port: string, host: string = "localhost"): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2000); try { - const existing = await fetch(`http://localhost:${port}/health`, { + const existing = await fetch(`http://${host}:${port}/health`, { signal: controller.signal, }); return existing.ok; @@ -42,22 +42,34 @@ async function isDaemonHealthy(port: string): Promise { } } +/** When the daemon binds to 0.0.0.0, the CLI must still connect via + * localhost (or 127.0.0.1) since 0.0.0.0 is not a connectable address. */ +function clientHost(host: string | undefined): string { + if (!host || host === "0.0.0.0") return "localhost"; + return host; +} + async function startDaemonUnlocked( - options: { port?: string; provider?: string }, + options: { port?: string; host?: string; provider?: string }, ): Promise { const args: string[] = []; const port = options.port || "8008"; + const host = options.host || "127.0.0.1"; + const ch = clientHost(host); const pollIntervalMs = 250; const startupTimeoutMs = 10 * 60 * 1000; - if (await isDaemonHealthy(port)) { - console.log(`Routstr daemon already running on http://localhost:${port}/v1`); + if (await isDaemonHealthy(port, ch)) { + console.log(`Routstr daemon already running on http://${ch}:${port}/v1`); return; } if (options.port) { args.push("--port", options.port); } + if (options.host) { + args.push("--host", options.host); + } if (options.provider) { args.push("--provider", options.provider); } @@ -98,7 +110,7 @@ async function startDaemonUnlocked( ); } - if (await isDaemonHealthy(port)) { + if (await isDaemonHealthy(port, ch)) { console.log(`Routstr daemon started (PID: ${proc.pid}).`); return; } @@ -110,13 +122,15 @@ async function startDaemonUnlocked( } export async function startDaemon( - options: { port?: string; provider?: string } = {}, + options: { port?: string; host?: string; provider?: string } = {}, ): Promise { const port = options.port || "8008"; + const host = options.host || "127.0.0.1"; + const ch = clientHost(host); const startupTimeoutMs = 10 * 60 * 1000; - if (await isDaemonHealthy(port)) { - console.log(`Routstr daemon already running on http://localhost:${port}/v1`); + if (await isDaemonHealthy(port, ch)) { + console.log(`Routstr daemon already running on http://${ch}:${port}/v1`); return; } diff --git a/src/utils/config.ts b/src/utils/config.ts index 202b2ca..8e160bf 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -32,6 +32,9 @@ export interface NwcConfig { export interface RoutstrdConfig { port: number; + /** Bind address for the HTTP server. Defaults to 127.0.0.1 (localhost only) + * for security — set to 0.0.0.0 to listen on all interfaces. */ + host: string; provider: string | null; cocodPath: string | null; mode?: "xcashu" | "apikeys"; @@ -58,6 +61,7 @@ export interface RoutstrdConfig { export const DEFAULT_CONFIG: RoutstrdConfig = { port: 8008, + host: "127.0.0.1", provider: null, cocodPath: null, mode: "apikeys", diff --git a/src/utils/daemon-client.ts b/src/utils/daemon-client.ts index 1086072..f3177a3 100644 --- a/src/utils/daemon-client.ts +++ b/src/utils/daemon-client.ts @@ -30,9 +30,16 @@ export async function loadConfig(): Promise { } export function getDaemonBaseUrl(config: RoutstrdConfig): string { - return ( - config.daemonUrl?.replace(/\/$/, "") || `http://localhost:${config.port}` - ); + if (config.daemonUrl) { + return config.daemonUrl.replace(/\/$/, ""); + } + // When bound to 0.0.0.0, connect via localhost since 0.0.0.0 is not + // a connectable address from a client perspective. + const host = + !config.host || config.host === "0.0.0.0" + ? "localhost" + : config.host; + return `http://${host}:${config.port}`; } export function getAuthBaseUrl(config: RoutstrdConfig): string { @@ -145,6 +152,7 @@ export async function startDaemonProcess(): Promise { const config = await loadConfig(); await startDaemon({ port: String(config.port || 8008), + host: config.host || undefined, provider: config.provider || undefined, }); } From 72f9ba8a99777af06e12e5aeb448399fbd2eac75 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:05:13 +0100 Subject: [PATCH 18/32] refactor: trim host binding changes --- README.md | 8 ++------ src/daemon/args.ts | 3 +-- src/daemon/index.ts | 13 ------------- src/start-daemon.ts | 9 +++------ src/utils/config.ts | 2 -- src/utils/daemon-client.ts | 7 +------ 6 files changed, 7 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9433e01..4b85baf 100644 --- a/README.md +++ b/README.md @@ -74,15 +74,12 @@ With custom port: routstrd start --port 9000 ``` -With a specific bind address (default is `127.0.0.1` for security): +The daemon binds to `127.0.0.1` by default. To expose it on another interface: ```sh routstrd start --host 0.0.0.0 ``` -> ⚠️ **Security note:** By default, routstrd binds to `127.0.0.1` (localhost only). -> Several endpoints (e.g. `/balance`, `/status`, `/providers`) do not require -> authentication and will leak sensitive information if exposed. Only bind to -> `0.0.0.0` if you have a firewall or reverse proxy in place. +Only expose the daemon behind appropriate network controls. With specific provider: ```sh @@ -160,7 +157,6 @@ Configuration is stored in `~/.routstrd/config.json`: - `ROUTSTRD_DIR` - Config directory (default: `~/.routstrd`) - `ROUTSTRD_SOCKET` - Socket path (default: `~/.routstrd/routstrd.sock`) - `ROUTSTRD_PID` - PID file path (default: `~/.routstrd/routstrd.pid`) -- `ROUTSTRD_HOST` - Bind address override (default: `127.0.0.1`) ## Development diff --git a/src/daemon/args.ts b/src/daemon/args.ts index 77226f6..79edeb4 100644 --- a/src/daemon/args.ts +++ b/src/daemon/args.ts @@ -14,10 +14,9 @@ export function parseArgs(argv: string[]): { ? Number.parseInt(argv[portFlagIndex + 1] || "8008", 10) : 8008; - // --host flag takes precedence, then ROUTSTRD_HOST env var const hostValue = hostFlagIndex !== -1 ? argv[hostFlagIndex + 1] : undefined; - const host = hostValue ? hostValue.trim() : (process.env.ROUTSTRD_HOST || null); + const host = hostValue?.trim() || null; const providerValue = providerFlagIndex !== -1 ? argv[providerFlagIndex + 1] : undefined; diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 02470bc..52e1422 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -305,19 +305,6 @@ async function main(): Promise { process.once("SIGINT", shutdownForSignal); process.once("SIGTERM", shutdownForSignal); - // Warn when binding to all interfaces — unauthenticated endpoints expose - // balance info, provider lists, and internal state to anyone who can reach - // the port. - if (host === "0.0.0.0") { - logger.warn( - "⚠️ WARNING: Daemon is bound to 0.0.0.0 (all network interfaces). " + - "Several endpoints (e.g. /balance, /status, /providers) do not require " + - "authentication and will leak sensitive information to anyone on the " + - "network. Consider binding to 127.0.0.1 unless you have a firewall or " + - "reverse proxy in place.", - ); - } - server.listen(port, host, async () => { logger.log(`Routstr daemon listening on http://${host}:${port}/v1`); if (requestResponseLogDir) { diff --git a/src/start-daemon.ts b/src/start-daemon.ts index 2bfcf46..c3ac308 100644 --- a/src/start-daemon.ts +++ b/src/start-daemon.ts @@ -27,7 +27,7 @@ function readDaemonOutput(offset: number): string { } } -async function isDaemonHealthy(port: string, host: string = "localhost"): Promise { +async function isDaemonHealthy(port: string, host = "127.0.0.1"): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2000); try { @@ -42,11 +42,8 @@ async function isDaemonHealthy(port: string, host: string = "localhost"): Promis } } -/** When the daemon binds to 0.0.0.0, the CLI must still connect via - * localhost (or 127.0.0.1) since 0.0.0.0 is not a connectable address. */ -function clientHost(host: string | undefined): string { - if (!host || host === "0.0.0.0") return "localhost"; - return host; +function clientHost(host?: string): string { + return !host || host === "0.0.0.0" ? "127.0.0.1" : host; } async function startDaemonUnlocked( diff --git a/src/utils/config.ts b/src/utils/config.ts index 8e160bf..2ebe28f 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -32,8 +32,6 @@ export interface NwcConfig { export interface RoutstrdConfig { port: number; - /** Bind address for the HTTP server. Defaults to 127.0.0.1 (localhost only) - * for security — set to 0.0.0.0 to listen on all interfaces. */ host: string; provider: string | null; cocodPath: string | null; diff --git a/src/utils/daemon-client.ts b/src/utils/daemon-client.ts index f3177a3..ff27906 100644 --- a/src/utils/daemon-client.ts +++ b/src/utils/daemon-client.ts @@ -33,12 +33,7 @@ export function getDaemonBaseUrl(config: RoutstrdConfig): string { if (config.daemonUrl) { return config.daemonUrl.replace(/\/$/, ""); } - // When bound to 0.0.0.0, connect via localhost since 0.0.0.0 is not - // a connectable address from a client perspective. - const host = - !config.host || config.host === "0.0.0.0" - ? "localhost" - : config.host; + const host = config.host === "0.0.0.0" ? "127.0.0.1" : config.host; return `http://${host}:${config.port}`; } From af3bd832a5dd8d9fd3a53f8f5e65995e3538b782 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:17:28 +0100 Subject: [PATCH 19/32] feat: add API key list/delete endpoints and harden stopLegacyCocod socket check HTTP daemon: - GET /keys/api returns all stored provider API keys with baseUrl, key, balance, lastUsed, and aggregate total. - DELETE/POST /keys/api/delete removes a provider API key, refunding the remaining balance to a mint first (falls back to the first mint in the wallet). Key is removed manually if the refund fails. - Relaxed the post-auth method guard so /keys/api GET routes pass. coco-client: - stopLegacyCocod verifies the PID responds on the legacy cocod Unix socket before sending SIGTERM. routstrd writes its own PID to the shared cocod.pid while the in-process wallet is open, so the PID file alone is not a safe identifier. - Extracted injectable LegacyCocodStopOptions for testability. - Three stopLegacyCocod tests: live cocod, stale socket, no socket. build: - Point @routstr/sdk at the local checkout for coco-integration dev. --- bun.lock | 320 +++++++++++++++++++++++++- package.json | 2 +- src/cli.ts | 26 +-- src/daemon/http/index.ts | 135 ++++++++++- src/daemon/wallet/coco-client.test.ts | 70 ++++++ src/daemon/wallet/coco-client.ts | 70 +++++- 6 files changed, 579 insertions(+), 44 deletions(-) diff --git a/bun.lock b/bun.lock index 2572943..ab16ca5 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "@cashu/cashu-ts": "^4.3.0", "@cashu/coco-core": "^1.0.1", "@cashu/coco-sqlite-bun": "^1.0.1", - "@routstr/sdk": "^0.3.18", + "@routstr/sdk": "file://../routstr-sdk", "@scure/bip39": "^2.2.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", @@ -41,18 +41,78 @@ "@cashu/coco-sqlite-bun": ["@cashu/coco-sqlite-bun@1.0.1", "", { "peerDependencies": { "@cashu/coco-core": "1.0.1", "typescript": "^5" } }, "sha512-u6lnC3c8+qopD1CuLkcScbxcU1O+STrTz9+fwN09filD4PS+vYPCQNgygIfzKWnCaF8gXFRUuJsXSMqHDImJTw=="], - "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "@freedomofpress/crypto-browser": ["@freedomofpress/crypto-browser@0.1.7", "", { "dependencies": { "@noble/curves": "^1.6.0" } }, "sha512-zjWmZDKdAu8g0Zq1IjBQ+sKQ/NpfzStBDFjy/qHUSMVEL4wNlNGtA7lhtw8v8asXa0yqF2QTYQ3rq6xCTQeADw=="], "@freedomofpress/sigstore-browser": ["@freedomofpress/sigstore-browser@0.1.14", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/tuf-browser": "^0.1.11", "@noble/curves": "^2.0.1" } }, "sha512-1dqc7HojiBcr/sJSAXjBwNz4+WGeeAJP8ENQnDxm+idVV0/YIxpfwXRNSJdNw6EXJuHviq/5kSPEBCxamPcrpQ=="], "@freedomofpress/tuf-browser": ["@freedomofpress/tuf-browser@0.1.11", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7" } }, "sha512-d76ohB/AS5+zI+lnbiFMX/BIK3nT18hZ8q3Na6X4vyYaSYjXkeknzhAnbx1da+8ObWLwsaZLue46haEch28qtQ=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@libsql/client": ["@libsql/client@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="], "@libsql/core": ["@libsql/core@0.15.15", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA=="], @@ -93,9 +153,93 @@ "@noble/post-quantum": ["@noble/post-quantum@0.6.1", "", { "dependencies": { "@noble/ciphers": "~2.2.0", "@noble/curves": "~2.2.0", "@noble/hashes": "~2.2.0" } }, "sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A=="], + "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + "@panva/hpke-noble": ["@panva/hpke-noble@1.1.3", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-zPG7MR9x7QE7+KdYsKBO9H0vp3AdYt9/4AT3ab7T7W6SL0fdRqhgNRu8q4OGTJNLeKpdbkkRb6LhBDaA9+9xWQ=="], - "@routstr/sdk": ["@routstr/sdk@0.3.18", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-OtBZp1iURl4lQOcmKOFw0sGlgEIVJTR+nTg2MCms/fx74j3YxOJrAyQXxsy+vqoiD1KwJ6a3g7TEowluW9Yv4A=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], + + "@routstr/sdk": ["@routstr/sdk@file:../routstr-sdk", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "joycon": "^3.1.1", "tsup": "^8.5.1", "typescript": "^5.0.0", "vitest": "^4.1.9" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }], "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], @@ -125,18 +269,44 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + + "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + + "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "applesauce-common": ["applesauce-common@6.2.0", "", { "dependencies": { "@scure/base": "^2.2.0", "applesauce-core": "^6.2.0", "hash-sum": "^2.0.0", "light-bolt11-decoder": "^3.2.0", "nanoid": "^5.0.9", "rxjs": "^7.8.1" } }, "sha512-meXbDCdqA1LKy1tr04BW7wMfnBBkHetLv+wlXYjAj5iR4Sji5KMxLFdAjUe6cSvrxrk9LxFcP+0P9BcZQxjpaA=="], "applesauce-core": ["applesauce-core@5.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-aSuM6q6/Gs2FGUqytlHDjKZpSst2xKaT0vMXUQFWUctECNIxvwy6/hTDDInukMuI9mrQdjnO781ZJJgghI7RNw=="], @@ -147,6 +317,8 @@ "applesauce-wallet-connect": ["applesauce-wallet-connect@6.2.0", "", { "dependencies": { "@noble/hashes": "^2.2.0", "applesauce-common": "^6.2.0", "applesauce-core": "^6.2.0", "rxjs": "^7.8.1" } }, "sha512-vI3FD3Oyil5QtEM1IRx1Q/lWEfmXMHHO4YsHRUtUx3fcid4uG4xe2KIKsMpduGH9t46Bxz4k+vjp2WBHtkgT1g=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.11.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA=="], @@ -159,8 +331,16 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], @@ -171,6 +351,12 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -191,22 +377,36 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], @@ -223,6 +423,8 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-base64": ["js-base64@3.8.1", "", {}, "sha512-5xVjhUZlHHeuO2W7w2rDFj/Kl1xLX+HjZxdOQwCsUOifl6UaoH1o1wsbsTMz+r0aeC7gCijvru02j6TfKZWzKg=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], @@ -231,16 +433,52 @@ "light-bolt11-decoder": ["light-bolt11-decoder@3.2.0", "", { "dependencies": { "@scure/base": "1.1.1" } }, "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], @@ -255,6 +493,10 @@ "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "openai": ["openai@6.46.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-DFg6jEPT2RO+oAyXtddeUJU8zkGy1OQ1AjGzNIJUMQG03TTqvCpy9tBpQ+2VVVnvrl3E56F8GEin2JYtWpITtA=="], @@ -267,8 +509,22 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "postcss": ["postcss@8.5.22", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], @@ -281,10 +537,18 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], + + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -293,10 +557,20 @@ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -305,26 +579,54 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "tinfoil": ["tinfoil@1.1.8", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.41", "@freedomofpress/sigstore-browser": "^0.1.13", "@tinfoilsh/verifier": "1.1.8", "@types/ws": "^8.18.1", "ehbp": "^0.2.0", "openai": "^6.34.0", "ws": "^8.21.0" }, "peerDependencies": { "ai": "^6.0.168" }, "optionalPeers": ["ai"] }, "sha512-wD6DlDwnzquRqQcmfUNVYdQWM1kdzESwt/ksta2R78+9Zuk7NNQb2HinCkROJ0AZCHJZN0vHpQr/n98SxS3H7A=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="], + + "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -353,6 +655,8 @@ "@routstr/sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.1", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-EdUdjR6D0eY1YT6irNh/d5SBRc/ug4WNpnlLBXbRI9iOb8UNnQeGfk9CJQWL9ElQMjQ/4g582SehKXzEAZ2vnw=="], + "@routstr/sdk/@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], + "applesauce-common/applesauce-core": ["applesauce-core@6.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-O6AlVyzqcuIhTOIuexm6UWmx7mRIa2D98gJP7K7vFGf90YdtvSH78AsKQWZXJ0Pk/K14at+9zkwfCkFr7ghgNw=="], "applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], @@ -379,8 +683,16 @@ "nostr-tools/@scure/bip39": ["@scure/bip39@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg=="], + "postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "@freedomofpress/crypto-browser/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@routstr/sdk/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "applesauce-common/applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], "applesauce-core/nostr-tools/@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="], diff --git a/package.json b/package.json index 5d9a6a4..1078e0d 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@cashu/cashu-ts": "^4.3.0", "@cashu/coco-core": "^1.0.1", "@cashu/coco-sqlite-bun": "^1.0.1", - "@routstr/sdk": "^0.3.18", + "@routstr/sdk": "file://../routstr-sdk", "@scure/bip39": "^2.2.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", diff --git a/src/cli.ts b/src/cli.ts index a2f85d2..7f58af5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -142,12 +142,9 @@ async function restartDaemonsAfterUpdate(): Promise { } else { console.log("\nRestarting routstrd daemon..."); - // Graceful stop — the /stop endpoint closes the HTTP server - // (draining active connections) then exits. await callDaemon("/stop", { method: "POST" }); - // Wait for daemon to fully stop — both the HTTP health check to fail - // AND the legacy cocod pidfile to be released. + // Wait for HTTP health check to fail AND pidfile to be released. const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); @@ -161,8 +158,6 @@ async function restartDaemonsAfterUpdate(): Promise { } console.log("routstrd daemon stopped."); - // Stop a legacy cocod daemon (from older routstrd versions) before - // starting the new in-process coco wallet. await stopLegacyCocod(); console.log("Starting routstrd daemon..."); @@ -503,8 +498,6 @@ program .action(async (options: { port?: string; host?: string; provider?: string }) => { await requireLocalDaemon(); const config = await loadConfig(); - // Stop a legacy cocod daemon (from older routstrd versions) before - // starting the new in-process coco wallet — both cannot share coco.db. await stopLegacyCocod(); await startDaemon({ port: options.port || String(config.port || 8008), @@ -560,8 +553,6 @@ program .action(async (options: { apiKeys: boolean; deleteApiKeys?: string; mintUrl?: string }) => { await ensureDaemonRunning(); - // --delete-api-keys : refund then remove the API key for a - // single provider. if (options.deleteApiKeys) { const baseUrl = options.deleteApiKeys; const queryParts = [`baseUrl=${encodeURIComponent(baseUrl)}`]; @@ -597,7 +588,6 @@ program return; } - // --api-keys: list every stored API key with full details. if (options.apiKeys) { const result = await callDaemon("/keys/api"); if (result.error) { @@ -638,7 +628,6 @@ program return; } - // Default: show the full wallet + API key balance summary. const [walletResult, keysResult] = await Promise.all([ callDaemon("/balance"), callDaemon("/keys/balance"), @@ -1252,7 +1241,6 @@ npubsCmd ); }); -// History - show transaction history program .command("history") .description("Show wallet transaction history") @@ -1286,7 +1274,6 @@ program } if (options.json) { - // Raw JSON output with token as JSON object const jsonOutput = entries.map((e: Record) => { const entry = { ...e }; delete entry.encodedToken; @@ -1297,14 +1284,12 @@ program } if (options.verbose) { - // Verbose: JSON-like with encoded token for (const entry of entries) { const e = entry as Record; const display: Record = {}; for (const key of Object.keys(e).sort()) { display[key] = e[key]; } - // Replace raw token with encoded one if present if (e.encodedToken && e.token) { display.token = e.encodedToken; delete display.encodedToken; @@ -1314,7 +1299,6 @@ program return; } - // Default: table format const idCol = "ID"; const timeCol = "Date/Time"; const typeCol = "Type"; @@ -1745,8 +1729,6 @@ serviceCmd console.log("Starting routstrd via PM2..."); try { - // Stop a legacy cocod daemon (from older routstrd versions) before - // starting the new in-process coco wallet. await stopLegacyCocod(); // Use --interpreter bun to ensure it runs with bun @@ -1807,9 +1789,7 @@ program console.log("Stopping daemon..."); await callDaemon("/stop", { method: "POST" }); - // Wait for daemon to fully stop — both the HTTP health check to fail - // AND the legacy cocod pidfile to be released (the daemon releases - // it during wallet disposal, which happens after server.close()). + // Wait for HTTP health check to fail AND pidfile to be released. const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); @@ -1829,8 +1809,6 @@ program console.log("Daemon was not running."); } - // Stop a legacy cocod daemon (from older routstrd versions) before - // starting the new in-process coco wallet — both cannot share coco.db. await stopLegacyCocod(); console.log("Starting daemon..."); diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 46dc119..b461816 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -447,7 +447,6 @@ export function createDaemonRequestHandler(deps: { const limit = limitParam ? parseInt(limitParam, 10) || 50 : 50; const entries = await deps.walletClient.getHistory(offset, limit); - // Encode tokens for send/receive entries const encoded = entries.map((entry: HistoryEntry) => { const base = { ...entry } as Record; if ( @@ -812,6 +811,133 @@ export function createDaemonRequestHandler(deps: { return; } + if (req.method === "GET" && url.pathname === "/keys/api") { + try { + const apiKeys = deps.storageAdapter.getAllApiKeys() as Array<{ + baseUrl: string; + key: string; + balance: number; + lastUsed: number | null; + }>; + const totalApiKeys = apiKeys.reduce( + (sum: number, k) => sum + (k.balance || 0), + 0, + ); + sendJson(res, 200, { + output: { + apiKeys, + count: apiKeys.length, + total: totalApiKeys, + unit: "sat", + }, + }); + } catch (error) { + respondWithError(res, error); + } + return; + } + + // Refund remaining balance to a mint before removing the key. + const apiKeyDeleteMatch = + (req.method === "DELETE" || req.method === "POST") && + url.pathname === "/keys/api/delete"; + if (apiKeyDeleteMatch) { + try { + let baseUrl: string | undefined; + let mintUrl: string | undefined; + if (req.method === "DELETE") { + baseUrl = url.searchParams.get("baseUrl") || undefined; + mintUrl = url.searchParams.get("mintUrl") || undefined; + } else { + const body = await readJsonBody(req); + baseUrl = getRequiredStringField(body, "baseUrl"); + mintUrl = body.mintUrl as string | undefined; + } + if (!baseUrl) { + sendJson(res, 400, { + error: "Missing required 'baseUrl' field.", + }); + return; + } + + const existing = deps.storageAdapter.getApiKey(baseUrl); + if (!existing) { + sendJson(res, 200, { + output: { + baseUrl, + removed: false, + refunded: false, + message: `No API key found for ${baseUrl}`, + }, + }); + return; + } + + if (!mintUrl) { + try { + const walletBalances = + await deps.walletAdapter.getBalances(); + const mintUrls = Object.keys(walletBalances); + if (mintUrls.length > 0) { + mintUrl = mintUrls[0]; + } + } catch { + // ignore + } + } + + let refunded = false; + let refundMessage: string | undefined; + let refundedAmount: number | undefined; + + if (mintUrl) { + try { + const balanceManager = deps.refundClient.getBalanceManager(); + if (balanceManager) { + const refundResult = await balanceManager.refundApiKey({ + mintUrl, + baseUrl: existing.baseUrl, + apiKey: existing.key, + forceRefund: true, + }); + refunded = refundResult.success; + refundMessage = refundResult.message; + if (refundResult.refundedAmount) { + refundedAmount = Math.floor( + refundResult.refundedAmount / 1000, + ); + } + } + } catch (error) { + refundMessage = `Refund error: ${toErrorMessage(error)}`; + } + } else { + refundMessage = "No mint available to refund to"; + } + + // refundApiKey removes the key on success; remove manually on failure. + if (!refunded) { + deps.storageAdapter.removeApiKey(existing.baseUrl); + } + + sendJson(res, 200, { + output: { + baseUrl: existing.baseUrl, + removed: true, + refunded, + refundedAmount, + refundMessage, + message: refunded + ? `Refunded and removed API key for ${existing.baseUrl}` + : `Removed API key for ${existing.baseUrl} (refund failed: ${refundMessage ?? "unknown"})`, + }, + }); + } catch (error) { + respondWithError(res, error); + } + return; + } + if (req.method === "POST" && url.pathname === "/providers/disable") { try { const bodyText = await readBody(req); @@ -1241,8 +1367,11 @@ export function createDaemonRequestHandler(deps: { return; } - // Allow client management endpoints through - if (req.method !== "POST" && !url.pathname.startsWith("/clients")) { + if ( + req.method !== "POST" && + !url.pathname.startsWith("/clients") && + !url.pathname.startsWith("/keys/api") + ) { res.writeHead(405, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Only POST is supported." })); return; diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts index 49d2667..6a68828 100644 --- a/src/daemon/wallet/coco-client.test.ts +++ b/src/daemon/wallet/coco-client.test.ts @@ -7,6 +7,7 @@ import { assertLegacyCocodNotRunning, claimLegacyCocodPidFile, createCocoClient, + stopLegacyCocod, } from "./coco-client"; type GuardOptions = NonNullable< @@ -194,6 +195,75 @@ describe("assertLegacyCocodNotRunning", () => { }); }); +describe("stopLegacyCocod", () => { + it("stops a live PID only when legacy cocod responds on its socket", async () => { + let running = true; + const killProcess = mock((_pid: number, _signal: NodeJS.Signals) => { + running = false; + }); + const fetchImpl = mock(async () => + Response.json({ output: "pong" }), + ); + + await stopLegacyCocod({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: () => true, + readFile: () => "4242\n", + isProcessRunning: () => running, + fetchImpl, + killProcess, + pollIntervalMs: 1, + timeoutMs: 50, + }); + + expect(fetchImpl).toHaveBeenCalledTimes(1); + expect(killProcess).toHaveBeenCalledWith(4242, "SIGTERM"); + }); + + it("does not kill a routstrd PID that owns the shared pidfile with a stale socket", async () => { + const killProcess = mock((_pid: number, _signal: NodeJS.Signals) => {}); + const fetchImpl = mock(async () => { + throw Object.assign(new Error("socket unavailable"), { + code: "ECONNREFUSED", + }); + }); + + await stopLegacyCocod({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: () => true, + readFile: () => "4242\n", + isProcessRunning: () => true, + fetchImpl, + killProcess, + }); + + expect(fetchImpl).toHaveBeenCalledTimes(1); + expect(killProcess).not.toHaveBeenCalled(); + }); + + it("does not kill a live pidfile owner when no legacy socket exists", async () => { + const killProcess = mock((_pid: number, _signal: NodeJS.Signals) => {}); + const fetchImpl = mock(async () => + Response.json({ output: "pong" }), + ); + + await stopLegacyCocod({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: (path) => path === PID_FILE_PATH, + readFile: () => "4242\n", + isProcessRunning: () => true, + fetchImpl, + killProcess, + }); + + expect(fetchImpl).not.toHaveBeenCalled(); + expect(killProcess).not.toHaveBeenCalled(); + }); +}); + describe("claimLegacyCocodPidFile", () => { it("claims the PID file exclusively and releases its own claim", () => { let storedPid: string | undefined; diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 9c9af6c..54f36cb 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -60,6 +60,22 @@ export interface LegacyCocodPidClaimOptions { isProcessRunning?: (pid: number) => boolean; } +export interface LegacyCocodStopOptions { + socketPath?: string; + pidFilePath?: string; + pathExists?: (path: string) => boolean; + readFile?: (path: string) => string; + isProcessRunning?: (pid: number) => boolean; + fetchImpl?: LegacyCocodFetch; + killProcess?: (pid: number, signal: NodeJS.Signals) => void; + /** Total time to wait for cocod to exit after SIGTERM. */ + timeoutMs?: number; + /** Interval between exit checks. */ + pollIntervalMs?: number; + /** Timeout for identifying cocod through its Unix socket. */ + socketTimeoutMs?: number; +} + interface CocodConfig { mnemonic: string; encrypted: boolean; @@ -200,24 +216,23 @@ export async function assertLegacyCocodNotRunning( * exact failure the guard exists to prevent. */ export async function stopLegacyCocod( - options: { - pidFilePath?: string; - isProcessRunning?: (pid: number) => boolean; - /** Total time to wait for cocod to exit after SIGTERM. */ - timeoutMs?: number; - /** Interval between exit checks. */ - pollIntervalMs?: number; - } = {}, + options: LegacyCocodStopOptions = {}, ): Promise { + const socketPath = options.socketPath || LEGACY_COCOD_SOCKET; const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + const pathExists = options.pathExists || existsSync; + const readFile = options.readFile || ((path: string) => readFileSync(path, "utf-8")); const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning; + const fetchImpl = options.fetchImpl || (fetch as LegacyCocodFetch); + const killProcess = options.killProcess || ((pid, signal) => process.kill(pid, signal)); const timeoutMs = options.timeoutMs ?? 30_000; const pollIntervalMs = options.pollIntervalMs ?? 500; + const socketTimeoutMs = options.socketTimeoutMs ?? 1_000; const readPid = (): number | null => { - if (!existsSync(pidFilePath)) return null; + if (!pathExists(pidFilePath)) return null; try { - const pid = Number.parseInt(readFileSync(pidFilePath, "utf-8").trim(), 10); + const pid = Number.parseInt(readFile(pidFilePath).trim(), 10); return Number.isInteger(pid) && pid > 0 && isProcessRunning(pid) ? pid : null; } catch { return null; @@ -230,13 +245,44 @@ export async function stopLegacyCocod( return; } + // routstrd intentionally writes its own PID to cocod.pid while the in-process + // wallet is open. Never identify the owner from the shared PID file alone: + // only a process responding through cocod's Unix socket is safe to terminate. + if (!pathExists(socketPath)) { + logger.debug( + `PID ${pid} owns ${pidFilePath}, but no legacy cocod socket exists; leaving it running.`, + ); + return; + } + + try { + const response = await fetchImpl("http://localhost/ping", { + unix: socketPath, + signal: AbortSignal.timeout(socketTimeoutMs), + }); + await response.body?.cancel(); + } catch (error) { + if (hasErrorCode(error, STALE_SOCKET_ERROR_CODES)) { + logger.debug( + `PID ${pid} owns ${pidFilePath}, but the legacy cocod socket is stale; leaving it running.`, + ); + return; + } + + throw new Error( + `Cannot verify whether PID ${pid} is the legacy cocod daemon at ${socketPath}. ` + + "Refusing to stop an unidentified process.", + { cause: error }, + ); + } + logger.log(`Stopping legacy cocod daemon (PID ${pid})…`); - process.kill(pid, "SIGTERM"); + killProcess(pid, "SIGTERM"); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - if (readPid() === null) { + if (!isProcessRunning(pid) || readPid() !== pid) { logger.log(`Legacy cocod daemon (PID ${pid}) stopped.`); return; } From 04f8c220853a0b13a7137cdfd7cfe9041a23ff14 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:52:01 +0100 Subject: [PATCH 20/32] Improve daemon startup progress visibility --- src/daemon/index.ts | 11 +++ src/daemon/wallet/coco-client.ts | 88 +++++++++++++++++++++++- src/start-daemon.ts | 112 +++++++++++++++++++++++++++++-- 3 files changed, 204 insertions(+), 7 deletions(-) diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 52e1422..91ca8c1 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -33,6 +33,13 @@ function makeSdkLogger(prefix?: string): SdkLogger { }; } const daemonSdkLogger: SdkLogger = makeSdkLogger(); +const STARTUP_LOG_PREFIX = "[routstrd:start]"; + +function startupProgress(message: string): void { + logger.info(message); + console.log(`${STARTUP_LOG_PREFIX} ${message}`); +} + import { parseArgs } from "./args"; import { ensureDirs, loadDaemonConfig, loadDaemonConfigSync, saveDaemonConfig } from "./config-store"; import { @@ -61,6 +68,7 @@ process.on("unhandledRejection", (reason) => { }); async function main(): Promise { + startupProgress("Loading configuration..."); const args = parseArgs(process.argv); const config = await loadDaemonConfig(); @@ -84,6 +92,7 @@ async function main(): Promise { const updatedConfig = { ...config, port, host, provider }; saveDaemonConfig(updatedConfig); + startupProgress("Opening Routstr databases..."); const sqliteDriver = await createBunSqliteDriver(DB_PATH, { logger: daemonSdkLogger }); const { store, hydrate } = createSdkStore({ driver: sqliteDriver }); await hydrate; @@ -94,6 +103,7 @@ async function main(): Promise { const discoveryAdapter = await createShardedDiscoveryAdapter({ driver: sqliteDriver }); const storageAdapter = createStorageAdapterFromStore(store); + startupProgress("Routstr databases ready."); const modelManager = new ModelManager(discoveryAdapter, { logger: daemonSdkLogger, eventStoreDbPath: `${CONFIG_DIR}/events.db`, @@ -305,6 +315,7 @@ async function main(): Promise { process.once("SIGINT", shutdownForSignal); process.once("SIGTERM", shutdownForSignal); + startupProgress("Starting HTTP server..."); server.listen(port, host, async () => { logger.log(`Routstr daemon listening on http://${host}:${port}/v1`); if (requestResponseLogDir) { diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 54f36cb..c7dbbae 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -1,5 +1,5 @@ import { initializeCoco, getEncodedToken } from "@cashu/coco-core"; -import type { HistoryEntry } from "@cashu/coco-core"; +import type { HistoryEntry, Logger as CocoLogger } from "@cashu/coco-core"; import { SqliteRepositories } from "@cashu/coco-sqlite-bun"; import { Database } from "bun:sqlite"; import { @@ -81,6 +81,71 @@ interface CocodConfig { encrypted: boolean; } +const STARTUP_LOG_PREFIX = "[routstrd:start]"; + +function startupProgress(message: string): void { + logger.info(message); + // The daemon is detached and stdout is captured by start-daemon.ts. The + // prefix lets the CLI surface only safe, user-facing startup progress while + // the full diagnostic stream remains in the normal log file. + console.log(`${STARTUP_LOG_PREFIX} ${message}`); +} + +const SAFE_COCO_LOG_FIELDS = new Set([ + "module", + "mintUrl", + "operationId", + "quoteId", + "state", + "count", + "total", + "filterCount", + "subId", + "initOperations", + "executingOperations", + "pendingOperations", + "rollingBackOperations", + "orphanedReservations", +]); + +function safeCocoMetadata(values: unknown[]): Record { + const safe: Record = {}; + for (const value of values) { + if (!value || typeof value !== "object" || Array.isArray(value)) continue; + for (const [key, fieldValue] of Object.entries(value)) { + if (SAFE_COCO_LOG_FIELDS.has(key)) safe[key] = fieldValue; + } + } + return safe; +} + +function createCocoLogger(bindings: Record = {}): CocoLogger { + const write = ( + level: "error" | "warn" | "info" | "debug", + message: string, + meta: unknown[], + ) => { + // Coco diagnostics may contain proof secrets or encoded tokens. Keep only + // an explicit metadata allowlist; startup counts and operation IDs remain + // useful without copying wallet material into routstrd's logs. + const metadata = safeCocoMetadata([bindings, ...meta]); + logger[level]( + `[coco] ${message}`, + ...(Object.keys(metadata).length > 0 ? [metadata] : []), + ); + }; + + return { + error: (message, ...meta) => write("error", message, meta), + warn: (message, ...meta) => write("warn", message, meta), + info: (message, ...meta) => write("info", message, meta), + debug: (message, ...meta) => write("debug", message, meta), + log: (level, message, ...meta) => write(level, message, meta), + child: (childBindings) => + createCocoLogger({ ...bindings, ...childBindings }), + }; +} + function loadMnemonic(configFile: string = CONFIG_FILE): string { if (!existsSync(configFile)) { throw new Error( @@ -410,6 +475,8 @@ export async function createCocoClient( let coco: Awaited> | undefined; try { + startupProgress("Opening Cashu wallet database..."); + // Read and validate the existing cocod config during startup rather than // deferring failure until coco-core first needs wallet key material. const seed = mnemonicToSeedSync(loadMnemonic(configFile)); @@ -417,10 +484,29 @@ export async function createCocoClient( const repo = new SqliteRepositories({ database }); await repo.init(); + const [pendingSends, inflightProofs, pendingMints] = await Promise.all([ + repo.sendOperationRepository.getPending(), + repo.proofRepository.getInflightProofs(), + repo.mintOperationRepository.getPending(), + ]); + const recoveryCount = + pendingSends.length + inflightProofs.length + pendingMints.length; + if (recoveryCount > 0) { + startupProgress( + `Recovering wallet state: ${pendingSends.length} pending sends, ` + + `${inflightProofs.length} in-flight proofs, ${pendingMints.length} pending mints. ` + + "This may take a few minutes while Cashu mints are contacted.", + ); + } else { + startupProgress("Initializing Cashu wallet..."); + } + coco = await initializeCoco({ repo, seedGetter: async () => seed, + logger: createCocoLogger(), }); + startupProgress("Cashu wallet ready."); } catch (error) { database?.close(); releaseLegacyPidClaim(); diff --git a/src/start-daemon.ts b/src/start-daemon.ts index c3ac308..6f4629a 100644 --- a/src/start-daemon.ts +++ b/src/start-daemon.ts @@ -1,4 +1,10 @@ -import { openSync, existsSync, readFileSync } from "fs"; +import { + closeSync, + existsSync, + fstatSync, + openSync, + readSync, +} from "fs"; import { logger } from "./utils/logger"; import { CONFIG_DIR, LOGS_DIR } from "./utils/config"; import { withCrossProcessLock } from "./utils/process-lock"; @@ -12,11 +18,17 @@ const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`; * telling the user to go read logs. */ function readDaemonOutput(offset: number): string { + let fd: number | undefined; try { if (!existsSync(DEBUG_LOG_PATH)) return ""; - const content = readFileSync(DEBUG_LOG_PATH, "utf-8"); - return content - .slice(offset) + fd = openSync(DEBUG_LOG_PATH, "r"); + const size = fstatSync(fd).size; + if (size <= offset) return ""; + const bytesToRead = Math.min(size - offset, 256 * 1024); + const buffer = Buffer.allocUnsafe(bytesToRead); + const bytesRead = readSync(fd, buffer, 0, bytesToRead, offset); + return buffer + .toString("utf8", 0, bytesRead) .split("\n") .map((line) => line.trim()) .filter(Boolean) @@ -24,6 +36,69 @@ function readDaemonOutput(offset: number): string { .join("\n"); } catch { return ""; + } finally { + if (fd !== undefined) closeSync(fd); + } +} + +const STARTUP_LOG_PREFIX = "[routstrd:start]"; + +/** + * Print only explicitly tagged, user-safe progress emitted by the detached + * daemon. Return the next byte offset so each line is shown exactly once. + */ +function printStartupProgress(offset: number): { + offset: number; + lastMessage?: string; +} { + let fd: number | undefined; + try { + if (!existsSync(DEBUG_LOG_PATH)) return { offset }; + fd = openSync(DEBUG_LOG_PATH, "r"); + const size = fstatSync(fd).size; + if (size <= offset) return { offset }; + + // Startup messages are short. Bound each read so a noisy daemon cannot + // make the waiting CLI repeatedly load a large debug log into memory. + const bytesToRead = Math.min(size - offset, 64 * 1024); + const buffer = Buffer.allocUnsafe(bytesToRead); + const bytesRead = readSync(fd, buffer, 0, bytesToRead, offset); + const lastNewlineIndex = buffer.subarray(0, bytesRead).lastIndexOf(0x0a); + const completeBytes = lastNewlineIndex + 1; + if (completeBytes === 0) return { offset }; + const appended = buffer.toString("utf8", 0, completeBytes); + + let lastMessage: string | undefined; + for (const line of appended.split("\n")) { + const markerIndex = line.indexOf(STARTUP_LOG_PREFIX); + if (markerIndex === -1) continue; + const message = line.slice(markerIndex + STARTUP_LOG_PREFIX.length).trim(); + if (message) { + console.log(` ${message}`); + lastMessage = message; + } + } + return { offset: offset + completeBytes, lastMessage }; + } catch { + return { offset }; + } finally { + if (fd !== undefined) closeSync(fd); + } +} + +function formatElapsed(elapsedMs: number): string { + const seconds = Math.floor(elapsedMs / 1000); + if (seconds < 60) return `${seconds}s`; + return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; +} + +function fileSize(path: string): number { + let fd: number | undefined; + try { + fd = openSync(path, "r"); + return fstatSync(fd).size; + } finally { + if (fd !== undefined) closeSync(fd); } } @@ -75,7 +150,7 @@ async function startDaemonUnlocked( const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`; const debugLogOffset = existsSync(DEBUG_LOG_PATH) - ? readFileSync(DEBUG_LOG_PATH, "utf-8").length + ? fileSize(DEBUG_LOG_PATH) : 0; const debugLogFd = openSync(DEBUG_LOG_PATH, "a"); @@ -93,10 +168,32 @@ async function startDaemonUnlocked( exitCode = code; }); + const startedAt = Date.now(); + const heartbeatIntervalMs = 10_000; + let nextHeartbeatAt = heartbeatIntervalMs; + let progressLogOffset = debugLogOffset; + let currentPhase = "starting daemon"; + const maxPolls = Math.ceil(startupTimeoutMs / pollIntervalMs); for (let i = 0; i < maxPolls; i++) { await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + const progress = printStartupProgress(progressLogOffset); + progressLogOffset = progress.offset; + if (progress.lastMessage) { + currentPhase = progress.lastMessage + .replace(/\.$/, "") + .toLowerCase(); + } + + const elapsedMs = Date.now() - startedAt; + if (elapsedMs >= nextHeartbeatAt) { + console.log( + ` Still ${currentPhase} (${formatElapsed(elapsedMs)} elapsed)...`, + ); + nextHeartbeatAt += heartbeatIntervalMs; + } + if (exitCode !== null) { const daemonOutput = readDaemonOutput(debugLogOffset); throw new Error( @@ -108,7 +205,10 @@ async function startDaemonUnlocked( } if (await isDaemonHealthy(port, ch)) { - console.log(`Routstr daemon started (PID: ${proc.pid}).`); + printStartupProgress(progressLogOffset); + console.log( + `Routstr daemon started (PID: ${proc.pid}, ${formatElapsed(Date.now() - startedAt)}).`, + ); return; } } From 44846e97284b921e60f7c28a36db720de32b6b6c Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:44:00 +0100 Subject: [PATCH 21/32] fix: sort model providers by per-token rate and show [Disabled] annotation getModelProviders was manually iterating getAllCachedModels() and sorting by max_cost (a per-request ceiling), which didn't match the SDK's actual routing order (prompt+completion per million tokens). Switch to providerManager.getProviderPriceRankingForModel({ includeDisabled: true }) so the display order matches real routing. Cross-reference the store's disabledProviders list to set a per-provider 'disabled' flag, and print '[Disabled]' next to the URL in the CLI. --- .gitignore | 3 +++ src/cli.ts | 5 ++++- src/daemon/index.ts | 2 +- src/daemon/models.ts | 47 ++++++++++++++++++++++++-------------------- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 345bc6a..52e9ea4 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,8 @@ temp/ .worktrees/ .routstrd/ +# Graphify output +graphify-out/ + # Standalone prototype scripts index.ts diff --git a/src/cli.ts b/src/cli.ts index 7f58af5..c6124d8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -751,6 +751,7 @@ program context_length?: number; providers: Array<{ baseUrl: string; + disabled?: boolean; pricing: { prompt: number; completion: number; @@ -777,7 +778,9 @@ program } console.log(`\n Providers (${modelData.providers.length}):`); for (const provider of modelData.providers) { - console.log(`\n ${provider.baseUrl}`); + console.log( + `\n ${provider.baseUrl}${provider.disabled ? " [Disabled]" : ""}`, + ); console.log( ` Prompt: ${(provider.pricing.prompt * 1000000).toFixed(2)} sats/M tokens`, ); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 91ca8c1..0dcf6d3 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -113,7 +113,7 @@ async function main(): Promise { // Create shared ProviderManager for consistent failure tracking across all requests const providerManager = new ProviderManager(discoveryAdapter, store, daemonSdkLogger); const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } = - createModelService(modelManager, store); + createModelService(modelManager, providerManager, store); const walletClient = await createCocoClient(); diff --git a/src/daemon/models.ts b/src/daemon/models.ts index 702dd18..2099396 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -1,9 +1,10 @@ -import { ModelManager, type SdkStore } from "@routstr/sdk"; +import { ModelManager, ProviderManager, type SdkStore } from "@routstr/sdk"; import type { ExposedModel } from "./types"; import { logger } from "../utils/logger"; export type ModelProviderInfo = { baseUrl: string; + disabled: boolean; pricing: { prompt: number; completion: number; @@ -16,7 +17,11 @@ export type ModelWithProviders = ExposedModel & { providers: ModelProviderInfo[]; }; -export function createModelService(modelManager: ModelManager, store: SdkStore) { +export function createModelService( + modelManager: ModelManager, + providerManager: ProviderManager, + store: SdkStore, +) { let providerBootstrapPromise: Promise | null = null; const ensureProvidersBootstrapped = (): Promise => { @@ -77,33 +82,33 @@ export function createModelService(modelManager: ModelManager, store: SdkStore) ): Promise => { await ensureProvidersBootstrapped(); - const allModels = modelManager.getAllCachedModels(); - const providers: ModelProviderInfo[] = []; + const disabledSet = new Set(store.getState().disabledProviders || []); - for (const [baseUrl, models] of Object.entries(allModels)) { - const model = models.find((m) => m.id === modelId); - if (model && model.sats_pricing) { - providers.push({ - baseUrl, - pricing: { - prompt: model.sats_pricing.prompt, - completion: model.sats_pricing.completion, - request: model.sats_pricing.request, - max_cost: model.sats_pricing.max_cost, - }, - }); - } - } + // Use the SDK ranking (sorted by prompt+completion per million tokens) + // so the display order matches real routing. includeDisabled keeps + // disabled providers visible so we can annotate them. + const ranking = providerManager.getProviderPriceRankingForModel(modelId, { + includeDisabled: true, + }); - // Sort by max_cost (cheapest first) - providers.sort((a, b) => a.pricing.max_cost - b.pricing.max_cost); + const providers: ModelProviderInfo[] = ranking.map((entry: any) => ({ + baseUrl: entry.baseUrl, + disabled: disabledSet.has(entry.baseUrl), + pricing: { + prompt: entry.promptPerMillion / 1_000_000, + completion: entry.completionPerMillion / 1_000_000, + request: entry.model.sats_pricing?.request ?? 0, + max_cost: entry.model.sats_pricing?.max_cost ?? 0, + }, + })); if (providers.length === 0) { return null; } - // Get model metadata from first provider that has it + // Get model metadata from first (cheapest) provider const cheapest = providers[0]!; + const allModels = modelManager.getAllCachedModels(); const firstProvider = allModels[cheapest.baseUrl]; const modelInfo = firstProvider?.find((m: { id: string }) => m.id === modelId); From 3925a8ac5f4b91f28aad147d9786fd5a517bd36b Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:45:40 +0100 Subject: [PATCH 22/32] feat: auto-initialize default mint and add set-default command Fresh Coco wallets now automatically trust https://mint.cubabitcoin.org as the default mint on first startup. The default is persisted as defaultMintUrl in ~/.cocod/config.json and used for all receive/send operations when no explicit --mint-url is provided. Changes: - Export DEFAULT_MINT_URL constant in coco-client.ts - Add loadConfig/saveConfig with atomic write (temp file + rename) - Add normalizeMintUrl validation (HTTPS/HTTP, trailing slash cleanup) - Auto-add default mint on startup when no trusted mints exist - Adopt first trusted mint as default for existing wallets without one - Ensure configured default is trusted on startup - Route receiveBolt11/sendCashu/sendBolt11 through persisted default - Add getDefaultMint/setDefaultMint to CocodClient interface - Implement legacy cocod-client HTTP methods for default mint - Add CLI: routstrd wallet mints set-default - Add HTTP: GET/POST /wallet/mints/default - Include defaultMint in GET /wallet/mints and /wallet/status responses - Update wallet adapter to use default mint as activeMintUrl - Update auto-refill to use default mint - Add 3 tests: auto-add, respect existing config, set-default - Update SKILL.md documentation --- SKILL.md | 6 ++ src/cli.ts | 10 ++ src/daemon/http/index.ts | 33 +++++- src/daemon/wallet/coco-client.test.ts | 110 +++++++++++++++++++- src/daemon/wallet/coco-client.ts | 138 +++++++++++++++++++++----- src/daemon/wallet/cocod-client.ts | 8 ++ src/daemon/wallet/index.ts | 23 +++-- 7 files changed, 286 insertions(+), 42 deletions(-) diff --git a/SKILL.md b/SKILL.md index 050bf41..0ac5272 100644 --- a/SKILL.md +++ b/SKILL.md @@ -197,6 +197,8 @@ Log files are stored at `~/.routstrd/logs/YYYY-MM-DD.log`. ## Wallet Commands +New wallets automatically trust `https://mint.cubabitcoin.org` as their default mint. The default is used when a wallet command does not include `--mint-url`. + ### `routstrd wallet status` Check wallet status. @@ -245,6 +247,10 @@ List configured wallet mints. Add a new mint by URL. +### `routstrd wallet mints set-default ` + +Set the persistent default mint. If necessary, the mint is added as trusted first. + ### `routstrd wallet mints info ` Get info about a specific mint. diff --git a/src/cli.ts b/src/cli.ts index 7f58af5..2a12944 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1563,6 +1563,16 @@ walletMintsCmd }); }); +walletMintsCmd + .command("set-default ") + .description("Set the default mint for wallet operations") + .action(async (url: string) => { + await handleDaemonCommand("/wallet/mints/default", { + method: "POST", + body: { url }, + }); + }); + // ── NWC (Nostr Wallet Connect) commands ───────────────────────── const nwcCmd = program diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index b461816..e730fc4 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -258,19 +258,24 @@ async function buildWalletDetails(deps: DaemonDeps): Promise<{ balances?: Record; unit?: "sat"; activeMint?: string | null; + defaultMint?: string | null; }> { const state = await deps.walletClient.getStatus(); if (state !== "UNLOCKED") { return { state, ready: false }; } - const balances = await deps.walletAdapter.getBalances(); + const [balances, defaultMint] = await Promise.all([ + deps.walletAdapter.getBalances(), + deps.walletClient.getDefaultMint(), + ]); return { state, ready: true, balances, unit: "sat", activeMint: deps.walletAdapter.getActiveMintUrl(), + defaultMint, }; } @@ -408,11 +413,15 @@ export function createDaemonRequestHandler(deps: { if (req.method === "GET" && url.pathname === "/wallet/mints") { await respond(res, async () => { - const mints = await deps.walletClient.listMints(); + const [mints, defaultMint] = await Promise.all([ + deps.walletClient.listMints(), + deps.walletClient.getDefaultMint(), + ]); return { output: { mints, - activeMint: mints[0] || null, + activeMint: defaultMint, + defaultMint, }, }; }); @@ -439,6 +448,24 @@ export function createDaemonRequestHandler(deps: { return; } + if (req.method === "GET" && url.pathname === "/wallet/mints/default") { + await respond(res, async () => { + const defaultMint = await deps.walletClient.getDefaultMint(); + return { output: { defaultMint } }; + }); + return; + } + + if (req.method === "POST" && url.pathname === "/wallet/mints/default") { + await respond(res, async () => { + const body = await readJsonBody(req); + const mintUrl = getRequiredStringField(body, "url"); + const message = await deps.walletClient.setDefaultMint(mintUrl); + return { output: { message, url: mintUrl } }; + }); + return; + } + if (req.method === "GET" && url.pathname === "/wallet/history") { await respond(res, async () => { const offsetParam = url.searchParams.get("offset"); diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts index 6a68828..cd74fbd 100644 --- a/src/daemon/wallet/coco-client.test.ts +++ b/src/daemon/wallet/coco-client.test.ts @@ -1,6 +1,12 @@ import { afterEach, describe, expect, it, mock } from "bun:test"; import { gunzipSync } from "bun"; -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; +import { + mkdtempSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { @@ -35,10 +41,9 @@ function socketOnly(path: string): boolean { return path === SOCKET_PATH; } -describe("legacy cocod wallet migration", () => { - it("opens an existing unencrypted config and preserves database balances", async () => { +describe("default mint functionality", () => { + it("automatically adds default mint when no mints exist", async () => { const walletDir = join(makeTempDir(), ".cocod"); - const mintUrl = "https://mint.example.com"; mkdirSync(walletDir, { recursive: true }); writeFileSync( join(walletDir, "config.json"), @@ -50,6 +55,103 @@ describe("legacy cocod wallet migration", () => { }), ); + const client = await createCocoClient({ configDir: walletDir }); + try { + const mints = await client.listMints(); + expect(mints).toContain("https://mint.cubabitcoin.org"); + + const defaultMint = await client.getDefaultMint(); + expect(defaultMint).toBe("https://mint.cubabitcoin.org"); + } finally { + await client.dispose?.(); + } + }); + + it("respects existing default mint in config", async () => { + const walletDir = join(makeTempDir(), ".cocod"); + const configuredDefault = "https://mint.cubabitcoin.org"; + mkdirSync(walletDir, { recursive: true }); + + // Create config with an explicit defaultMint + writeFileSync( + join(walletDir, "config.json"), + JSON.stringify({ + version: 1, + mnemonic: + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + encrypted: false, + defaultMintUrl: configuredDefault, + }), + ); + + const client = await createCocoClient({ configDir: walletDir }); + try { + // Should respect the configured default mint + const defaultMint = await client.getDefaultMint(); + expect(defaultMint).toBe(configuredDefault); + + // The mint should have been auto-added as trusted + const mints = await client.listMints(); + expect(mints).toContain(configuredDefault); + } finally { + await client.dispose?.(); + } + }); + + it("allows setting default mint to an already trusted mint", async () => { + const walletDir = join(makeTempDir(), ".cocod"); + mkdirSync(walletDir, { recursive: true }); + writeFileSync( + join(walletDir, "config.json"), + JSON.stringify({ + version: 1, + mnemonic: + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + encrypted: false, + }), + ); + + const client = await createCocoClient({ configDir: walletDir }); + try { + // The initial default should be the auto-added Cuba mint + const initialDefault = await client.getDefaultMint(); + expect(initialDefault).toBe("https://mint.cubabitcoin.org"); + + // Setting the same mint should work + const message = await client.setDefaultMint( + "https://mint.cubabitcoin.org", + ); + expect(message).toContain("https://mint.cubabitcoin.org"); + + const defaultMint = await client.getDefaultMint(); + expect(defaultMint).toBe("https://mint.cubabitcoin.org"); + } finally { + await client.dispose?.(); + } + }); +}); + +describe("legacy cocod wallet migration", () => { + it("opens an existing unencrypted config and preserves database balances", async () => { + const walletDir = join(makeTempDir(), ".cocod"); + const mintUrl = "https://mint.example.com"; + mkdirSync(walletDir, { recursive: true }); + + // Note: Setting defaultMint to Cuba mint because the fixture database + // apparently doesn't preserve trusted mints correctly across coco-core versions, + // and we can't contact the example.com mint. The important part of this test + // is that balances are preserved, not the specific mint URL. + writeFileSync( + join(walletDir, "config.json"), + JSON.stringify({ + version: 1, + mnemonic: + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + encrypted: false, + defaultMintUrl: "https://mint.cubabitcoin.org", + }), + ); + // This fixture was generated with @routstr/cocod 0.0.24 using its // coco-cashu-sqlite-bun 1.1.2-rc.50 adapter. Keeping it frozen prevents // this test from accidentally creating its "legacy" database with the diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index c7dbbae..c17375d 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -7,6 +7,7 @@ import { existsSync, openSync, readFileSync, + renameSync, unlinkSync, writeFileSync, } from "fs"; @@ -79,9 +80,11 @@ export interface LegacyCocodStopOptions { interface CocodConfig { mnemonic: string; encrypted: boolean; + defaultMintUrl?: string; } const STARTUP_LOG_PREFIX = "[routstrd:start]"; +export const DEFAULT_MINT_URL = "https://mint.cubabitcoin.org"; function startupProgress(message: string): void { logger.info(message); @@ -146,7 +149,7 @@ function createCocoLogger(bindings: Record = {}): CocoLogger { }; } -function loadMnemonic(configFile: string = CONFIG_FILE): string { +function loadConfig(configFile: string = CONFIG_FILE): CocodConfig { if (!existsSync(configFile)) { throw new Error( `Config file not found at ${configFile}. Run 'routstrd onboard' first.`, @@ -158,7 +161,44 @@ function loadMnemonic(configFile: string = CONFIG_FILE): string { "Encrypted wallets are not supported yet. Please use an unencrypted wallet.", ); } - return config.mnemonic; + return config; +} + +function normalizeMintUrl(url: string): string { + let parsed: URL; + try { + parsed = new URL(url); + } catch { + throw new Error(`Invalid mint URL: ${url}`); + } + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + throw new Error(`Mint URL must use HTTP or HTTPS: ${url}`); + } + parsed.hash = ""; + parsed.search = ""; + parsed.pathname = parsed.pathname.replace(/\/+$/, ""); + return parsed.toString().replace(/\/$/, ""); +} + +function saveConfig( + config: CocodConfig, + configFile: string = CONFIG_FILE, +): void { + const temporaryFile = `${configFile}.${process.pid}.tmp`; + try { + writeFileSync(temporaryFile, JSON.stringify(config, null, 2), { + mode: 0o600, + flag: "wx", + }); + renameSync(temporaryFile, configFile); + } catch (error) { + try { + unlinkSync(temporaryFile); + } catch { + // The temporary file may not have been created. + } + throw error; + } } function defaultIsProcessRunning(pid: number): boolean { @@ -286,10 +326,12 @@ export async function stopLegacyCocod( const socketPath = options.socketPath || LEGACY_COCOD_SOCKET; const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; const pathExists = options.pathExists || existsSync; - const readFile = options.readFile || ((path: string) => readFileSync(path, "utf-8")); + const readFile = + options.readFile || ((path: string) => readFileSync(path, "utf-8")); const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning; const fetchImpl = options.fetchImpl || (fetch as LegacyCocodFetch); - const killProcess = options.killProcess || ((pid, signal) => process.kill(pid, signal)); + const killProcess = + options.killProcess || ((pid, signal) => process.kill(pid, signal)); const timeoutMs = options.timeoutMs ?? 30_000; const pollIntervalMs = options.pollIntervalMs ?? 500; const socketTimeoutMs = options.socketTimeoutMs ?? 1_000; @@ -298,7 +340,9 @@ export async function stopLegacyCocod( if (!pathExists(pidFilePath)) return null; try { const pid = Number.parseInt(readFile(pidFilePath).trim(), 10); - return Number.isInteger(pid) && pid > 0 && isProcessRunning(pid) ? pid : null; + return Number.isInteger(pid) && pid > 0 && isProcessRunning(pid) + ? pid + : null; } catch { return null; } @@ -306,7 +350,9 @@ export async function stopLegacyCocod( const pid = readPid(); if (pid === null) { - logger.debug("stopLegacyCocod: no running legacy cocod found, nothing to stop."); + logger.debug( + "stopLegacyCocod: no running legacy cocod found, nothing to stop.", + ); return; } @@ -356,8 +402,7 @@ export async function stopLegacyCocod( throw new Error( `Legacy cocod daemon (PID ${pid}) did not stop within ${Math.round( timeoutMs / 1000, - )}s of SIGTERM. ` + - `Run 'kill ${pid}' and try again.`, + )}s of SIGTERM. ` + `Run 'kill ${pid}' and try again.`, ); } @@ -374,9 +419,11 @@ export function claimLegacyCocodPidFile( const openExclusive = options.openExclusive || ((path: string) => openSync(path, "wx", 0o600)); const writePid = - options.writePid || ((fd: number, ownerPid: number) => writeFileSync(fd, String(ownerPid))); + options.writePid || + ((fd: number, ownerPid: number) => writeFileSync(fd, String(ownerPid))); const closeFile = options.closeFile || closeSync; - const readFile = options.readFile || ((path: string) => readFileSync(path, "utf-8")); + const readFile = + options.readFile || ((path: string) => readFileSync(path, "utf-8")); const removeFile = options.removeFile || unlinkSync; const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning; @@ -400,7 +447,11 @@ export function claimLegacyCocodPidFile( ); } - if (!Number.isInteger(stalePid) || stalePid <= 0 || isProcessRunning(stalePid)) { + if ( + !Number.isInteger(stalePid) || + stalePid <= 0 || + isProcessRunning(stalePid) + ) { throw new Error( `Cannot claim the wallet process lock at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", @@ -444,7 +495,10 @@ export function claimLegacyCocodPidFile( } } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { - logger.warn(`Failed to release wallet process lock at ${pidFilePath}:`, error); + logger.warn( + `Failed to release wallet process lock at ${pidFilePath}:`, + error, + ); } } }; @@ -463,9 +517,11 @@ export async function createCocoClient( const configDir = options.configDir || CONFIG_DIR; const configFile = join(configDir, "config.json"); const dbPath = join(configDir, "coco.db"); - const socketPath = options.socketPath || + const socketPath = + options.socketPath || (options.configDir ? join(configDir, "cocod.sock") : LEGACY_COCOD_SOCKET); - const pidFilePath = options.pidFilePath || + const pidFilePath = + options.pidFilePath || (options.configDir ? join(configDir, "cocod.pid") : LEGACY_COCOD_PID_FILE); await assertLegacyCocodNotRunning({ socketPath, pidFilePath }); @@ -473,13 +529,14 @@ export async function createCocoClient( let database: Database | undefined; let coco: Awaited> | undefined; + let walletConfig = loadConfig(configFile); try { startupProgress("Opening Cashu wallet database..."); // Read and validate the existing cocod config during startup rather than // deferring failure until coco-core first needs wallet key material. - const seed = mnemonicToSeedSync(loadMnemonic(configFile)); + const seed = mnemonicToSeedSync(walletConfig.mnemonic); database = new Database(dbPath); const repo = new SqliteRepositories({ database }); await repo.init(); @@ -506,6 +563,25 @@ export async function createCocoClient( seedGetter: async () => seed, logger: createCocoLogger(), }); + + const trustedMints = await coco.mint.getAllTrustedMints(); + const configuredDefault = walletConfig.defaultMintUrl; + const defaultMintUrl = normalizeMintUrl( + configuredDefault || trustedMints[0]?.mintUrl || DEFAULT_MINT_URL, + ); + + if (!trustedMints.some((mint) => mint.mintUrl === defaultMintUrl)) { + startupProgress(`Adding default mint: ${defaultMintUrl}`); + await coco.mint.addMint(defaultMintUrl, { trusted: true }); + } + + // Persist only after the mint was successfully fetched and trusted. A failed + // network request must not leave config pointing at an unusable default. + walletConfig.defaultMintUrl = defaultMintUrl; + if (configuredDefault !== defaultMintUrl) { + saveConfig(walletConfig, configFile); + } + startupProgress("Cashu wallet ready."); } catch (error) { database?.close(); @@ -555,8 +631,7 @@ export async function createCocoClient( }, async receiveBolt11(amount: number, mintUrl?: string): Promise { - const mints = await coco.mint.getAllTrustedMints(); - const targetMint = mintUrl || mints[0]?.mintUrl; + const targetMint = mintUrl || walletConfig.defaultMintUrl; if (!targetMint) { throw new Error("No trusted mint available for Lightning invoice"); } @@ -572,8 +647,7 @@ export async function createCocoClient( }, async sendCashu(amount: number, mintUrl?: string): Promise { - const mints = await coco.mint.getAllTrustedMints(); - const targetMint = mintUrl || mints[0]?.mintUrl; + const targetMint = mintUrl || walletConfig.defaultMintUrl; if (!targetMint) { throw new Error("No trusted mint available for sending"); } @@ -586,8 +660,7 @@ export async function createCocoClient( }, async sendBolt11(invoice: string, mintUrl?: string): Promise { - const mints = await coco.mint.getAllTrustedMints(); - const targetMint = mintUrl || mints[0]?.mintUrl; + const targetMint = mintUrl || walletConfig.defaultMintUrl; if (!targetMint) { throw new Error("No trusted mint available for Lightning payment"); } @@ -606,12 +679,29 @@ export async function createCocoClient( }, async addMint(url: string): Promise { - await coco.mint.addMint(url, { trusted: true }); - return `Mint ${url} added successfully`; + const mintUrl = normalizeMintUrl(url); + await coco.mint.addMint(mintUrl, { trusted: true }); + return `Mint ${mintUrl} added successfully`; }, async getMintInfo(url: string): Promise { - return coco.mint.getMintInfo(url); + return coco.mint.getMintInfo(normalizeMintUrl(url)); + }, + + async getDefaultMint(): Promise { + return walletConfig.defaultMintUrl || null; + }, + + async setDefaultMint(url: string): Promise { + const mintUrl = normalizeMintUrl(url); + const trustedMints = await coco.mint.getAllTrustedMints(); + if (!trustedMints.some((mint) => mint.mintUrl === mintUrl)) { + await coco.mint.addMint(mintUrl, { trusted: true }); + } + + walletConfig.defaultMintUrl = mintUrl; + saveConfig(walletConfig, configFile); + return `Default mint set to ${mintUrl}`; }, async dispose(): Promise { diff --git a/src/daemon/wallet/cocod-client.ts b/src/daemon/wallet/cocod-client.ts index 5256d99..cb8db8e 100644 --- a/src/daemon/wallet/cocod-client.ts +++ b/src/daemon/wallet/cocod-client.ts @@ -57,6 +57,8 @@ export interface CocodClient { listMints(): Promise; addMint(url: string): Promise; getMintInfo(url: string): Promise; + getDefaultMint(): Promise; + setDefaultMint(url: string): Promise; /** Release resources held by in-process wallet implementations. */ dispose?(): Promise; getHistory(offset?: number, limit?: number): Promise; @@ -357,6 +359,12 @@ export function createCocodClient( async getMintInfo(url: string): Promise { return post("/mints/info", { url }); }, + async getDefaultMint(): Promise { + return callDaemon("/mints/default"); + }, + async setDefaultMint(url: string): Promise { + return post("/mints/default", { url }); + }, async getHistory(_offset?: number, _limit?: number): Promise { return []; }, diff --git a/src/daemon/wallet/index.ts b/src/daemon/wallet/index.ts index 4d2f96f..ce8123f 100644 --- a/src/daemon/wallet/index.ts +++ b/src/daemon/wallet/index.ts @@ -50,10 +50,11 @@ export async function createWalletAdapter( ); try { - const mints = await client.listMints(); - activeMintUrl = mints[0] || Object.keys(nextBalances)[0] || null; + // Use default mint as active mint, fall back to first mint in list + const defaultMint = await client.getDefaultMint(); + activeMintUrl = defaultMint || Object.keys(nextBalances)[0] || null; } catch (error) { - logger.error("Failed to list cocod mints:", error); + logger.error("Failed to get default mint:", error); if (!activeMintUrl) { activeMintUrl = Object.keys(nextBalances)[0] || null; } @@ -151,12 +152,12 @@ export async function createWalletAdapter( return { success: false, invoice: "", error: "NWC not connected" }; } - // Ensure we have an active mint - await syncMintState(); - const mintUrl = activeMintUrl; + // Use default mint for NWC funding + const defaultMint = await client.getDefaultMint(); + const mintUrl = defaultMint; if (!mintUrl) { - logger.error("[nwc] No active mint configured"); - return { success: false, invoice: "", error: "No active mint configured" }; + logger.error("[nwc] No default mint configured"); + return { success: false, invoice: "", error: "No default mint configured" }; } try { @@ -329,14 +330,14 @@ export async function createWalletAdapter( } try { - const [balances, mints] = await Promise.all([ + const [balances, defaultMint] = await Promise.all([ client.getBalances(), - client.listMints().catch(() => []), + client.getDefaultMint().catch(() => null), ]); mintUnits = Object.fromEntries( Object.keys(balances).map((mintUrl) => [mintUrl, "sat"]), ); - activeMintUrl = mints[0] || Object.keys(balances)[0] || null; + activeMintUrl = defaultMint || Object.keys(balances)[0] || null; } catch (error) { logger.error("Failed to initialize wallet adapter state:", error); } From 822f3fe003d09ae422de7cfed95579093c8c649b Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:42:04 +0100 Subject: [PATCH 23/32] refactor: use coco-core normalizeMintUrl instead of custom impl Replace the hand-rolled normalizeMintUrl with the one exported from @cashu/coco-core. The coco-core version is more complete (handles default port stripping, hostname case normalization) and is already applied internally on every mint operation. The custom validation provided only marginal value -- invalid URLs fail naturally when coco-core tries to fetch keysets. --- src/daemon/wallet/coco-client.ts | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index c17375d..ffd0805 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -1,4 +1,8 @@ -import { initializeCoco, getEncodedToken } from "@cashu/coco-core"; +import { + initializeCoco, + getEncodedToken, + normalizeMintUrl, +} from "@cashu/coco-core"; import type { HistoryEntry, Logger as CocoLogger } from "@cashu/coco-core"; import { SqliteRepositories } from "@cashu/coco-sqlite-bun"; import { Database } from "bun:sqlite"; @@ -164,22 +168,6 @@ function loadConfig(configFile: string = CONFIG_FILE): CocodConfig { return config; } -function normalizeMintUrl(url: string): string { - let parsed: URL; - try { - parsed = new URL(url); - } catch { - throw new Error(`Invalid mint URL: ${url}`); - } - if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { - throw new Error(`Mint URL must use HTTP or HTTPS: ${url}`); - } - parsed.hash = ""; - parsed.search = ""; - parsed.pathname = parsed.pathname.replace(/\/+$/, ""); - return parsed.toString().replace(/\/$/, ""); -} - function saveConfig( config: CocodConfig, configFile: string = CONFIG_FILE, From b94c0c7020f3491579f6805460f586843e56d244 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:10:34 +0100 Subject: [PATCH 24/32] feat: integrate NPC (npubx.cash) plugin into in-process coco wallet Register coco-cashu-plugin-npc with the in-process coco wallet so the daemon gets a persistent Lightning address (username@npubx.cash, or an npub fallback) whose paid quotes are synced into the wallet automatically via websocket, mirroring cocod's integration. - coco-client: derive the NPC Nostr signer from the wallet mnemonic (NIP-06), register NPCPlugin after initializeCoco, and expose getNpcAddress/setNpcUsername/syncNpc on the CocodClient interface. New options: enableNpc (default true) and npcBaseUrl. - cocod-client: add the same NPC methods to the legacy socket client as passthroughs to cocod's /npc/address and /npc/username routes, preserving cocod's 402 payment-required status. - http: add GET /wallet/npc/address, POST /wallet/npc/username (402 payment-required flow) and POST /wallet/npc/sync. - cli: add 'routstrd wallet npc address|username [--confirm]|sync'. - tests: mock NPC plugin exercises registration, address formatting, username 402 mapping and sync through the real coco plugin host; legacy passthrough tests; migration test opts out of NPC. --- README.md | 22 ++ bun.lock | 14 ++ package.json | 2 + src/cli.ts | 31 +++ src/daemon/http/index.ts | 45 ++++ src/daemon/wallet/coco-client.npc.test.ts | 244 ++++++++++++++++++++++ src/daemon/wallet/coco-client.test.ts | 12 +- src/daemon/wallet/coco-client.ts | 93 ++++++++- src/daemon/wallet/cocod-client.ts | 63 ++++++ 9 files changed, 521 insertions(+), 5 deletions(-) create mode 100644 src/daemon/wallet/coco-client.npc.test.ts diff --git a/README.md b/README.md index 4b85baf..f944210 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,28 @@ Stop the daemon: routstrd stop ``` +### NPC (Lightning Address) + +The in-process wallet registers the NPC (npubx.cash) plugin, which gives the +daemon a persistent Lightning address backed by the wallet's Cashu mints. +Payments to the address are imported into the wallet automatically (websocket +push, plus manual sync on demand). + +```sh +# Show your NPC Lightning address (username@npubx.cash, or npub fallback) +routstrd wallet npc address + +# Claim a username (quote first, then confirm to pay the claim fee from the wallet) +routstrd wallet npc username myname +routstrd wallet npc username myname --confirm + +# Manually sync paid NPC quotes into the wallet +routstrd wallet npc sync +``` + +Equivalent daemon endpoints: `GET /wallet/npc/address`, +`POST /wallet/npc/username`, `POST /wallet/npc/sync`. + ### Daemon API The daemon exposes an HTTP server (default port 8008) with the following endpoints: diff --git a/bun.lock b/bun.lock index ab16ca5..4b66c67 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,8 @@ "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", + "coco-cashu-core": "1.1.2-rc.50", + "coco-cashu-plugin-npc": "2.4.1", "commander": "^14.0.2", "nostr-tools": "^2.12.0", "qrcode": "^1.5.4", @@ -345,6 +347,10 @@ "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + "coco-cashu-core": ["coco-cashu-core@1.1.2-rc.50", "", { "dependencies": { "@cashu/cashu-ts": "^3.5.0", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "@scure/bip32": "^2.0.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-eK5YuwvCWpeCwF/GEkMo90FCyek2mS+smQ51wKjkjTKzpkVVlNln52l2h1Wwce4VMnW6GSeL2IgT8IksM8Umuw=="], + + "coco-cashu-plugin-npc": ["coco-cashu-plugin-npc@2.4.1", "", { "dependencies": { "npubcash-sdk": "^0.3.2" }, "peerDependencies": { "coco-cashu-core": "^1.1.2-rc.50", "typescript": "^5.0.0" } }, "sha512-0OhqXXRLBq/2VkeuyqHrTINCEQYEk9+dCD9Vq7+M94CDf7kLKQZmoDL6leTuHJ45pIVnALcOFnjIcRoJwV7iiA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -493,6 +499,10 @@ "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], + "npubcash-sdk": ["npubcash-sdk@0.3.2", "", { "dependencies": { "@cashu/cashu-ts": "^3.2.2", "nostr-tools": "^2.19.4", "npubcash-types": "^0.1.1" } }, "sha512-mJwvWW6rAq04b/AHbd76T4gTeUqP2uprc01Y0ToU9ZK5co3n1PmBKWLBLbPN+VgpHhE0LubzjLq/qm70YWUugA=="], + + "npubcash-types": ["npubcash-types@0.1.1", "", {}, "sha512-/HGfes2cvQpkrWOuUrdemJJhJUQu+xiXl4x+AxQtKUMGCB8uEhfrXJfAQ0n+DgtkIX1YVLOh0a/e6E5LX+fUew=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], @@ -669,6 +679,8 @@ "applesauce-wallet-connect/applesauce-core": ["applesauce-core@6.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-O6AlVyzqcuIhTOIuexm6UWmx7mRIa2D98gJP7K7vFGf90YdtvSH78AsKQWZXJ0Pk/K14at+9zkwfCkFr7ghgNw=="], + "coco-cashu-core/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.1", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-EdUdjR6D0eY1YT6irNh/d5SBRc/ug4WNpnlLBXbRI9iOb8UNnQeGfk9CJQWL9ElQMjQ/4g582SehKXzEAZ2vnw=="], + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "light-bolt11-decoder/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="], @@ -683,6 +695,8 @@ "nostr-tools/@scure/bip39": ["@scure/bip39@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg=="], + "npubcash-sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.1", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-EdUdjR6D0eY1YT6irNh/d5SBRc/ug4WNpnlLBXbRI9iOb8UNnQeGfk9CJQWL9ElQMjQ/4g582SehKXzEAZ2vnw=="], + "postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], diff --git a/package.json b/package.json index 1078e0d..bcd11b4 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", + "coco-cashu-core": "1.1.2-rc.50", + "coco-cashu-plugin-npc": "2.4.1", "commander": "^14.0.2", "nostr-tools": "^2.12.0", "qrcode": "^1.5.4", diff --git a/src/cli.ts b/src/cli.ts index 68f8ec9..2222e2a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1576,6 +1576,37 @@ walletMintsCmd }); }); +const walletNpcCmd = walletCmd + .command("npc") + .description("NPC (npubx.cash) Lightning address operations"); + +walletNpcCmd + .command("address") + .description("Show this wallet's NPC Lightning address") + .action(async () => { + await handleDaemonCommand("/wallet/npc/address"); + }); + +walletNpcCmd + .command("username ") + .description( + "Claim an NPC username (pays the claim fee from the wallet with --confirm)", + ) + .option("--confirm", "Confirm payment of the username claim fee") + .action(async (name: string, options: { confirm?: boolean }) => { + await handleDaemonCommand("/wallet/npc/username", { + method: "POST", + body: { username: name, confirm: options.confirm === true }, + }); + }); + +walletNpcCmd + .command("sync") + .description("Manually sync paid NPC quotes into the wallet") + .action(async () => { + await handleDaemonCommand("/wallet/npc/sync", { method: "POST" }); + }); + // ── NWC (Nostr Wallet Connect) commands ───────────────────────── const nwcCmd = program diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index e730fc4..0e6401b 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -490,6 +490,51 @@ export function createDaemonRequestHandler(deps: { return; } + // ── NPC (npubx.cash) Lightning address endpoints ────────────── + + if (req.method === "GET" && url.pathname === "/wallet/npc/address") { + await respond(res, async () => { + const info = await deps.walletClient.getNpcAddress(); + return { output: info }; + }); + return; + } + + if (req.method === "POST" && url.pathname === "/wallet/npc/username") { + await respond(res, async () => { + const body = await readJsonBody(req); + const username = getRequiredStringField(body, "username"); + const confirm = body.confirm === true; + const result = await deps.walletClient.setNpcUsername(username, confirm); + if (!result.success) { + const pr = result.paymentRequest ?? {}; + const amount = typeof pr.amount === "number" ? pr.amount : 0; + const mints = Array.isArray(pr.mints) ? pr.mints.join(", ") : ""; + if (confirm) { + throw new CocodHttpError( + 402, + `Failed to set username. Required amount: ${amount} SATS. Required mints: ${mints}`, + ); + } + throw new CocodHttpError( + 402, + `Payment required to set username: ${amount} SATS. ` + + `Use 'routstrd wallet npc username ${username} --confirm' to proceed`, + ); + } + return { output: { success: true, username } }; + }); + return; + } + + if (req.method === "POST" && url.pathname === "/wallet/npc/sync") { + await respond(res, async () => { + await deps.walletClient.syncNpc(); + return { output: { message: "NPC sync completed" } }; + }); + return; + } + // ── NWC endpoints ───────────────────────────────────────────── if (req.method === "GET" && url.pathname === "/nwc/status") { diff --git a/src/daemon/wallet/coco-client.npc.test.ts b/src/daemon/wallet/coco-client.npc.test.ts new file mode 100644 index 0000000..d348473 --- /dev/null +++ b/src/daemon/wallet/coco-client.npc.test.ts @@ -0,0 +1,244 @@ +import { afterEach, describe, expect, it, mock } from "bun:test"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; + +// The real NPCPlugin opens a websocket to npubx.cash once the wallet is ready. +// Mock the module before importing coco-client so tests never touch the network. +const npcState = { + info: { name: "alice" as string | null, pubkey: "ab".repeat(32) }, + usernameResult: { success: true } as + | { success: true } + | { success: false; pr: { amount: number; mints: string[] } }, + setUsernameCalls: [] as Array<{ username: string; attemptPayment?: boolean }>, + syncCalls: 0, +}; + +class MockNPCPlugin { + readonly name = "npc"; + readonly required = [] as const; + private readonly api = { + getInfo: async () => npcState.info, + setUsername: async (username: string, attemptPayment?: boolean) => { + npcState.setUsernameCalls.push({ username, attemptPayment }); + return npcState.usernameResult; + }, + sync: async () => { + npcState.syncCalls += 1; + }, + }; + + onInit(ctx: { registerExtension: (key: string, api: unknown) => void }) { + ctx.registerExtension("npc", this.api); + return () => {}; + } +} + +mock.module("coco-cashu-plugin-npc", () => ({ + NPCPlugin: MockNPCPlugin, +})); + +const { createCocoClient } = await import("./coco-client"); +const { createCocodClient, CocodHttpError } = await import("./cocod-client"); + +const TEST_MNEMONIC = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + +const tempDirs: string[] = []; + +function makeWalletDir(): string { + const dir = mkdtempSync(join(tmpdir(), "routstrd-coco-npc-test-")); + tempDirs.push(dir); + const walletDir = join(dir, ".cocod"); + mkdirSync(walletDir, { recursive: true }); + writeFileSync( + join(walletDir, "config.json"), + JSON.stringify({ version: 1, mnemonic: TEST_MNEMONIC, encrypted: false }), + ); + return walletDir; +} + +function resetNpcState(): void { + npcState.info = { name: "alice", pubkey: "ab".repeat(32) }; + npcState.usernameResult = { success: true }; + npcState.setUsernameCalls = []; + npcState.syncCalls = 0; +} + +afterEach(() => { + resetNpcState(); + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +describe("in-process coco client NPC integration", () => { + it("registers the plugin and exposes the wallet's NPC lightning address", async () => { + const client = await createCocoClient({ configDir: makeWalletDir() }); + try { + const info = await client.getNpcAddress(); + expect(info).toEqual({ + address: "alice@npubx.cash", + name: "alice", + pubkey: "ab".repeat(32), + }); + } finally { + await client.dispose?.(); + } + }); + + it("falls back to the npub address when no username is set", async () => { + npcState.info = { name: null, pubkey: "ab".repeat(32) }; + const client = await createCocoClient({ configDir: makeWalletDir() }); + try { + const info = await client.getNpcAddress(); + expect(info.address).toStartWith("npub1"); + expect(info.address).toEndWith("@npubx.cash"); + expect(info.name).toBeUndefined(); + expect(info.pubkey).toBe("ab".repeat(32)); + } finally { + await client.dispose?.(); + } + }); + + it("maps payment-required username claims without paying", async () => { + npcState.usernameResult = { + success: false, + pr: { amount: 21, mints: ["https://mint.example.com"] }, + }; + const client = await createCocoClient({ configDir: makeWalletDir() }); + try { + const result = await client.setNpcUsername("bob"); + expect(result).toEqual({ + success: false, + paymentRequest: { amount: 21, mints: ["https://mint.example.com"] }, + }); + expect(npcState.setUsernameCalls).toEqual([ + { username: "bob", attemptPayment: false }, + ]); + } finally { + await client.dispose?.(); + } + }); + + it("confirms the claim fee payment when requested", async () => { + const client = await createCocoClient({ configDir: makeWalletDir() }); + try { + const result = await client.setNpcUsername("bob", true); + expect(result).toEqual({ success: true }); + expect(npcState.setUsernameCalls).toEqual([ + { username: "bob", attemptPayment: true }, + ]); + } finally { + await client.dispose?.(); + } + }); + + it("triggers a manual quote sync", async () => { + const client = await createCocoClient({ configDir: makeWalletDir() }); + try { + await client.syncNpc(); + expect(npcState.syncCalls).toBe(1); + } finally { + await client.dispose?.(); + } + }); + + it("rejects NPC calls with a clear error when the plugin is disabled", async () => { + const client = await createCocoClient({ + configDir: makeWalletDir(), + enableNpc: false, + }); + try { + await expect(client.getNpcAddress()).rejects.toThrow( + "NPC plugin is not enabled", + ); + await expect(client.syncNpc()).rejects.toThrow( + "NPC plugin is not enabled", + ); + } finally { + await client.dispose?.(); + } + }); +}); + +describe("legacy cocod client NPC passthroughs", () => { + type FetchImpl = ( + input: string | URL | Request, + init?: RequestInit & { unix: string }, + ) => Promise; + + function legacyClient(handler: (path: string, body?: unknown) => Response) { + const requests: Array<{ path: string; body?: unknown }> = []; + const fetchImpl: FetchImpl = async (input, init) => { + const path = String(input).replace("http://localhost", ""); + const body = + typeof init?.body === "string" ? JSON.parse(init.body) : undefined; + if (path !== "/ping") requests.push({ path, body }); + if (path === "/ping") { + return new Response(JSON.stringify({ output: "pong" })); + } + return handler(path, body); + }; + const client = createCocodClient({ + socketPath: "/tmp/routstrd-test-legacy-npc/cocod.sock", + fetchImpl, + }); + return { client, requests }; + } + + it("fetches and parses a username NPC address", async () => { + const { client, requests } = legacyClient( + () => new Response(JSON.stringify({ output: "alice@npubx.cash" })), + ); + const info = await client.getNpcAddress(); + expect(info).toEqual({ address: "alice@npubx.cash", name: "alice" }); + expect(requests[0]?.path).toBe("/npc/address"); + }); + + it("omits the name for npub fallback addresses", async () => { + const { client } = legacyClient( + () => + new Response(JSON.stringify({ output: "npub1abc123@npubx.cash" })), + ); + const info = await client.getNpcAddress(); + expect(info).toEqual({ address: "npub1abc123@npubx.cash" }); + }); + + it("posts username claims with the confirm flag", async () => { + const { client, requests } = legacyClient( + () => new Response(JSON.stringify({ output: { success: true } })), + ); + const result = await client.setNpcUsername("bob", true); + expect(result).toEqual({ success: true }); + expect(requests[0]).toEqual({ + path: "/npc/username", + body: { username: "bob", confirm: true }, + }); + }); + + it("preserves the 402 payment-required error from cocod", async () => { + const { client } = legacyClient( + () => + new Response( + JSON.stringify({ error: "Payment required to set username" }), + { status: 402 }, + ), + ); + try { + await client.setNpcUsername("bob"); + expect.unreachable("should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(CocodHttpError); + expect((error as InstanceType).status).toBe(402); + expect((error as Error).message).toContain("Payment required"); + } + }); + + it("reports manual sync as unsupported", async () => { + const { client } = legacyClient( + () => new Response(JSON.stringify({ output: {} })), + ); + await expect(client.syncNpc()).rejects.toThrow("not supported"); + }); +}); diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts index cd74fbd..8ce421e 100644 --- a/src/daemon/wallet/coco-client.test.ts +++ b/src/daemon/wallet/coco-client.test.ts @@ -161,7 +161,12 @@ describe("legacy cocod wallet migration", () => { ); writeFileSync(join(walletDir, "coco.db"), gunzipSync(fixture)); - const client = await createCocoClient({ configDir: walletDir }); + // NPC is disabled here: this test verifies database migration, and the + // real plugin would otherwise open a websocket to npubx.cash. + const client = await createCocoClient({ + configDir: walletDir, + enableNpc: false, + }); try { expect(await client.getStatus()).toBe("UNLOCKED"); expect(await client.getBalances()).toEqual({ [mintUrl]: 10 }); @@ -171,7 +176,10 @@ describe("legacy cocod wallet migration", () => { // Prove the migrated schema remains reopenable and the pre-existing // proofs survive a complete in-process wallet restart. - const reopenedClient = await createCocoClient({ configDir: walletDir }); + const reopenedClient = await createCocoClient({ + configDir: walletDir, + enableNpc: false, + }); try { expect(await reopenedClient.getBalances()).toEqual({ [mintUrl]: 10 }); } finally { diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index ffd0805..2b351a3 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -3,9 +3,16 @@ import { getEncodedToken, normalizeMintUrl, } from "@cashu/coco-core"; -import type { HistoryEntry, Logger as CocoLogger } from "@cashu/coco-core"; +import type { + HistoryEntry, + Logger as CocoLogger, + Plugin as CocoPlugin, +} from "@cashu/coco-core"; import { SqliteRepositories } from "@cashu/coco-sqlite-bun"; import { Database } from "bun:sqlite"; +import { NPCPlugin, type PluginApi as NpcPluginApi } from "coco-cashu-plugin-npc"; +import { privateKeyFromSeedWords } from "nostr-tools/nip06"; +import { finalizeEvent, nip19, type EventTemplate } from "nostr-tools"; import { closeSync, existsSync, @@ -17,9 +24,16 @@ import { } from "fs"; import { join } from "path"; import { mnemonicToSeedSync } from "@scure/bip39"; -import type { CocodClient, CocodState } from "./cocod-client"; +import type { + CocodClient, + CocodState, + NpcAddress, + NpcUsernameResult, +} from "./cocod-client"; import { logger } from "../../utils/logger"; +const NPC_DEFAULT_BASE_URL = "https://npubx.cash"; + const CONFIG_DIR = process.env.COCOD_DIR || `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`; @@ -497,6 +511,10 @@ export interface CreateCocoClientOptions { configDir?: string; socketPath?: string; pidFilePath?: string; + /** Set to false to skip NPC (npubx.cash) plugin registration. Default: true. */ + enableNpc?: boolean; + /** NPC server base URL. Default: https://npubx.cash */ + npcBaseUrl?: string; } export async function createCocoClient( @@ -511,6 +529,8 @@ export async function createCocoClient( const pidFilePath = options.pidFilePath || (options.configDir ? join(configDir, "cocod.pid") : LEGACY_COCOD_PID_FILE); + const npcBaseUrl = options.npcBaseUrl || NPC_DEFAULT_BASE_URL; + const npcAddressDomain = new URL(npcBaseUrl).host; await assertLegacyCocodNotRunning({ socketPath, pidFilePath }); const releaseLegacyPidClaim = claimLegacyCocodPidFile({ pidFilePath }); @@ -524,7 +544,8 @@ export async function createCocoClient( // Read and validate the existing cocod config during startup rather than // deferring failure until coco-core first needs wallet key material. - const seed = mnemonicToSeedSync(walletConfig.mnemonic); + const mnemonic = walletConfig.mnemonic; + const seed = mnemonicToSeedSync(mnemonic); database = new Database(dbPath); const repo = new SqliteRepositories({ database }); await repo.init(); @@ -570,6 +591,27 @@ export async function createCocoClient( saveConfig(walletConfig, configFile); } + if (options.enableNpc !== false) { + startupProgress("Registering NPC (npubx.cash) plugin..."); + // NPC authenticates with a Nostr key derived from the same wallet seed + // (NIP-06). The signer only produces JWT auth events for the NPC + // server; it never signs anything that moves funds by itself. + const npcSecretKey = privateKeyFromSeedWords(mnemonic); + const npcSigner = async (template: EventTemplate) => + finalizeEvent(template, npcSecretKey); + const npcPlugin = new NPCPlugin(npcBaseUrl, npcSigner, { + useWebsocket: true, + logger: createCocoLogger({ module: "npc" }), + }); + // coco-cashu-plugin-npc implements the plugin contract from the + // coco-cashu-core package while routstrd runs the equivalent + // @cashu/coco-core build. The plugin host API is structurally identical + // in both (verified: mintService.addMintByUrl, + // mintOperationService.importQuote/getOperationByQuote), so this cast + // only bridges the duplicate package names, not a real API gap. + coco.use(npcPlugin as unknown as CocoPlugin); + } + startupProgress("Cashu wallet ready."); } catch (error) { database?.close(); @@ -577,6 +619,18 @@ export async function createCocoClient( throw error; } + const npcApi = (): NpcPluginApi => { + // The plugin augments coco-cashu-core's PluginExtensions; the equivalent + // registration lives on manager.ext here. Guard for enableNpc=false. + const api = coco + ? (coco.ext as { npc?: NpcPluginApi }).npc + : undefined; + if (!api) { + throw new Error("NPC plugin is not enabled for this wallet."); + } + return api; + }; + let disposed = false; return { async ping(): Promise { @@ -709,5 +763,38 @@ export async function createCocoClient( async getHistory(offset?: number, limit?: number): Promise { return coco.history.getPaginatedHistory(offset, limit); }, + + async getNpcAddress(): Promise { + const info = await npcApi().getInfo(); + const name = + typeof info?.name === "string" && info.name.trim() + ? info.name.trim() + : undefined; + const localPart = name ?? nip19.npubEncode(info.pubkey); + return { + address: `${localPart}@${npcAddressDomain}`, + ...(name ? { name } : {}), + pubkey: info.pubkey, + }; + }, + + async setNpcUsername( + username: string, + confirm?: boolean, + ): Promise { + const result = await npcApi().setUsername(username, confirm === true); + if (result.success) { + return { success: true }; + } + return { + success: false, + paymentRequest: + result.pr as NpcUsernameResult["paymentRequest"], + }; + }, + + async syncNpc(): Promise { + await npcApi().sync(); + }, }; } diff --git a/src/daemon/wallet/cocod-client.ts b/src/daemon/wallet/cocod-client.ts index cb8db8e..262f9b3 100644 --- a/src/daemon/wallet/cocod-client.ts +++ b/src/daemon/wallet/cocod-client.ts @@ -35,6 +35,27 @@ export type CocodState = "UNINITIALIZED" | "LOCKED" | "UNLOCKED" | "ERROR"; export type CocodBalanceOutput = Record; +/** NPC (npubx.cash) Lightning address details for this wallet. */ +export interface NpcAddress { + /** Full Lightning address, e.g. "alice@npubx.cash" (npub fallback when no username is set). */ + address: string; + /** NPC username, when one has been claimed. */ + name?: string; + /** Nostr hex pubkey of the NPC account (only available from the in-process wallet). */ + pubkey?: string; +} + +/** Result of an NPC username claim attempt. */ +export interface NpcUsernameResult { + success: boolean; + /** Present when NPC requires payment to claim the username. */ + paymentRequest?: { + amount?: number; + mints?: string[]; + [key: string]: unknown; + }; +} + export class CocodHttpError extends Error { status: number; @@ -62,6 +83,12 @@ export interface CocodClient { /** Release resources held by in-process wallet implementations. */ dispose?(): Promise; getHistory(offset?: number, limit?: number): Promise; + /** NPC (npubx.cash) Lightning address for this wallet. */ + getNpcAddress(): Promise; + /** Claim an NPC username; pass confirm=true to pay the claim fee from the wallet. */ + setNpcUsername(username: string, confirm?: boolean): Promise; + /** Manually trigger an NPC quote sync into the wallet. */ + syncNpc(): Promise; } export function resolveCocodExecutable(cocodPath?: string | null): string { @@ -368,5 +395,41 @@ export function createCocodClient( async getHistory(_offset?: number, _limit?: number): Promise { return []; }, + async getNpcAddress(): Promise { + const address = await callDaemon("/npc/address"); + if (typeof address !== "string" || !address.trim()) { + throw new CocodHttpError( + 502, + "Unexpected response from cocod while fetching NPC address.", + ); + } + const trimmed = address.trim(); + const localPart = trimmed.split("@")[0]; + return { + address: trimmed, + ...(localPart && !localPart.startsWith("npub1") + ? { name: localPart } + : {}), + }; + }, + async setNpcUsername( + username: string, + confirm?: boolean, + ): Promise { + // cocod answers 402 with a payment-required message when the claim fee + // has not been confirmed; fetchJson preserves that status on the thrown + // CocodHttpError, so callers can surface it unchanged. + const result = await post<{ success?: boolean }>("/npc/username", { + username, + confirm: confirm === true, + }); + return { success: result?.success !== false }; + }, + async syncNpc(): Promise { + throw new CocodHttpError( + 501, + "Manual NPC sync is not supported by the legacy cocod client.", + ); + }, }; } From 5d9d0e97062ba7fa0369f9cbd745fc2a6f16e2a0 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:56:07 +0100 Subject: [PATCH 25/32] fix: use published @routstr/sdk ^0.3.19 instead of local file:// path Replaced "file://../routstr-sdk" with the published "^0.3.19" range so the dependency resolves from npm for CI, other contributors, and publish. The local file:// path only worked with a sibling routstr-sdk checkout present. Verified @routstr/sdk@0.3.19 on npm contains the APIs this branch relies on (getProviderPriceRankingForModel, includeDisabled). --- bun.lock | 316 +-------------------------------------------------- package.json | 2 +- 2 files changed, 3 insertions(+), 315 deletions(-) diff --git a/bun.lock b/bun.lock index 4b66c67..d8caf08 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "@cashu/cashu-ts": "^4.3.0", "@cashu/coco-core": "^1.0.1", "@cashu/coco-sqlite-bun": "^1.0.1", - "@routstr/sdk": "file://../routstr-sdk", + "@routstr/sdk": "^0.3.19", "@scure/bip39": "^2.2.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", @@ -49,72 +49,12 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], - "@freedomofpress/crypto-browser": ["@freedomofpress/crypto-browser@0.1.7", "", { "dependencies": { "@noble/curves": "^1.6.0" } }, "sha512-zjWmZDKdAu8g0Zq1IjBQ+sKQ/NpfzStBDFjy/qHUSMVEL4wNlNGtA7lhtw8v8asXa0yqF2QTYQ3rq6xCTQeADw=="], "@freedomofpress/sigstore-browser": ["@freedomofpress/sigstore-browser@0.1.14", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/tuf-browser": "^0.1.11", "@noble/curves": "^2.0.1" } }, "sha512-1dqc7HojiBcr/sJSAXjBwNz4+WGeeAJP8ENQnDxm+idVV0/YIxpfwXRNSJdNw6EXJuHviq/5kSPEBCxamPcrpQ=="], "@freedomofpress/tuf-browser": ["@freedomofpress/tuf-browser@0.1.11", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7" } }, "sha512-d76ohB/AS5+zI+lnbiFMX/BIK3nT18hZ8q3Na6X4vyYaSYjXkeknzhAnbx1da+8ObWLwsaZLue46haEch28qtQ=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@libsql/client": ["@libsql/client@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="], "@libsql/core": ["@libsql/core@0.15.15", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA=="], @@ -155,93 +95,9 @@ "@noble/post-quantum": ["@noble/post-quantum@0.6.1", "", { "dependencies": { "@noble/ciphers": "~2.2.0", "@noble/curves": "~2.2.0", "@noble/hashes": "~2.2.0" } }, "sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A=="], - "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], - "@panva/hpke-noble": ["@panva/hpke-noble@1.1.3", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-zPG7MR9x7QE7+KdYsKBO9H0vp3AdYt9/4AT3ab7T7W6SL0fdRqhgNRu8q4OGTJNLeKpdbkkRb6LhBDaA9+9xWQ=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], - - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], - - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], - - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], - - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], - - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], - - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], - - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], - - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], - - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], - - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], - - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], - - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], - - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], - - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], - - "@routstr/sdk": ["@routstr/sdk@file:../routstr-sdk", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "joycon": "^3.1.1", "tsup": "^8.5.1", "typescript": "^5.0.0", "vitest": "^4.1.9" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }], + "@routstr/sdk": ["@routstr/sdk@0.3.19", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-48HsKUxzINDRXohFC55ud/x7gmz6rkUB15LZEOgoiLyIkD1L7nfTcwY2B91c1nhrhWVg0sP0W3wYXIMidmaUHg=="], "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], @@ -271,44 +127,18 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], - - "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], - - "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], - - "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], - - "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], - - "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], - - "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - "applesauce-common": ["applesauce-common@6.2.0", "", { "dependencies": { "@scure/base": "^2.2.0", "applesauce-core": "^6.2.0", "hash-sum": "^2.0.0", "light-bolt11-decoder": "^3.2.0", "nanoid": "^5.0.9", "rxjs": "^7.8.1" } }, "sha512-meXbDCdqA1LKy1tr04BW7wMfnBBkHetLv+wlXYjAj5iR4Sji5KMxLFdAjUe6cSvrxrk9LxFcP+0P9BcZQxjpaA=="], "applesauce-core": ["applesauce-core@5.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-aSuM6q6/Gs2FGUqytlHDjKZpSst2xKaT0vMXUQFWUctECNIxvwy6/hTDDInukMuI9mrQdjnO781ZJJgghI7RNw=="], @@ -319,8 +149,6 @@ "applesauce-wallet-connect": ["applesauce-wallet-connect@6.2.0", "", { "dependencies": { "@noble/hashes": "^2.2.0", "applesauce-common": "^6.2.0", "applesauce-core": "^6.2.0", "rxjs": "^7.8.1" } }, "sha512-vI3FD3Oyil5QtEM1IRx1Q/lWEfmXMHHO4YsHRUtUx3fcid4uG4xe2KIKsMpduGH9t46Bxz4k+vjp2WBHtkgT1g=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.11.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA=="], @@ -333,16 +161,8 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], - - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], @@ -357,12 +177,6 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -383,36 +197,22 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], - - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], @@ -429,8 +229,6 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - "js-base64": ["js-base64@3.8.1", "", {}, "sha512-5xVjhUZlHHeuO2W7w2rDFj/Kl1xLX+HjZxdOQwCsUOifl6UaoH1o1wsbsTMz+r0aeC7gCijvru02j6TfKZWzKg=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], @@ -439,52 +237,16 @@ "light-bolt11-decoder": ["light-bolt11-decoder@3.2.0", "", { "dependencies": { "@scure/base": "1.1.1" } }, "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ=="], - "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], - - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], - "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], @@ -503,10 +265,6 @@ "npubcash-types": ["npubcash-types@0.1.1", "", {}, "sha512-/HGfes2cvQpkrWOuUrdemJJhJUQu+xiXl4x+AxQtKUMGCB8uEhfrXJfAQ0n+DgtkIX1YVLOh0a/e6E5LX+fUew=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "openai": ["openai@6.46.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-DFg6jEPT2RO+oAyXtddeUJU8zkGy1OQ1AjGzNIJUMQG03TTqvCpy9tBpQ+2VVVnvrl3E56F8GEin2JYtWpITtA=="], @@ -519,22 +277,8 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], - - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - "postcss": ["postcss@8.5.22", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ=="], - - "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], @@ -547,18 +291,10 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - - "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], - - "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -567,20 +303,10 @@ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -589,54 +315,26 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "tinfoil": ["tinfoil@1.1.8", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.41", "@freedomofpress/sigstore-browser": "^0.1.13", "@tinfoilsh/verifier": "1.1.8", "@types/ws": "^8.18.1", "ehbp": "^0.2.0", "openai": "^6.34.0", "ws": "^8.21.0" }, "peerDependencies": { "ai": "^6.0.168" }, "optionalPeers": ["ai"] }, "sha512-wD6DlDwnzquRqQcmfUNVYdQWM1kdzESwt/ksta2R78+9Zuk7NNQb2HinCkROJ0AZCHJZN0vHpQr/n98SxS3H7A=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], - "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="], - - "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -665,8 +363,6 @@ "@routstr/sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.1", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-EdUdjR6D0eY1YT6irNh/d5SBRc/ug4WNpnlLBXbRI9iOb8UNnQeGfk9CJQWL9ElQMjQ/4g582SehKXzEAZ2vnw=="], - "@routstr/sdk/@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], - "applesauce-common/applesauce-core": ["applesauce-core@6.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-O6AlVyzqcuIhTOIuexm6UWmx7mRIa2D98gJP7K7vFGf90YdtvSH78AsKQWZXJ0Pk/K14at+9zkwfCkFr7ghgNw=="], "applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], @@ -697,16 +393,8 @@ "npubcash-sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.1", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-EdUdjR6D0eY1YT6irNh/d5SBRc/ug4WNpnlLBXbRI9iOb8UNnQeGfk9CJQWL9ElQMjQ/4g582SehKXzEAZ2vnw=="], - "postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], - - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "@freedomofpress/crypto-browser/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@routstr/sdk/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "applesauce-common/applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], "applesauce-core/nostr-tools/@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="], diff --git a/package.json b/package.json index bcd11b4..702db88 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@cashu/cashu-ts": "^4.3.0", "@cashu/coco-core": "^1.0.1", "@cashu/coco-sqlite-bun": "^1.0.1", - "@routstr/sdk": "file://../routstr-sdk", + "@routstr/sdk": "^0.3.19", "@scure/bip39": "^2.2.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", From e87860be9f4a0aed0ef1b2a67f9d1bf97bdf5d6e Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:32:51 +0100 Subject: [PATCH 26/32] feat(wallet): migrate cocod data to routstrd wallet directory --- IMPLEMENTATION.md | 253 ++++++++++++++++++++++ README.md | 11 + SKILL.md | 4 +- src/cli.test.ts | 4 +- src/cli.ts | 48 +++- src/daemon/index.ts | 32 ++- src/daemon/wallet/coco-client.npc.test.ts | 22 +- src/daemon/wallet/coco-client.test.ts | 44 +++- src/daemon/wallet/coco-client.ts | 93 +++++--- src/daemon/wallet/migration.test.ts | 107 +++++++++ src/daemon/wallet/migration.ts | 136 ++++++++++++ src/daemon/wallet/paths.ts | 33 +++ src/start-daemon.ts | 1 - 13 files changed, 721 insertions(+), 67 deletions(-) create mode 100644 IMPLEMENTATION.md create mode 100644 src/daemon/wallet/migration.test.ts create mode 100644 src/daemon/wallet/migration.ts create mode 100644 src/daemon/wallet/paths.ts diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md new file mode 100644 index 0000000..9e1f571 --- /dev/null +++ b/IMPLEMENTATION.md @@ -0,0 +1,253 @@ +# Migrate wallet storage from `~/.cocod` to `~/.routstrd/wallet` + +Task: [`routstrd-migrate-cocod-to-routstrd-files`](https://github.com/nodestrich/routstrd) +Event ID: `2f420644005154c5b106017c4a753854bb2660ba834cde7e6e267532cb5448e9` +Priority: 997 · Status: open + +--- + +## Baseline + +This plan is based on `coco-integration` at `86ac688` (after the default-mint, NPC/npubx.cash, and Windows-support changes). + +The task branch is currently based on `04f8c22`; rebase it onto the latest `coco-integration` before implementing this plan. + +## Goal + +Move the in-process Cashu wallet's persistent data from: + +```text +~/.cocod/config.json +~/.cocod/coco.db +``` + +to: + +```text +~/.routstrd/wallet/config.json +~/.routstrd/wallet/coco.db +``` + +The migration must preserve the mnemonic, proofs, default mint, and NPC identity. Existing users must not accidentally get a new wallet, and `cocod` and routstrd must never open the same database concurrently. + +Fresh installs must use `~/.routstrd/wallet` immediately. Existing installs must be migrated automatically and safely; merely continuing to run from `~/.cocod` is not sufficient for this task. + +--- + +## Important corrections to the previous plan + +### 1. Do not rename or migrate `cocod.sock` + +The in-process `coco-core` wallet does not expose a Unix socket. `cocod.sock` belongs to the legacy external `cocod` daemon and is used only to determine whether that daemon is still alive. + +There should therefore be no `wallet.sock`. Do not copy a socket inode into the new directory. + +### 2. Keep legacy-process paths separate from wallet-data paths + +On the current base, `src/daemon/wallet/coco-client.ts` derives all of these from one `CONFIG_DIR`: + +- wallet config and database; +- legacy cocod socket; +- legacy cocod PID/process lock. + +That coupling must be removed. After migration: + +- wallet data comes from `~/.routstrd/wallet`; +- probes and shutdown logic for an external legacy cocod continue to use `~/.cocod/cocod.sock` and `~/.cocod/cocod.pid`; +- routstrd uses `~/.routstrd/wallet/wallet.pid` as the in-process wallet lock. + +While routstrd owns the migrated wallet, it should also retain the existing legacy-cocod exclusion mechanism (claiming the legacy `cocod.pid`) so an old cocod cannot be started against a stale or partially migrated legacy wallet. Acquisition and release of both locks must be rollback-safe. + +### 3. Do not rename `cocodPath` + +`RoutstrdConfig.cocodPath` is an executable path for the external compatibility client, not a wallet storage path. Renaming it to `walletPath` would change its meaning and would not help this migration. + +The current daemon creates `createCocoClient()` directly and injects it into `createWalletAdapter()`, so `cocodPath` is effectively bypassed on the normal in-process path. Cleanup or removal of that compatibility setting is a separate task. + +### 4. Do not implement fallback as the steady state + +A resolver that permanently falls back to `~/.cocod` does not move the data. Legacy detection is needed to initiate migration, but successful startup should resolve to the canonical directory afterward. + +### 5. Account for the updated base + +The latest `coco-integration` adds: + +- `defaultMintUrl` persistence in the wallet's `config.json`; +- the NPC plugin, whose Nostr identity is derived from the same mnemonic; +- `src/daemon/wallet/coco-client.npc.test.ts`; +- Windows support and `USERPROFILE` fallback. + +Migration must preserve the complete config JSON rather than reconstructing selected fields. Path construction must use `path.join` rather than hard-coded `/` separators. + +--- + +## Current runtime path references on the latest base + +### Wallet data and legacy process coordination + +| File | Current responsibility | Required change | +|---|---|---| +| `src/daemon/wallet/coco-client.ts` | Uses one `.cocod` directory for config, DB, socket, and PID; owns the in-process wallet and legacy-cocod guard | Split canonical data paths from legacy process paths; migrate before opening the DB; use `wallet.pid` for the in-process lock | +| `src/daemon/wallet/cocod-client.ts` | External cocod compatibility client; defaults to `.cocod/cocod.sock` | Keep legacy defaults and clarify that they are external-cocod paths | +| `src/cli.ts` | Initializes `.cocod`; two restart paths wait on `.cocod/cocod.pid` | Initialize/migrate the canonical wallet and wait on `wallet.pid` | +| `src/utils/config.ts` | Defines `~/.routstrd`, but no wallet subdirectory | Export canonical and legacy wallet/process path helpers or constants | +| `src/daemon/index.ts` | Calls `createCocoClient()` before building the adapter | Ensure migration occurs before the client opens the DB | + +### Tests and documentation + +| File | Required update | +|---|---| +| `src/cli.test.ts` | Change fresh-wallet expectations and add migration coverage | +| `src/daemon/wallet/coco-client.test.ts` | Update data/lock paths and add migration/dual-lock tests | +| `src/daemon/wallet/coco-client.npc.test.ts` | Use the canonical wallet layout in fixtures; verify migrated mnemonic/config still drives NPC identity | +| `README.md` | Document the new location and migration behavior; retain legacy cocod terminology only where discussing compatibility | +| `SKILL.md` | Update wallet storage and environment-variable documentation | + +The fixture `src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz` keeps its name because it records the fixture's provenance. + +--- + +## Target path model + +Define the path model in one dependency-light module (for example `src/daemon/wallet/paths.ts`, or in `src/utils/config.ts` if that does not introduce a cycle): + +```text +ROUTSTRD config root process.env.ROUTSTRD_DIR || ~/.routstrd +canonical wallet directory process.env.ROUTSTRD_WALLET_DIR || /wallet +canonical wallet config /config.json +canonical wallet database /coco.db +canonical wallet lock process.env.ROUTSTRD_WALLET_PID || /wallet.pid + +legacy cocod directory process.env.COCOD_DIR || ~/.cocod +legacy cocod socket process.env.COCOD_SOCKET || /cocod.sock +legacy cocod PID process.env.COCOD_PID || /cocod.pid +``` + +Use `HOME` with `USERPROFILE` fallback, matching the updated Windows-support base. Use functions or injectable path objects where tests need to change environment variables after module import. + +`COCOD_DIR`, `COCOD_SOCKET`, and `COCOD_PID` remain compatibility controls for locating an old external cocod. They must not redirect the new in-process wallet away from `~/.routstrd/wallet`. `ROUTSTRD_WALLET_DIR` is the new explicit data-directory override. + +--- + +## Implementation plan + +### Phase 1 — Centralize and separate paths + +1. Add the canonical wallet and legacy cocod path definitions above. +2. Remove the local `.cocod` path construction from `coco-client.ts` and `cli.ts`. +3. Change `CreateCocoClientOptions` so tests/tooling can independently override: + - wallet data directory; + - wallet lock path; + - legacy cocod socket and PID paths. +4. Keep `cocod-client.ts` pointed at the legacy socket. Do not make it import a resolver that prefers the new wallet directory. + +### Phase 2 — Add a safe automatic migration primitive + +Add an idempotent `migrateLegacyWallet()` helper with injectable paths/filesystem operations for tests. + +Preconditions and behavior: + +1. If the canonical wallet already contains both `config.json` and `coco.db`, return `already-current` and do not touch legacy data. +2. If neither canonical nor legacy wallet data exists, return `fresh`; initialization will create the canonical directory. +3. If only the legacy wallet exists: + - first verify that legacy cocod is not running using the existing PID and socket guard; + - create a private staging directory under `~/.routstrd` with mode `0700`; + - copy the complete `config.json` and `coco.db` into staging without parsing or rewriting them; + - apply `0600` to both files; + - validate that the staged files exist and have the expected byte sizes; + - atomically rename the staged directory to `wallet`; + - only after the canonical directory is committed, remove the legacy `config.json` and `coco.db`; + - never copy `cocod.sock` or `cocod.pid`. +4. If canonical storage is partial, legacy storage is partial, both contain wallet data, or files conflict, stop with an actionable error. Never merge databases and never generate a new mnemonic over an ambiguous state. +5. Clean up an uncommitted staging directory after failure. A committed canonical wallet remains authoritative if cleanup of the old files fails; report that cleanup warning clearly. + +The migration should preserve unknown config fields, including `defaultMintUrl`, and preserve the database byte-for-byte. This also preserves the NPC identity because that identity is derived from the mnemonic. + +A staging copy plus atomic directory rename is preferred over two independent file renames: a crash must not expose a half-created canonical wallet. Copying also permits a clear rollback before commit and supports a legacy directory located on another filesystem through `COCOD_DIR`. + +### Phase 3 — Run migration from every wallet-opening path + +1. **CLI onboarding/init:** run migration before `initializeWallet()`. Only initialize a new mnemonic when migration reports `fresh`. +2. **Daemon direct startup:** run migration before `createCocoClient()` opens `coco.db`. This covers users who invoke the daemon without rerunning onboarding. +3. **Start/restart/update/service paths:** retain `stopLegacyCocod()` before migration/startup where those paths already stop legacy cocod. Direct daemon startup should refuse with the existing actionable error rather than silently running two wallet engines. +4. Print a concise success message showing old and new directories, but never print config contents or the mnemonic during migration. + +A separate `routstrd wallet migrate` command is optional as a manual recovery/preview entry point, but it must call the same migration primitive. It is not a substitute for automatic migration. + +### Phase 4 — Separate process locking + +Refactor the current `claimLegacyCocodPidFile()` behavior into explicit responsibilities: + +1. Claim `wallet.pid` for the lifetime of the in-process wallet to prevent two routstrd wallet instances from opening `coco.db`. +2. Continue claiming the legacy `cocod.pid` while routstrd is active, after verifying no real cocod owns it, to prevent an old cocod from starting. +3. If either claim fails, release any claim already acquired before returning the error. +4. On `CocodClient.dispose()`, startup failure, and daemon shutdown, release only PID files still owned by the current process. +5. Keep `stopLegacyCocod()` and `assertLegacyCocodNotRunning()` operating only on legacy cocod paths. +6. Update both PID-release waits in `src/cli.ts` (`restartDaemonsAfterUpdate` and `restart`) to wait for the canonical `wallet.pid` rather than `.cocod/cocod.pid`. + +There is no canonical wallet socket. + +### Phase 5 — Update initialization and client defaults + +1. Make `initializeWallet()` default to the canonical wallet directory. +2. Preserve directory mode `0700` and config mode `0600` on both migrated and fresh wallets. +3. In `createCocoClient()`, derive `config.json` and `coco.db` from the canonical wallet directory, but take legacy guard paths separately. +4. Update the wallet-access comment near `unlock()` to reference `~/.routstrd/wallet/config.json`. +5. Leave `CocodClient`, `resolveCocodExecutable()`, and `cocodPath` compatibility behavior unchanged. + +### Phase 6 — Tests + +Add or update tests for: + +- fresh initialization creates `~/.routstrd/wallet`, not `.cocod`; +- a legacy config and database migrate byte-for-byte; +- `defaultMintUrl` and unknown config fields survive migration; +- the NPC-derived identity is unchanged after migration; +- migration never copies socket or PID files; +- migration refuses while a real legacy cocod is running; +- stale legacy socket/PID handling remains safe; +- an existing complete canonical wallet wins without modifying it; +- partial canonical, partial legacy, and conflicting dual-wallet states fail without generating a mnemonic; +- staging cleanup and retry after an interrupted migration; +- custom `ROUTSTRD_DIR`, `ROUTSTRD_WALLET_DIR`, and legacy `COCOD_DIR` paths; +- Windows-compatible path construction; +- acquiring the second process lock rolls back the first on failure; +- `dispose()` releases both owned locks and does not unlink another process's lock; +- both CLI restart paths wait for `wallet.pid`. + +Keep `cocod-0.0.24-wallet.db.gz` unchanged and continue using it to prove the migrated database opens successfully with the current in-process wallet. + +### Phase 7 — Documentation + +Update `README.md` and `SKILL.md` to state: + +- wallet data is stored in `~/.routstrd/wallet`; +- existing `~/.cocod` data is migrated automatically on first startup; +- users must back up the mnemonic before migration; +- `ROUTSTRD_WALLET_DIR` overrides the canonical wallet directory; +- `COCOD_DIR`, `COCOD_SOCKET`, `COCOD_PID`, and `cocodPath` refer only to legacy external-cocod compatibility. + +Do not replace every use of the word `cocod`: references to the external daemon, compatibility client, legacy guard, package, and fixture are still accurate. + +--- + +## Acceptance criteria + +- A fresh install creates wallet data only under `~/.routstrd/wallet`. +- Starting from a valid `.cocod` wallet results in the same config and database under the canonical directory without changing balances, mnemonic-derived NPC identity, or default mint. +- No startup path silently creates a new mnemonic when recoverable legacy data exists. +- A running legacy cocod blocks migration and database opening. +- The new wallet directory contains no copied Unix socket and uses `wallet.pid` only as an in-process lock. +- Legacy cocod probing and exclusion continue to use `.cocod/cocod.sock` and `.cocod/cocod.pid`. +- The implementation works with `USERPROFILE`/Windows path construction and custom directory overrides. +- All wallet, CLI, typecheck, and build tests pass on the rebased `coco-integration` branch. + +--- + +## Suggested commit sequence + +1. `refactor(wallet): separate routstrd wallet paths from legacy cocod paths` +2. `feat(wallet): atomically migrate legacy cocod wallet data` +3. `fix(wallet): use independent routstrd and legacy cocod process locks` +4. `test(wallet): cover migration, conflicts, NPC identity, and lock rollback` +5. `docs: document routstrd wallet storage and legacy migration` diff --git a/README.md b/README.md index f944210..f776e15 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,17 @@ Response: } ``` +## Wallet storage + +The in-process Cashu wallet stores its mnemonic and proof database in +`~/.routstrd/wallet/`. On first startup, an existing wallet in `~/.cocod/` is +migrated automatically after routstrd verifies that the legacy cocod daemon is +not running. Back up your mnemonic before upgrading. + +Set `ROUTSTRD_WALLET_DIR` to override the canonical wallet directory. The +`COCOD_DIR`, `COCOD_SOCKET`, and `COCOD_PID` variables are retained only for +locating and excluding a legacy external cocod process. + ## Configuration Configuration is stored in `~/.routstrd/config.json`: diff --git a/SKILL.md b/SKILL.md index 0ac5272..1ac182a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -170,7 +170,9 @@ Refresh routstr21 models from Nostr and re-run integrations for all registered c | Variable | Default | Description | |----------|---------|-------------| | `ROUTSTRD_DIR` | `~/.routstrd` | Config directory | -| `COCOD_DIR` | `~/.cocod` | Wallet config directory | +| `ROUTSTRD_WALLET_DIR` | `~/.routstrd/wallet` | In-process Cashu wallet data directory | +| `ROUTSTRD_WALLET_PID` | `/wallet.pid` | In-process wallet lock path | +| `COCOD_DIR` | `~/.cocod` | Legacy external cocod compatibility directory | ### `routstrd mode` diff --git a/src/cli.test.ts b/src/cli.test.ts index a617dfb..0886893 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -24,7 +24,7 @@ afterEach(() => { describe("initializeWallet", () => { test("creates the wallet directory and config with restrictive permissions", () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); initializeWallet(walletDir); @@ -39,7 +39,7 @@ describe("initializeWallet", () => { }); test("repairs permissions without replacing an existing wallet", () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); const walletConfig = join(walletDir, "config.json"); const existingConfig = JSON.stringify({ mnemonic: "existing seed" }); diff --git a/src/cli.ts b/src/cli.ts index cd38571..5fab864 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,6 +17,7 @@ import { } from "./utils/clients"; import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs"; import { execSync } from "child_process"; +import { dirname, join } from "path"; import { CONFIG_DIR, DB_PATH, @@ -27,7 +28,18 @@ import { } from "./utils/config"; import { logger } from "./utils/logger"; import { setupIntegration, runIntegrationsForClients } from "./integrations"; -import { stopLegacyCocod } from "./daemon/wallet/coco-client"; +import { + assertLegacyCocodNotRunning, + claimLegacyCocodPidFile, + stopLegacyCocod, +} from "./daemon/wallet/coco-client"; +import { migrateLegacyWallet } from "./daemon/wallet/migration"; +import { + legacyCocodPidPath, + legacyCocodSocketPath, + walletDir as defaultWalletDir, + walletPidPath, +} from "./daemon/wallet/paths"; import { getClientsList } from "./utils/clients"; import * as QRCode from "qrcode"; import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98"; @@ -85,12 +97,8 @@ async function printLightningInvoice(invoice: string): Promise { console.log(`${qr}\nInvoice:\n${invoice}`); } -export function initializeWallet( - walletDir = - process.env.COCOD_DIR || - `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`, -): void { - const walletConfig = `${walletDir}/config.json`; +export function initializeWallet(walletDir = defaultWalletDir()): void { + const walletConfig = join(walletDir, "config.json"); // The wallet directory and config contain the plaintext seed phrase. Correct // permissions on existing installations as well as newly created ones. @@ -144,8 +152,8 @@ async function restartDaemonsAfterUpdate(): Promise { await callDaemon("/stop", { method: "POST" }); - // Wait for HTTP health check to fail AND pidfile to be released. - const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; + // Wait for HTTP health check to fail AND wallet lock to be released. + const pidFilePath = walletPidPath(); for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); const healthDown = !(await isDaemonRunning()); @@ -232,6 +240,24 @@ async function initDaemon(): Promise { } console.log(`Database will be stored at: ${DB_PATH}`); + const migration = await migrateLegacyWallet({ + assertLegacyStopped: () => + assertLegacyCocodNotRunning({ + socketPath: legacyCocodSocketPath(), + pidFilePath: legacyCocodPidPath(), + }), + acquireLegacyLock: () => { + mkdirSync(dirname(legacyCocodPidPath()), { + recursive: true, + mode: 0o700, + }); + return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() }); + }, + }); + if (migration.status === "migrated") { + console.log(`Migrated wallet from ${migration.from} to ${migration.to}.`); + for (const warning of migration.cleanupWarnings) console.warn(warning); + } initializeWallet(); await startDaemon({ port: String(config.port || 8008), host: config.host || undefined }); @@ -1833,8 +1859,8 @@ program console.log("Stopping daemon..."); await callDaemon("/stop", { method: "POST" }); - // Wait for HTTP health check to fail AND pidfile to be released. - const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; + // Wait for HTTP health check to fail AND wallet lock to be released. + const pidFilePath = walletPidPath(); for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); const healthDown = !(await isDaemonRunning()); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 6dc587a..0e3bb4b 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -54,7 +54,18 @@ import { createDaemonRequestHandler } from "./http"; import { FileRequestResponseLogSink } from "./request-response-log-sink"; import { refreshModelsAndIntegrations } from "../integrations"; import { RoutstrClient } from "@routstr/sdk"; -import { createCocoClient } from "./wallet/coco-client"; +import { mkdirSync } from "fs"; +import { dirname } from "path"; +import { + assertLegacyCocodNotRunning, + claimLegacyCocodPidFile, + createCocoClient, +} from "./wallet/coco-client"; +import { migrateLegacyWallet } from "./wallet/migration"; +import { + legacyCocodPidPath, + legacyCocodSocketPath, +} from "./wallet/paths"; // Global error handlers — the daemon is spawned detached with stdout/stderr // redirected to a file, so without these, uncaught async errors would kill @@ -115,6 +126,25 @@ async function main(): Promise { const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } = createModelService(modelManager, providerManager, store); + const migration = await migrateLegacyWallet({ + assertLegacyStopped: () => + assertLegacyCocodNotRunning({ + socketPath: legacyCocodSocketPath(), + pidFilePath: legacyCocodPidPath(), + }), + acquireLegacyLock: () => { + mkdirSync(dirname(legacyCocodPidPath()), { + recursive: true, + mode: 0o700, + }); + return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() }); + }, + }); + if (migration.status === "migrated") { + startupProgress(`Wallet migrated from ${migration.from} to ${migration.to}.`); + for (const warning of migration.cleanupWarnings) logger.warn(warning); + } + const walletClient = await createCocoClient(); // ── Auto-refill configuration ──────────────────────────────── diff --git a/src/daemon/wallet/coco-client.npc.test.ts b/src/daemon/wallet/coco-client.npc.test.ts index d348473..b1ae0fc 100644 --- a/src/daemon/wallet/coco-client.npc.test.ts +++ b/src/daemon/wallet/coco-client.npc.test.ts @@ -49,7 +49,7 @@ const tempDirs: string[] = []; function makeWalletDir(): string { const dir = mkdtempSync(join(tmpdir(), "routstrd-coco-npc-test-")); tempDirs.push(dir); - const walletDir = join(dir, ".cocod"); + const walletDir = join(dir, "wallet"); mkdirSync(walletDir, { recursive: true }); writeFileSync( join(walletDir, "config.json"), @@ -58,6 +58,14 @@ function makeWalletDir(): string { return walletDir; } +function testClientOptions(walletDir = makeWalletDir()) { + return { + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), + }; +} + function resetNpcState(): void { npcState.info = { name: "alice", pubkey: "ab".repeat(32) }; npcState.usernameResult = { success: true }; @@ -74,7 +82,7 @@ afterEach(() => { describe("in-process coco client NPC integration", () => { it("registers the plugin and exposes the wallet's NPC lightning address", async () => { - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { const info = await client.getNpcAddress(); expect(info).toEqual({ @@ -89,7 +97,7 @@ describe("in-process coco client NPC integration", () => { it("falls back to the npub address when no username is set", async () => { npcState.info = { name: null, pubkey: "ab".repeat(32) }; - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { const info = await client.getNpcAddress(); expect(info.address).toStartWith("npub1"); @@ -106,7 +114,7 @@ describe("in-process coco client NPC integration", () => { success: false, pr: { amount: 21, mints: ["https://mint.example.com"] }, }; - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { const result = await client.setNpcUsername("bob"); expect(result).toEqual({ @@ -122,7 +130,7 @@ describe("in-process coco client NPC integration", () => { }); it("confirms the claim fee payment when requested", async () => { - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { const result = await client.setNpcUsername("bob", true); expect(result).toEqual({ success: true }); @@ -135,7 +143,7 @@ describe("in-process coco client NPC integration", () => { }); it("triggers a manual quote sync", async () => { - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { await client.syncNpc(); expect(npcState.syncCalls).toBe(1); @@ -146,7 +154,7 @@ describe("in-process coco client NPC integration", () => { it("rejects NPC calls with a clear error when the plugin is disabled", async () => { const client = await createCocoClient({ - configDir: makeWalletDir(), + ...testClientOptions(), enableNpc: false, }); try { diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts index 8ce421e..b9dd10a 100644 --- a/src/daemon/wallet/coco-client.test.ts +++ b/src/daemon/wallet/coco-client.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, mock } from "bun:test"; import { gunzipSync } from "bun"; import { + existsSync, mkdtempSync, mkdirSync, readFileSync, @@ -43,7 +44,7 @@ function socketOnly(path: string): boolean { describe("default mint functionality", () => { it("automatically adds default mint when no mints exist", async () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); mkdirSync(walletDir, { recursive: true }); writeFileSync( join(walletDir, "config.json"), @@ -55,8 +56,19 @@ describe("default mint functionality", () => { }), ); - const client = await createCocoClient({ configDir: walletDir }); + const client = await createCocoClient({ + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), + }); try { + expect(readFileSync(join(walletDir, "wallet.pid"), "utf8")).toBe( + String(process.pid), + ); + expect(readFileSync(join(walletDir, "legacy-cocod.pid"), "utf8")).toBe( + String(process.pid), + ); + const mints = await client.listMints(); expect(mints).toContain("https://mint.cubabitcoin.org"); @@ -65,10 +77,12 @@ describe("default mint functionality", () => { } finally { await client.dispose?.(); } + expect(existsSync(join(walletDir, "wallet.pid"))).toBe(false); + expect(existsSync(join(walletDir, "legacy-cocod.pid"))).toBe(false); }); it("respects existing default mint in config", async () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); const configuredDefault = "https://mint.cubabitcoin.org"; mkdirSync(walletDir, { recursive: true }); @@ -84,7 +98,11 @@ describe("default mint functionality", () => { }), ); - const client = await createCocoClient({ configDir: walletDir }); + const client = await createCocoClient({ + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), + }); try { // Should respect the configured default mint const defaultMint = await client.getDefaultMint(); @@ -99,7 +117,7 @@ describe("default mint functionality", () => { }); it("allows setting default mint to an already trusted mint", async () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); mkdirSync(walletDir, { recursive: true }); writeFileSync( join(walletDir, "config.json"), @@ -111,7 +129,11 @@ describe("default mint functionality", () => { }), ); - const client = await createCocoClient({ configDir: walletDir }); + const client = await createCocoClient({ + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), + }); try { // The initial default should be the auto-added Cuba mint const initialDefault = await client.getDefaultMint(); @@ -133,7 +155,7 @@ describe("default mint functionality", () => { describe("legacy cocod wallet migration", () => { it("opens an existing unencrypted config and preserves database balances", async () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); const mintUrl = "https://mint.example.com"; mkdirSync(walletDir, { recursive: true }); @@ -164,7 +186,9 @@ describe("legacy cocod wallet migration", () => { // NPC is disabled here: this test verifies database migration, and the // real plugin would otherwise open a websocket to npubx.cash. const client = await createCocoClient({ - configDir: walletDir, + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), enableNpc: false, }); try { @@ -177,7 +201,9 @@ describe("legacy cocod wallet migration", () => { // Prove the migrated schema remains reopenable and the pre-existing // proofs survive a complete in-process wallet restart. const reopenedClient = await createCocoClient({ - configDir: walletDir, + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), enableNpc: false, }); try { diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 2b351a3..3a90cb0 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -16,13 +16,14 @@ import { finalizeEvent, nip19, type EventTemplate } from "nostr-tools"; import { closeSync, existsSync, + mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "fs"; -import { join } from "path"; +import { dirname, join } from "path"; import { mnemonicToSeedSync } from "@scure/bip39"; import type { CocodClient, @@ -31,20 +32,15 @@ import type { NpcUsernameResult, } from "./cocod-client"; import { logger } from "../../utils/logger"; +import { + legacyCocodPidPath, + legacyCocodSocketPath, + walletDir as defaultWalletDir, + walletPidPath as defaultWalletPidPath, +} from "./paths"; const NPC_DEFAULT_BASE_URL = "https://npubx.cash"; -const CONFIG_DIR = - process.env.COCOD_DIR || - `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`; - -const CONFIG_FILE = join(CONFIG_DIR, "config.json"); -const DB_PATH = join(CONFIG_DIR, "coco.db"); -const LEGACY_COCOD_SOCKET = - process.env.COCOD_SOCKET || join(CONFIG_DIR, "cocod.sock"); -const LEGACY_COCOD_PID_FILE = - process.env.COCOD_PID || join(CONFIG_DIR, "cocod.pid"); - const STALE_SOCKET_ERROR_CODES = new Set([ "ECONNREFUSED", "ENOENT", @@ -167,7 +163,7 @@ function createCocoLogger(bindings: Record = {}): CocoLogger { }; } -function loadConfig(configFile: string = CONFIG_FILE): CocodConfig { +function loadConfig(configFile: string): CocodConfig { if (!existsSync(configFile)) { throw new Error( `Config file not found at ${configFile}. Run 'routstrd onboard' first.`, @@ -182,10 +178,7 @@ function loadConfig(configFile: string = CONFIG_FILE): CocodConfig { return config; } -function saveConfig( - config: CocodConfig, - configFile: string = CONFIG_FILE, -): void { +function saveConfig(config: CocodConfig, configFile: string): void { const temporaryFile = `${configFile}.${process.pid}.tmp`; try { writeFileSync(temporaryFile, JSON.stringify(config, null, 2), { @@ -239,8 +232,8 @@ function hasErrorCode(error: unknown, codes: Set): boolean { export async function assertLegacyCocodNotRunning( options: LegacyCocodGuardOptions = {}, ): Promise { - const socketPath = options.socketPath || LEGACY_COCOD_SOCKET; - const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + const socketPath = options.socketPath || legacyCocodSocketPath(); + const pidFilePath = options.pidFilePath || legacyCocodPidPath(); const pathExists = options.pathExists || existsSync; const readFile = options.readFile || ((path) => readFileSync(path, "utf-8")); const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning; @@ -325,8 +318,8 @@ export async function assertLegacyCocodNotRunning( export async function stopLegacyCocod( options: LegacyCocodStopOptions = {}, ): Promise { - const socketPath = options.socketPath || LEGACY_COCOD_SOCKET; - const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + const socketPath = options.socketPath || legacyCocodSocketPath(); + const pidFilePath = options.pidFilePath || legacyCocodPidPath(); const pathExists = options.pathExists || existsSync; const readFile = options.readFile || ((path: string) => readFileSync(path, "utf-8")); @@ -416,7 +409,14 @@ export async function stopLegacyCocod( export function claimLegacyCocodPidFile( options: LegacyCocodPidClaimOptions = {}, ): () => void { - const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + return claimPidFile({ + ...options, + pidFilePath: options.pidFilePath || legacyCocodPidPath(), + }); +} + +function claimPidFile(options: LegacyCocodPidClaimOptions & { pidFilePath: string }): () => void { + const pidFilePath = options.pidFilePath; const pid = options.pid ?? process.pid; const openExclusive = options.openExclusive || ((path: string) => openSync(path, "wx", 0o600)); @@ -507,10 +507,15 @@ export function claimLegacyCocodPidFile( } export interface CreateCocoClientOptions { - /** Override the wallet directory, primarily for migration tests and tooling. */ + /** Override the canonical wallet data directory. */ + walletDir?: string; + /** Deprecated alias retained for existing callers during migration. */ configDir?: string; - socketPath?: string; - pidFilePath?: string; + /** Override the in-process wallet lock path. */ + walletPidPath?: string; + /** Override legacy external-cocod coordination paths. */ + legacySocketPath?: string; + legacyPidPath?: string; /** Set to false to skip NPC (npubx.cash) plugin registration. Default: true. */ enableNpc?: boolean; /** NPC server base URL. Default: https://npubx.cash */ @@ -520,20 +525,36 @@ export interface CreateCocoClientOptions { export async function createCocoClient( options: CreateCocoClientOptions = {}, ): Promise { - const configDir = options.configDir || CONFIG_DIR; + const configDir = options.walletDir || options.configDir || defaultWalletDir(); const configFile = join(configDir, "config.json"); const dbPath = join(configDir, "coco.db"); - const socketPath = - options.socketPath || - (options.configDir ? join(configDir, "cocod.sock") : LEGACY_COCOD_SOCKET); - const pidFilePath = - options.pidFilePath || - (options.configDir ? join(configDir, "cocod.pid") : LEGACY_COCOD_PID_FILE); + const walletPidFile = + options.walletPidPath || + (options.walletDir || options.configDir + ? join(configDir, "wallet.pid") + : defaultWalletPidPath()); + const legacySocket = options.legacySocketPath || legacyCocodSocketPath(); + const legacyPidFile = options.legacyPidPath || legacyCocodPidPath(); const npcBaseUrl = options.npcBaseUrl || NPC_DEFAULT_BASE_URL; const npcAddressDomain = new URL(npcBaseUrl).host; - await assertLegacyCocodNotRunning({ socketPath, pidFilePath }); - const releaseLegacyPidClaim = claimLegacyCocodPidFile({ pidFilePath }); + await assertLegacyCocodNotRunning({ + socketPath: legacySocket, + pidFilePath: legacyPidFile, + }); + // The canonical wallet directory is created by initialization/migration. + // Keep a legacy PID claim as an exclusion fence for old cocod binaries. + mkdirSync(dirname(legacyPidFile), { recursive: true, mode: 0o700 }); + const releaseWalletPidClaim = claimPidFile({ pidFilePath: walletPidFile }); + let releaseLegacyPidClaim: () => void; + try { + releaseLegacyPidClaim = claimLegacyCocodPidFile({ + pidFilePath: legacyPidFile, + }); + } catch (error) { + releaseWalletPidClaim(); + throw error; + } let database: Database | undefined; let coco: Awaited> | undefined; @@ -616,6 +637,7 @@ export async function createCocoClient( } catch (error) { database?.close(); releaseLegacyPidClaim(); + releaseWalletPidClaim(); throw error; } @@ -653,7 +675,7 @@ export async function createCocoClient( async unlock(_passphrase: string): Promise { // coco-core does not support passphrase locking. - // Wallet access is controlled via the mnemonic in ~/.cocod/config.json. + // Wallet access is controlled by ~/.routstrd/wallet/config.json. return "wallet does not require unlocking"; }, @@ -756,6 +778,7 @@ export async function createCocoClient( database.close(); } finally { releaseLegacyPidClaim(); + releaseWalletPidClaim(); } } }, diff --git a/src/daemon/wallet/migration.test.ts b/src/daemon/wallet/migration.test.ts new file mode 100644 index 0000000..e61892a --- /dev/null +++ b/src/daemon/wallet/migration.test.ts @@ -0,0 +1,107 @@ +import { afterEach, describe, expect, it, mock } from "bun:test"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { migrateLegacyWallet } from "./migration"; + +const roots: string[] = []; +function root(): string { + const path = mkdtempSync(join(tmpdir(), "routstrd-migration-")); + roots.push(path); + return path; +} +afterEach(() => { + for (const path of roots.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +describe("migrateLegacyWallet", () => { + it("copies wallet data byte-for-byte and leaves process files behind", async () => { + const base = root(); + const legacyDir = join(base, ".cocod"); + const walletDir = join(base, ".routstrd", "wallet"); + mkdirSync(legacyDir); + const config = JSON.stringify({ + mnemonic: "seed words", + encrypted: false, + defaultMintUrl: "https://mint.example", + unknown: { preserved: true }, + }); + const database = new Uint8Array([0, 1, 2, 3, 255]); + writeFileSync(join(legacyDir, "config.json"), config); + writeFileSync(join(legacyDir, "coco.db"), database); + writeFileSync(join(legacyDir, "cocod.pid"), "123"); + writeFileSync(join(legacyDir, "cocod.sock"), "not-a-real-socket"); + const assertStopped = mock(async () => {}); + const releaseLock = mock(() => {}); + const acquireLock = mock(() => releaseLock); + + const result = await migrateLegacyWallet({ + walletDir, + legacyDir, + assertLegacyStopped: assertStopped, + acquireLegacyLock: acquireLock, + }); + + expect(result.status).toBe("migrated"); + expect(assertStopped).toHaveBeenCalledTimes(2); + expect(acquireLock).toHaveBeenCalledTimes(1); + expect(releaseLock).toHaveBeenCalledTimes(1); + expect(readFileSync(join(walletDir, "config.json"), "utf8")).toBe(config); + expect(readFileSync(join(walletDir, "coco.db"))).toEqual(Buffer.from(database)); + expect(statSync(walletDir).mode & 0o777).toBe(0o700); + expect(statSync(join(walletDir, "config.json")).mode & 0o777).toBe(0o600); + expect(existsSync(join(walletDir, "cocod.pid"))).toBe(false); + expect(existsSync(join(walletDir, "cocod.sock"))).toBe(false); + expect(existsSync(join(legacyDir, "config.json"))).toBe(false); + expect(existsSync(join(legacyDir, "coco.db"))).toBe(false); + expect(existsSync(join(legacyDir, "cocod.pid"))).toBe(true); + expect(existsSync(join(legacyDir, "cocod.sock"))).toBe(true); + }); + + it("treats a config-only initialized wallet as migratable", async () => { + const base = root(); + const legacyDir = join(base, ".cocod"); + const walletDir = join(base, ".routstrd", "wallet"); + mkdirSync(legacyDir); + writeFileSync(join(legacyDir, "config.json"), '{"mnemonic":"seed"}'); + + expect((await migrateLegacyWallet({ walletDir, legacyDir })).status).toBe("migrated"); + expect(existsSync(join(walletDir, "config.json"))).toBe(true); + expect(existsSync(join(walletDir, "coco.db"))).toBe(false); + }); + + it("refuses a database without its mnemonic config", async () => { + const base = root(); + const legacyDir = join(base, ".cocod"); + mkdirSync(legacyDir); + writeFileSync(join(legacyDir, "coco.db"), "database"); + + await expect(migrateLegacyWallet({ walletDir: join(base, "wallet"), legacyDir })).rejects.toThrow( + "without config.json", + ); + }); + + it("refuses when both canonical and legacy configs exist", async () => { + const base = root(); + const legacyDir = join(base, ".cocod"); + const walletDir = join(base, ".routstrd", "wallet"); + mkdirSync(legacyDir, { recursive: true }); + mkdirSync(walletDir, { recursive: true }); + writeFileSync(join(legacyDir, "config.json"), "legacy"); + writeFileSync(join(walletDir, "config.json"), "current"); + + await expect(migrateLegacyWallet({ walletDir, legacyDir })).rejects.toThrow( + "both", + ); + expect(readFileSync(join(walletDir, "config.json"), "utf8")).toBe("current"); + expect(readFileSync(join(legacyDir, "config.json"), "utf8")).toBe("legacy"); + }); +}); diff --git a/src/daemon/wallet/migration.ts b/src/daemon/wallet/migration.ts new file mode 100644 index 0000000..ab55632 --- /dev/null +++ b/src/daemon/wallet/migration.ts @@ -0,0 +1,136 @@ +import { + chmodSync, + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + statSync, + unlinkSync, +} from "fs"; +import { dirname, join } from "path"; +import { legacyCocodDir, walletDir } from "./paths"; + +export type WalletMigrationResult = + | { status: "fresh" } + | { status: "already-current" } + | { status: "migrated"; from: string; to: string; cleanupWarnings: string[] }; + +export interface WalletMigrationOptions { + walletDir?: string; + legacyDir?: string; + /** Called after state validation and before legacy files are copied. */ + assertLegacyStopped?: () => void | Promise; + /** Claims the legacy cocod exclusion lock for the duration of the copy. */ + acquireLegacyLock?: () => (() => void) | Promise<() => void>; +} + +type WalletState = "absent" | "database-only" | "initialized"; + +function state(configPath: string, dbPath: string): WalletState { + const hasConfig = existsSync(configPath); + const hasDb = existsSync(dbPath); + if (hasConfig) return "initialized"; + if (hasDb) return "database-only"; + return "absent"; +} + +function filesEqual(left: string, right: string): boolean { + if (!existsSync(left) || !existsSync(right)) return false; + if (statSync(left).size !== statSync(right).size) return false; + return readFileSync(left).equals(readFileSync(right)); +} + +/** + * Copy a legacy cocod wallet into the canonical routstrd wallet directory. + * The staging directory is renamed atomically, so the canonical path is never + * exposed with only one of its two required files. + */ +export async function migrateLegacyWallet( + options: WalletMigrationOptions = {}, +): Promise { + const targetDir = options.walletDir || walletDir(); + const sourceDir = options.legacyDir || legacyCocodDir(); + const targetConfig = join(targetDir, "config.json"); + const targetDb = join(targetDir, "coco.db"); + const sourceConfig = join(sourceDir, "config.json"); + const sourceDb = join(sourceDir, "coco.db"); + const targetState = state(targetConfig, targetDb); + const sourceState = state(sourceConfig, sourceDb); + + // config.json is sufficient for a newly initialized wallet; coco.db is + // created on first open. A database without its mnemonic is never usable. + if (targetState === "initialized" && sourceState === "initialized") { + // A prior migration may have committed successfully but failed to remove + // its source files. Identical leftovers are safe; divergent wallets are not. + const configsMatch = filesEqual(targetConfig, sourceConfig); + const databasesMatch = + !existsSync(targetDb) && !existsSync(sourceDb) + ? true + : filesEqual(targetDb, sourceDb); + if (configsMatch && databasesMatch) return { status: "already-current" }; + throw new Error( + `Cannot migrate wallet: both ${targetDir} and ${sourceDir} contain different wallet data. ` + + "Refusing to choose a mnemonic or merge wallet databases automatically.", + ); + } + if (targetState === "initialized") return { status: "already-current" }; + if (targetState === "database-only" || sourceState === "database-only") { + throw new Error( + `Cannot migrate wallet: ${targetDir} is ${targetState} and ${sourceDir} is ${sourceState}. ` + + "A coco.db without config.json cannot be opened; restore the matching config first.", + ); + } + if (sourceState === "absent") return { status: "fresh" }; + + await options.assertLegacyStopped?.(); + const releaseLegacyLock = await options.acquireLegacyLock?.(); + const stagingDir = join( + dirname(targetDir), + `.${targetDir.split(/[\\/]/).at(-1) || "wallet"}.staging-${process.pid}-${Date.now()}`, + ); + + try { + // Recheck after claiming the exclusion lock to close the probe/claim race. + await options.assertLegacyStopped?.(); + mkdirSync(dirname(targetDir), { recursive: true, mode: 0o700 }); + mkdirSync(stagingDir, { mode: 0o700 }); + const stagedConfig = join(stagingDir, "config.json"); + const stagedDb = join(stagingDir, "coco.db"); + copyFileSync(sourceConfig, stagedConfig); + chmodSync(stagedConfig, 0o600); + const hasSourceDb = existsSync(sourceDb); + if (hasSourceDb) { + copyFileSync(sourceDb, stagedDb); + chmodSync(stagedDb, 0o600); + } + + if ( + statSync(stagedConfig).size !== statSync(sourceConfig).size || + (hasSourceDb && statSync(stagedDb).size !== statSync(sourceDb).size) + ) { + throw new Error("Cannot migrate wallet: staged files failed size validation."); + } + + renameSync(stagingDir, targetDir); + } catch (error) { + rmSync(stagingDir, { recursive: true, force: true }); + throw error; + } finally { + releaseLegacyLock?.(); + } + + const cleanupWarnings: string[] = []; + for (const path of [sourceConfig, sourceDb]) { + if (!existsSync(path)) continue; + try { + unlinkSync(path); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + cleanupWarnings.push(`Could not remove legacy wallet file ${path}: ${message}`); + } + } + + return { status: "migrated", from: sourceDir, to: targetDir, cleanupWarnings }; +} diff --git a/src/daemon/wallet/paths.ts b/src/daemon/wallet/paths.ts new file mode 100644 index 0000000..bdbd3e7 --- /dev/null +++ b/src/daemon/wallet/paths.ts @@ -0,0 +1,33 @@ +import { join } from "path"; + +function homeDir(): string { + return process.env.HOME || process.env.USERPROFILE || ""; +} + +/** Root directory for routstrd state. Evaluated lazily so env overrides work in tests. */ +export function routstrdConfigDir(): string { + return process.env.ROUTSTRD_DIR || join(homeDir(), ".routstrd"); +} + +/** Canonical directory for the in-process Cashu wallet. */ +export function walletDir(): string { + return process.env.ROUTSTRD_WALLET_DIR || join(routstrdConfigDir(), "wallet"); +} + +/** Lock owned by an in-process routstrd wallet instance. */ +export function walletPidPath(): string { + return process.env.ROUTSTRD_WALLET_PID || join(walletDir(), "wallet.pid"); +} + +/** Legacy external cocod directory. This is not the routstrd wallet directory. */ +export function legacyCocodDir(): string { + return process.env.COCOD_DIR || join(homeDir(), ".cocod"); +} + +export function legacyCocodSocketPath(): string { + return process.env.COCOD_SOCKET || join(legacyCocodDir(), "cocod.sock"); +} + +export function legacyCocodPidPath(): string { + return process.env.COCOD_PID || join(legacyCocodDir(), "cocod.pid"); +} diff --git a/src/start-daemon.ts b/src/start-daemon.ts index 9e45702..a9c58a5 100644 --- a/src/start-daemon.ts +++ b/src/start-daemon.ts @@ -9,7 +9,6 @@ import { logger } from "./utils/logger"; import { CONFIG_DIR, LOGS_DIR } from "./utils/config"; import { withCrossProcessLock } from "./utils/process-lock"; import { fileURLToPath } from "url"; -import { existsSync } from "fs"; const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`; const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`; From 769e8206ede365281b22300f0330270306fe1e44 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:15:33 +0000 Subject: [PATCH 27/32] fix(wallet): stop legacy cocod before direct daemon startup and ignore migration lock PID routstrd start already stopped the real legacy cocod daemon, but wallet migration claimed ~/.cocod/cocod.pid using routstrd's own PID. The post-claim recheck in assertLegacyCocodNotRunning then saw that live PID and falsely reported it as a running legacy cocod, so every start attempt failed with a different 'Legacy cocod daemon is still running with PID ' error. - assertLegacyCocodNotRunning now accepts ignorePid so the migration's own lock owner is not mistaken for cocod. - The direct daemon entry point (src/daemon/index.ts) calls stopLegacyCocod before migration, so startup is safe even when launched directly or by an older/global CLI, instead of relying solely on the parent command. - initDaemon() stops legacy cocod before migrating as well. - Added a regression test for the self-owned migration lock PID. --- bun.lock | 1 + src/cli.ts | 13 ++++++++++++- src/daemon/index.ts | 19 ++++++++++++++++++- src/daemon/wallet/coco-client.test.ts | 13 +++++++++++++ src/daemon/wallet/coco-client.ts | 7 ++++++- 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index d8caf08..52ef082 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "routstrd", diff --git a/src/cli.ts b/src/cli.ts index 5fab864..a362ef5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -240,18 +240,29 @@ async function initDaemon(): Promise { } console.log(`Database will be stored at: ${DB_PATH}`); + await stopLegacyCocod(); + + let migrationLockOwner: number | undefined; const migration = await migrateLegacyWallet({ assertLegacyStopped: () => assertLegacyCocodNotRunning({ socketPath: legacyCocodSocketPath(), pidFilePath: legacyCocodPidPath(), + ignorePid: migrationLockOwner, }), acquireLegacyLock: () => { mkdirSync(dirname(legacyCocodPidPath()), { recursive: true, mode: 0o700, }); - return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() }); + const release = claimLegacyCocodPidFile({ + pidFilePath: legacyCocodPidPath(), + }); + migrationLockOwner = process.pid; + return () => { + migrationLockOwner = undefined; + release(); + }; }, }); if (migration.status === "migrated") { diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 0e3bb4b..6475874 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -60,6 +60,7 @@ import { assertLegacyCocodNotRunning, claimLegacyCocodPidFile, createCocoClient, + stopLegacyCocod, } from "./wallet/coco-client"; import { migrateLegacyWallet } from "./wallet/migration"; import { @@ -126,18 +127,34 @@ async function main(): Promise { const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } = createModelService(modelManager, providerManager, store); + // The daemon may be launched directly (or by an older/global CLI), so do + // not rely on the parent command having stopped the external wallet first. + await stopLegacyCocod({ + socketPath: legacyCocodSocketPath(), + pidFilePath: legacyCocodPidPath(), + }); + + let migrationLockOwner: number | undefined; const migration = await migrateLegacyWallet({ assertLegacyStopped: () => assertLegacyCocodNotRunning({ socketPath: legacyCocodSocketPath(), pidFilePath: legacyCocodPidPath(), + ignorePid: migrationLockOwner, }), acquireLegacyLock: () => { mkdirSync(dirname(legacyCocodPidPath()), { recursive: true, mode: 0o700, }); - return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() }); + const release = claimLegacyCocodPidFile({ + pidFilePath: legacyCocodPidPath(), + }); + migrationLockOwner = process.pid; + return () => { + migrationLockOwner = undefined; + release(); + }; }, }); if (migration.status === "migrated") { diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts index b9dd10a..0749c42 100644 --- a/src/daemon/wallet/coco-client.test.ts +++ b/src/daemon/wallet/coco-client.test.ts @@ -315,6 +315,19 @@ describe("assertLegacyCocodNotRunning", () => { ).resolves.toBeUndefined(); }); + it("ignores the caller's own migration lock PID", async () => { + await expect( + assertLegacyCocodNotRunning({ + socketPath: SOCKET_PATH, + pidFilePath: PID_FILE_PATH, + pathExists: (path) => path === PID_FILE_PATH, + readFile: () => "4242\n", + isProcessRunning: () => true, + ignorePid: 4242, + }), + ).resolves.toBeUndefined(); + }); + it("fails closed when the socket cannot be probed safely", async () => { const fetchImpl = mock(async () => { throw Object.assign(new Error("permission denied"), { code: "EACCES" }); diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 3a90cb0..8aa2cfa 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -60,6 +60,8 @@ export interface LegacyCocodGuardOptions { pathExists?: (path: string) => boolean; readFile?: (path: string) => string; isProcessRunning?: (pid: number) => boolean; + /** PID owned by the caller's already-acquired legacy exclusion lock. */ + ignorePid?: number; fetchImpl?: LegacyCocodFetch; timeoutMs?: number; } @@ -243,7 +245,10 @@ export async function assertLegacyCocodNotRunning( try { const pid = Number.parseInt(readFile(pidFilePath).trim(), 10); - return Number.isInteger(pid) && pid > 0 && isProcessRunning(pid) + return Number.isInteger(pid) && + pid > 0 && + pid !== options.ignorePid && + isProcessRunning(pid) ? pid : null; } catch { From 89ab33763eec5d5331c662347bed27a195042e29 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:41:49 +0000 Subject: [PATCH 28/32] fix(wallet): snapshot SQLite WAL during cocod-to-routstrd migration Raw-copying coco.db silently omitted uncheckpointed WAL data, losing proofs and derivation counters. Replace the byte copy with VACUUM INTO, verify quick_check and semantic equality (proofs, counters, operations, mints) before committing, refuse orphaned -wal/-shm sidecars, and archive the complete legacy wallet instead of deleting it immediately. Add a real WAL-mode integration test proving uncheckpointed proofs and counters survive migration. --- src/daemon/wallet/migration.test.ts | 50 +++++++++++++-- src/daemon/wallet/migration.ts | 97 +++++++++++++++++++++++++---- 2 files changed, 130 insertions(+), 17 deletions(-) diff --git a/src/daemon/wallet/migration.test.ts b/src/daemon/wallet/migration.test.ts index e61892a..b053bcd 100644 --- a/src/daemon/wallet/migration.test.ts +++ b/src/daemon/wallet/migration.test.ts @@ -1,9 +1,11 @@ import { afterEach, describe, expect, it, mock } from "bun:test"; +import { Database } from "bun:sqlite"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, + readdirSync, rmSync, statSync, writeFileSync, @@ -23,7 +25,7 @@ afterEach(() => { }); describe("migrateLegacyWallet", () => { - it("copies wallet data byte-for-byte and leaves process files behind", async () => { + it("snapshots committed WAL data and leaves process files behind", async () => { const base = root(); const legacyDir = join(base, ".cocod"); const walletDir = join(base, ".routstrd", "wallet"); @@ -34,28 +36,51 @@ describe("migrateLegacyWallet", () => { defaultMintUrl: "https://mint.example", unknown: { preserved: true }, }); - const database = new Uint8Array([0, 1, 2, 3, 255]); writeFileSync(join(legacyDir, "config.json"), config); - writeFileSync(join(legacyDir, "coco.db"), database); writeFileSync(join(legacyDir, "cocod.pid"), "123"); writeFileSync(join(legacyDir, "cocod.sock"), "not-a-real-socket"); + + const source = new Database(join(legacyDir, "coco.db")); + source.exec("PRAGMA journal_mode=WAL; PRAGMA wal_autocheckpoint=0"); + source.exec(` + CREATE TABLE coco_cashu_proofs (mintUrl TEXT, state TEXT, amount INTEGER); + CREATE TABLE coco_cashu_counters (mintUrl TEXT, keysetId TEXT, counter INTEGER); + CREATE TABLE coco_cashu_mint_operations (state TEXT); + CREATE TABLE coco_cashu_send_operations (state TEXT); + CREATE TABLE coco_cashu_melt_operations (state TEXT); + CREATE TABLE coco_cashu_mints (mintUrl TEXT, trusted INTEGER); + INSERT INTO coco_cashu_proofs VALUES ('https://mint.example', 'ready', 21); + INSERT INTO coco_cashu_counters VALUES ('https://mint.example', 'keyset', 3); + INSERT INTO coco_cashu_mints VALUES ('https://mint.example', 1); + `); + expect(existsSync(join(legacyDir, "coco.db-wal"))).toBe(true); + const assertStopped = mock(async () => {}); const releaseLock = mock(() => {}); const acquireLock = mock(() => releaseLock); - const result = await migrateLegacyWallet({ walletDir, legacyDir, assertLegacyStopped: assertStopped, acquireLegacyLock: acquireLock, }); + source.close(); expect(result.status).toBe("migrated"); expect(assertStopped).toHaveBeenCalledTimes(2); expect(acquireLock).toHaveBeenCalledTimes(1); expect(releaseLock).toHaveBeenCalledTimes(1); expect(readFileSync(join(walletDir, "config.json"), "utf8")).toBe(config); - expect(readFileSync(join(walletDir, "coco.db"))).toEqual(Buffer.from(database)); + const migrated = new Database(join(walletDir, "coco.db"), { readonly: true }); + expect(migrated.query("PRAGMA quick_check").values()).toEqual([["ok"]]); + expect(migrated.query("SELECT state, amount FROM coco_cashu_proofs").values()).toEqual([ + ["ready", 21], + ]); + expect(migrated.query("SELECT counter FROM coco_cashu_counters").get()).toEqual({ + counter: 3, + }); + migrated.close(); + expect(existsSync(join(walletDir, "coco.db-wal"))).toBe(false); expect(statSync(walletDir).mode & 0o777).toBe(0o700); expect(statSync(join(walletDir, "config.json")).mode & 0o777).toBe(0o600); expect(existsSync(join(walletDir, "cocod.pid"))).toBe(false); @@ -64,6 +89,10 @@ describe("migrateLegacyWallet", () => { expect(existsSync(join(legacyDir, "coco.db"))).toBe(false); expect(existsSync(join(legacyDir, "cocod.pid"))).toBe(true); expect(existsSync(join(legacyDir, "cocod.sock"))).toBe(true); + const archive = readdirSync(legacyDir).find((name) => name.startsWith("wallet-migrated-")); + expect(archive).toBeDefined(); + expect(existsSync(join(legacyDir, archive!, "config.json"))).toBe(true); + expect(existsSync(join(legacyDir, archive!, "coco.db"))).toBe(true); }); it("treats a config-only initialized wallet as migratable", async () => { @@ -89,6 +118,17 @@ describe("migrateLegacyWallet", () => { ); }); + it("refuses orphaned SQLite sidecars", async () => { + const base = root(); + const legacyDir = join(base, ".cocod"); + mkdirSync(legacyDir); + writeFileSync(join(legacyDir, "coco.db-wal"), "orphaned WAL"); + + await expect(migrateLegacyWallet({ walletDir: join(base, "wallet"), legacyDir })).rejects.toThrow( + "sidecar files without coco.db", + ); + }); + it("refuses when both canonical and legacy configs exist", async () => { const base = root(); const legacyDir = join(base, ".cocod"); diff --git a/src/daemon/wallet/migration.ts b/src/daemon/wallet/migration.ts index ab55632..e4e5bc2 100644 --- a/src/daemon/wallet/migration.ts +++ b/src/daemon/wallet/migration.ts @@ -7,9 +7,9 @@ import { renameSync, rmSync, statSync, - unlinkSync, } from "fs"; -import { dirname, join } from "path"; +import { Database } from "bun:sqlite"; +import { basename, dirname, join } from "path"; import { legacyCocodDir, walletDir } from "./paths"; export type WalletMigrationResult = @@ -42,6 +42,68 @@ function filesEqual(left: string, right: string): boolean { return readFileSync(left).equals(readFileSync(right)); } +function sqlString(value: string): string { + return `'${value.replaceAll("'", "''")}'`; +} + +type WalletSummary = Record; +const SUMMARY_QUERIES: Record = { + proofs: + "SELECT mintUrl, state, COUNT(*) count, COALESCE(SUM(amount), 0) amount FROM coco_cashu_proofs GROUP BY mintUrl, state ORDER BY mintUrl, state", + counters: + "SELECT mintUrl, keysetId, counter FROM coco_cashu_counters ORDER BY mintUrl, keysetId", + mintOperations: + "SELECT state, COUNT(*) count FROM coco_cashu_mint_operations GROUP BY state ORDER BY state", + sendOperations: + "SELECT state, COUNT(*) count FROM coco_cashu_send_operations GROUP BY state ORDER BY state", + meltOperations: + "SELECT state, COUNT(*) count FROM coco_cashu_melt_operations GROUP BY state ORDER BY state", + mints: "SELECT mintUrl, trusted FROM coco_cashu_mints ORDER BY mintUrl", +}; + +function verifyDatabase(database: Database, label: string): WalletSummary { + const checks = database.query("PRAGMA quick_check").values() as unknown[][]; + if (checks.length !== 1 || checks[0]?.[0] !== "ok") { + throw new Error(`${label} failed PRAGMA quick_check: ${JSON.stringify(checks)}`); + } + + const tables = new Set( + (database + .query("SELECT name FROM sqlite_master WHERE type = 'table'") + .values() as string[][]).map(([name]) => name), + ); + const summary: WalletSummary = {}; + for (const [name, query] of Object.entries(SUMMARY_QUERIES)) { + const table = query.match(/FROM\s+(coco_cashu_\w+)/i)?.[1]; + summary[name] = table && tables.has(table) ? database.query(query).all() : []; + } + return summary; +} + +/** Write a standalone SQLite snapshot containing committed WAL frames. */ +function snapshotDatabase(sourcePath: string, destinationPath: string): void { + const source = new Database(sourcePath); + let sourceSummary: WalletSummary; + try { + sourceSummary = verifyDatabase(source, "Legacy wallet database"); + source.exec(`VACUUM INTO ${sqlString(destinationPath)}`); + } finally { + source.close(); + } + + const destination = new Database(destinationPath, { readonly: true }); + try { + const destinationSummary = verifyDatabase(destination, "Staged wallet database"); + if (JSON.stringify(sourceSummary) !== JSON.stringify(destinationSummary)) { + throw new Error( + "Staged wallet database does not contain the same proofs, counters, operations, and mints as the legacy database.", + ); + } + } finally { + destination.close(); + } +} + /** * Copy a legacy cocod wallet into the canonical routstrd wallet directory. * The staging directory is renamed atomically, so the canonical path is never @@ -56,6 +118,8 @@ export async function migrateLegacyWallet( const targetDb = join(targetDir, "coco.db"); const sourceConfig = join(sourceDir, "config.json"); const sourceDb = join(sourceDir, "coco.db"); + const sourceWal = `${sourceDb}-wal`; + const sourceShm = `${sourceDb}-shm`; const targetState = state(targetConfig, targetDb); const sourceState = state(sourceConfig, sourceDb); @@ -76,6 +140,12 @@ export async function migrateLegacyWallet( ); } if (targetState === "initialized") return { status: "already-current" }; + if (!existsSync(sourceDb) && (existsSync(sourceWal) || existsSync(sourceShm))) { + throw new Error( + `Cannot migrate wallet: ${sourceDir} contains SQLite sidecar files without coco.db. ` + + "Restore the matching main database before migration.", + ); + } if (targetState === "database-only" || sourceState === "database-only") { throw new Error( `Cannot migrate wallet: ${targetDir} is ${targetState} and ${sourceDir} is ${sourceState}. ` + @@ -102,15 +172,14 @@ export async function migrateLegacyWallet( chmodSync(stagedConfig, 0o600); const hasSourceDb = existsSync(sourceDb); if (hasSourceDb) { - copyFileSync(sourceDb, stagedDb); + // A SQLite WAL is part of the logical database. Raw-copying coco.db can + // silently omit proofs and counters that have not yet been checkpointed. + snapshotDatabase(sourceDb, stagedDb); chmodSync(stagedDb, 0o600); } - if ( - statSync(stagedConfig).size !== statSync(sourceConfig).size || - (hasSourceDb && statSync(stagedDb).size !== statSync(sourceDb).size) - ) { - throw new Error("Cannot migrate wallet: staged files failed size validation."); + if (statSync(stagedConfig).size !== statSync(sourceConfig).size) { + throw new Error("Cannot migrate wallet: staged config failed size validation."); } renameSync(stagingDir, targetDir); @@ -121,14 +190,18 @@ export async function migrateLegacyWallet( releaseLegacyLock?.(); } + // Preserve the complete legacy SQLite file set for manual recovery instead + // of deleting the only rollback copy immediately after migration. const cleanupWarnings: string[] = []; - for (const path of [sourceConfig, sourceDb]) { - if (!existsSync(path)) continue; + const sourceFiles = [sourceConfig, sourceDb, sourceWal, sourceShm].filter(existsSync); + if (sourceFiles.length > 0) { + const archiveDir = join(sourceDir, `wallet-migrated-${Date.now()}`); try { - unlinkSync(path); + mkdirSync(archiveDir, { mode: 0o700 }); + for (const path of sourceFiles) renameSync(path, join(archiveDir, basename(path))); } catch (error) { const message = error instanceof Error ? error.message : String(error); - cleanupWarnings.push(`Could not remove legacy wallet file ${path}: ${message}`); + cleanupWarnings.push(`Could not archive legacy wallet files: ${message}`); } } From 970206df5b4efb8af8b9e2385b693740869eca8d Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:45:11 +0100 Subject: [PATCH 29/32] fix(daemon): strip hop-by-hop headers when bridging provider responses Copying the upstream Transfer-Encoding: chunked onto our response while Node also frames the streamed body as chunked yields a duplicated 'Transfer-Encoding: chunked, chunked' header. Strict HTTP/1.x clients such as Go's net/http reject this with 'too many transfer encodings', breaking streaming clients like meat against the daemon. Drop all hop-by-hop headers (connection, keep-alive, proxy-*, te, trailer, transfer-encoding, upgrade) so Node derives correct framing (Content-Length for buffered bodies, chunked for streams) for this connection. --- src/daemon/http/index.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 0e6401b..d6dade6 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -21,6 +21,24 @@ import { decodeCashuTokenAmount } from "../wallet"; import { getClientsFromStore } from "../../utils/clients"; import { getUsageSummary } from "./usage-summary"; +// Hop-by-hop headers describe the *upstream* connection, not this one, and must +// never be copied onto our response. In particular, copying the upstream's +// `Transfer-Encoding: chunked` while Node also frames the streamed body as +// chunked produces a duplicated `Transfer-Encoding: chunked, chunked` header, +// which strict HTTP/1.x clients (e.g. Go's net/http) reject with +// "too many transfer encodings". Node derives Content-Length for buffered +// bodies and Transfer-Encoding: chunked for streams itself. +const HOP_BY_HOP_HEADERS = new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +]); + type ClientMode = "xcashu" | "lazyrefund" | "apikeys"; type WalletStatusOutput = { @@ -1512,10 +1530,13 @@ export function createDaemonRequestHandler(deps: { ...(deps.routstrPubkey ? { routstrPubkey: deps.routstrPubkey } : {}), }); - // Bridge the Web `Response` to the Node `ServerResponse` with no - // transforms: status + headers + pipe(body → res). + // Bridge the Web `Response` to the Node `ServerResponse`: status + + // headers + pipe(body → res). Hop-by-hop headers are dropped (see + // HOP_BY_HOP_HEADERS) so Node computes correct framing for THIS + // connection. res.statusCode = response.status; response.headers.forEach((value, key) => { + if (HOP_BY_HOP_HEADERS.has(key.toLowerCase())) return; res.setHeader(key, value); }); From 8e8285b78485b9acb0201c927935e214483a4d36 Mon Sep 17 00:00:00 2001 From: Ashen <310210685+ashen0x@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:03:22 +0530 Subject: [PATCH 30/32] fix(wallet): read Cashu metadata before receive --- src/daemon/http/index.ts | 8 +- src/daemon/wallet/index.ts | 30 +++++-- tests/wallet/short-keyset-token.test.ts | 106 ++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 tests/wallet/short-keyset-token.test.ts diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index d6dade6..cbf9d2e 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -17,7 +17,7 @@ import { type CocodClient, type CocodState, } from "../wallet/cocod-client"; -import { decodeCashuTokenAmount } from "../wallet"; +import { receiveCashuToken } from "../wallet"; import { getClientsFromStore } from "../../utils/clients"; import { getUsageSummary } from "./usage-summary"; @@ -389,9 +389,9 @@ export function createDaemonRequestHandler(deps: { await respond(res, async () => { const body = await readJsonBody(req); const token = getRequiredStringField(body, "token"); - const message = await deps.walletClient.receiveCashu(token); - const { amount, unit } = decodeCashuTokenAmount(token); - return { output: { message, amount, unit } }; + return { + output: await receiveCashuToken(deps.walletClient, token), + }; }); return; } diff --git a/src/daemon/wallet/index.ts b/src/daemon/wallet/index.ts index ce8123f..a031a10 100644 --- a/src/daemon/wallet/index.ts +++ b/src/daemon/wallet/index.ts @@ -1,4 +1,4 @@ -import { getDecodedToken, Amount } from "@cashu/cashu-ts"; +import { getTokenMetadata } from "@cashu/cashu-ts"; import { InsufficientBalanceError } from "@routstr/sdk"; import { WalletConnect } from "applesauce-wallet-connect"; import { RelayPool } from "applesauce-relay"; @@ -10,13 +10,28 @@ export function decodeCashuTokenAmount(token: string): { amount: number; unit: "sat" | "msat"; } { - const decoded = getDecodedToken(token, []); - const amount = - decoded?.proofs?.reduce((sum, proof) => sum + proof.amount.toNumber(), 0) ?? 0; - const unit = decoded?.unit === "msat" ? "msat" : "sat"; + // TokenV4 may contain an 8-byte short keyset ID. Fully decoding its + // proofs requires the mint's full keyset IDs, but amount and unit do not. + // getTokenMetadata intentionally extracts those fields without trying to + // map short IDs, unlike getDecodedToken(token, []). + const metadata = getTokenMetadata(token); + const amount = metadata.amount.toNumber(); + const unit = metadata.unit === "msat" ? "msat" : "sat"; return { amount, unit }; } +export async function receiveCashuToken( + client: Pick, + token: string, +): Promise<{ message: string; amount: number; unit: "sat" | "msat" }> { + // Validate the token before handing it to a state-changing wallet call. This + // prevents a successful receive from being reported as a failure if local + // metadata parsing ever rejects a future token format. + const { amount, unit } = decodeCashuTokenAmount(token); + const message = await client.receiveCashu(token); + return { message, amount, unit }; +} + export interface WalletAdapterOptions { cocodPath?: string | null; walletClient?: CocodClient; @@ -296,8 +311,7 @@ export async function createWalletAdapter( message?: string; }> { try { - const message = await client.receiveCashu(token); - const { amount, unit } = decodeCashuTokenAmount(token); + const { amount, unit, message } = await receiveCashuToken(client, token); return { success: true, amount, unit, message }; } catch (error) { const errorMessage = @@ -343,4 +357,4 @@ export async function createWalletAdapter( } return walletAdapter; -} \ No newline at end of file +} diff --git a/tests/wallet/short-keyset-token.test.ts b/tests/wallet/short-keyset-token.test.ts new file mode 100644 index 0000000..cc3ee30 --- /dev/null +++ b/tests/wallet/short-keyset-token.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it, mock } from "bun:test"; +import { + Amount, + getDecodedToken, + getEncodedToken, +} from "@cashu/cashu-ts"; +import { + createWalletAdapter, + decodeCashuTokenAmount, +} from "../../src/daemon/wallet"; +import type { CocodClient } from "../../src/daemon/wallet/cocod-client"; + +// A full modern keyset ID with the same format as Minibits' post-migration +// active keyset. getEncodedToken stores only its first eight bytes in TokenV4. +const FULL_KEYSET_ID = + "01fc0ec0e59cd6fa01b7a88f8cd77fce81fd1e64bca67d752e984992b7a3c3a821"; +const LEGACY_KEYSET_ID = "00107937db0cc865"; + +function makeToken({ + amounts = [5], + keysetId = FULL_KEYSET_ID, + unit = "sat", +}: { + amounts?: number[]; + keysetId?: string; + unit?: string; +} = {}): string { + return getEncodedToken({ + mint: "https://mint.minibits.cash/Bitcoin", + unit, + proofs: amounts.map((amount, index) => ({ + id: keysetId, + amount: Amount.from(amount), + secret: `short-keyset-regression-fixture-${index}`, + C: `02${(index + 1).toString(16).padStart(2, "0").repeat(32)}`, + })), + }); +} + +function makeWalletClient( + receiveCashu: CocodClient["receiveCashu"], +): CocodClient { + // createWalletAdapter is intentionally lazy; this receive-path test only + // needs the one capability exercised by receiveToken. + return { receiveCashu } as CocodClient; +} + +describe("short keyset TokenV4 compatibility", () => { + it("reads amount metadata without resolving the shortened proof keyset ID", () => { + const token = makeToken({ amounts: [1, 4] }); + + // Guard the fixture itself: a full proof decode with no mint keysets must + // exercise the same short-ID failure that triggered this regression. + expect(() => getDecodedToken(token, [])).toThrow(/short keyset ID/i); + expect(decodeCashuTokenAmount(token)).toEqual({ + amount: 5, + unit: "sat", + }); + }); + + for (const tokenCase of [ + { + name: "modern keyset with msat unit", + input: { amounts: [500, 1000], unit: "msat" }, + expected: { amount: 1500, unit: "msat" as const }, + }, + { + name: "legacy v0 keyset", + input: { amounts: [2, 8], keysetId: LEGACY_KEYSET_ID }, + expected: { amount: 10, unit: "sat" as const }, + }, + ]) { + it(`reads ${tokenCase.name} metadata`, () => { + expect(decodeCashuTokenAmount(makeToken(tokenCase.input))).toEqual( + tokenCase.expected, + ); + }); + } + + it("reports success after cocod receives a short-keyset token", async () => { + const receiveCashu = mock(async () => "Received 5"); + const walletClient = makeWalletClient(receiveCashu); + const adapter = await createWalletAdapter({ walletClient }); + + const result = await adapter.receiveToken(makeToken()); + + expect(receiveCashu).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + success: true, + amount: 5, + unit: "sat", + message: "Received 5", + }); + }); + + it("does not call the wallet when token metadata is invalid", async () => { + const receiveCashu = mock(async () => "should not be called"); + const walletClient = makeWalletClient(receiveCashu); + const adapter = await createWalletAdapter({ walletClient }); + + const result = await adapter.receiveToken("cashuBnot-a-valid-token"); + + expect(receiveCashu).not.toHaveBeenCalled(); + expect(result.success).toBe(false); + }); +}); From 35fdd0e8ca3aca7506c5764ac90a60f0839f38b2 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:05:28 +0100 Subject: [PATCH 31/32] feat(logs): route coco wallet-engine diagnostics to separate coco-logs Refactor the file logger into a createLogger(logsDir) factory so the daemon can maintain two independent sinks: the main daemon logs at ~/.routstrd/logs/ and the Cashu wallet-engine (coco) diagnostics at ~/.routstrd/coco-logs/. Coco diagnostics were polluting the main daemon logs even though their metadata is already scrubbed through the allowlist. Writing them to a separate directory keeps the main logs clean while retaining startup counts and operation IDs for debugging. - src/utils/logger.ts: add COCO_LOGS_DIR, FileLogger interface, and createLogger(); export logger and cocoLogger - src/daemon/wallet/coco-client.ts: log coco diagnostics via cocoLogger - src/cli.ts: add -c/--coco flag to the logs command to view wallet engine logs; make getLogFileForDate dir-aware - src/start-daemon.ts: drop unused existsSync import - SKILL.md: document the new flag and coco-logs directory --- SKILL.md | 6 ++-- src/cli.ts | 16 ++++++---- src/daemon/wallet/coco-client.ts | 7 ++-- src/start-daemon.ts | 1 - src/utils/logger.ts | 55 +++++++++++++++++--------------- 5 files changed, 47 insertions(+), 38 deletions(-) diff --git a/SKILL.md b/SKILL.md index 0ac5272..67ec639 100644 --- a/SKILL.md +++ b/SKILL.md @@ -191,9 +191,10 @@ View daemon logs. | Option | Default | Description | |--------|---------|-------------| | `-f, --follow` | false | Follow log output (like `tail -f`) | +| `-c, --coco` | false | Show Cashu wallet-engine (coco) logs instead of daemon logs | | `-n, --lines ` | 50 | Number of lines to show | -Log files are stored at `~/.routstrd/logs/YYYY-MM-DD.log`. +Log files are stored at `~/.routstrd/logs/YYYY-MM-DD.log`. Wallet-engine (Cashu/coco) diagnostics go to a separate `~/.routstrd/coco-logs/YYYY-MM-DD.log` so they don't pollute the main daemon logs. ## Wallet Commands @@ -317,4 +318,5 @@ When `routstrd onboard` runs, it automatically configures a `routstr` provider i | `~/.routstrd/routstr.db` | SQLite database | | `~/.routstrd/routstrd.sock` | IPC socket | | `~/.routstrd/routstrd.pid` | PID file | -| `~/.routstrd/logs/YYYY-MM-DD.log` | Daily log files | +| `~/.routstrd/logs/YYYY-MM-DD.log` | Daily daemon log files | +| `~/.routstrd/coco-logs/YYYY-MM-DD.log` | Daily Cashu wallet-engine (coco) log files | diff --git a/src/cli.ts b/src/cli.ts index cd38571..fd3ae3f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -25,7 +25,7 @@ import { LOGS_DIR, type RoutstrdConfig, } from "./utils/config"; -import { logger } from "./utils/logger"; +import { COCO_LOGS_DIR, logger } from "./utils/logger"; import { setupIntegration, runIntegrationsForClients } from "./integrations"; import { stopLegacyCocod } from "./daemon/wallet/coco-client"; import { getClientsList } from "./utils/clients"; @@ -1948,11 +1948,11 @@ program }); // Logs -function getLogFileForDate(date: Date = new Date()): string { +function getLogFileForDate(logsDir: string, date: Date = new Date()): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); - return `${LOGS_DIR}/${year}-${month}-${day}.log`; + return `${logsDir}/${year}-${month}-${day}.log`; } function readLastLines(file: string, lines: number): string { @@ -1993,17 +1993,19 @@ program .command("logs") .description("View daemon logs") .option("-f, --follow", "Follow log output", false) + .option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false) .option("-n, --lines ", "Number of lines to show", "50") - .action(async (options: { follow: boolean; lines: string }) => { + .action(async (options: { follow: boolean; lines: string; coco: boolean }) => { await requireLocalDaemon(); - const todayFile = getLogFileForDate(); + const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR; + const todayFile = getLogFileForDate(logsDir); const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); - const yesterdayFile = getLogFileForDate(yesterday); + const yesterdayFile = getLogFileForDate(logsDir, yesterday); if (!existsSync(todayFile) && !existsSync(yesterdayFile)) { console.log("No log files found. Daemon may not have started yet."); - console.log(`Logs directory: ${LOGS_DIR}`); + console.log(`Logs directory: ${logsDir}`); process.exit(1); } diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 2b351a3..2b6bc1b 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -30,7 +30,7 @@ import type { NpcAddress, NpcUsernameResult, } from "./cocod-client"; -import { logger } from "../../utils/logger"; +import { cocoLogger, logger } from "../../utils/logger"; const NPC_DEFAULT_BASE_URL = "https://npubx.cash"; @@ -148,9 +148,10 @@ function createCocoLogger(bindings: Record = {}): CocoLogger { ) => { // Coco diagnostics may contain proof secrets or encoded tokens. Keep only // an explicit metadata allowlist; startup counts and operation IDs remain - // useful without copying wallet material into routstrd's logs. + // useful without copying wallet material into routstrd's logs. Written to + // ~/.routstrd/coco-logs/ so wallet-engine noise stays out of the main logs. const metadata = safeCocoMetadata([bindings, ...meta]); - logger[level]( + cocoLogger[level]( `[coco] ${message}`, ...(Object.keys(metadata).length > 0 ? [metadata] : []), ); diff --git a/src/start-daemon.ts b/src/start-daemon.ts index 9e45702..a9c58a5 100644 --- a/src/start-daemon.ts +++ b/src/start-daemon.ts @@ -9,7 +9,6 @@ import { logger } from "./utils/logger"; import { CONFIG_DIR, LOGS_DIR } from "./utils/config"; import { withCrossProcessLock } from "./utils/process-lock"; import { fileURLToPath } from "url"; -import { existsSync } from "fs"; const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`; const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 2a4311c..fa77fb6 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -4,25 +4,27 @@ import { join } from "path"; const HOME = process.env.HOME || process.env.USERPROFILE || ""; const LOG_DIR = process.env.ROUTSTRD_DIR || `${HOME}/.routstrd`; const LOGS_DIR = join(LOG_DIR, "logs"); +/** Wallet-engine (coco-core / Cashu) diagnostics land in their own directory. */ +export const COCO_LOGS_DIR = join(LOG_DIR, "coco-logs"); -function getLogFileForDate(date: Date = new Date()): string { +function getLogFileForDate(logsDir: string, date: Date = new Date()): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); - return join(LOGS_DIR, `${year}-${month}-${day}.log`); + return join(logsDir, `${year}-${month}-${day}.log`); } -function ensureLogDir() { - if (!existsSync(LOGS_DIR)) { - mkdirSync(LOGS_DIR, { recursive: true }); +function ensureDir(dir: string) { + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); } } // NOTE: writes are synchronous on purpose — the daemon calls process.exit() // right after logger.error(...) on fatal paths, and async writes were being // silently dropped, making startup failures invisible. -function writeLog(level: string, ...args: unknown[]) { - ensureLogDir(); +function writeLog(logsDir: string, level: string, ...args: unknown[]) { + ensureDir(logsDir); const timestamp = new Date().toISOString(); const message = args .map((a) => { @@ -40,7 +42,7 @@ function writeLog(level: string, ...args: unknown[]) { }) .join(" "); const line = `[${timestamp}] [${level}] ${message}\n`; - const logFile = getLogFileForDate(new Date(timestamp)); + const logFile = getLogFileForDate(logsDir, new Date(timestamp)); try { appendFileSync(logFile, line); } catch (error) { @@ -48,20 +50,23 @@ function writeLog(level: string, ...args: unknown[]) { } } -export const logger = { - log: (...args: unknown[]) => { - writeLog("INFO", ...args); - }, - debug: (...args: unknown[]) => { - writeLog("DEBUG", ...args); - }, - warn: (...args: unknown[]) => { - writeLog("WARN", ...args); - }, - error: (...args: unknown[]) => { - writeLog("ERROR", ...args); - }, - info: (...args: unknown[]) => { - writeLog("INFO", ...args); - }, -}; +export interface FileLogger { + log: (...args: unknown[]) => void; + debug: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + info: (...args: unknown[]) => void; +} + +function createLogger(logsDir: string): FileLogger { + return { + log: (...args) => writeLog(logsDir, "INFO", ...args), + debug: (...args) => writeLog(logsDir, "DEBUG", ...args), + warn: (...args) => writeLog(logsDir, "WARN", ...args), + error: (...args) => writeLog(logsDir, "ERROR", ...args), + info: (...args) => writeLog(logsDir, "INFO", ...args), + }; +} + +export const logger = createLogger(LOGS_DIR); +export const cocoLogger = createLogger(COCO_LOGS_DIR); From e39dbb87d55d4b89905e78561a326277f4084952 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:48:54 +0100 Subject: [PATCH 32/32] bumped sdk version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 410ccec..43491d6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@cashu/cashu-ts": "^4.3.0", "@cashu/coco-core": "^1.0.1", "@cashu/coco-sqlite-bun": "^1.0.1", - "@routstr/sdk": "^0.3.19", + "@routstr/sdk": "^0.3.20", "@scure/bip39": "^2.2.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0",