diff --git a/.env.development b/.env.development index fd2d9c443a..757cd611ce 100644 --- a/.env.development +++ b/.env.development @@ -38,6 +38,9 @@ TWITTER_POSTER_API_KEY_SECRET= TWITTER_POSTER_ACCESS_TOKEN= TWITTER_POSTER_ACCESS_TOKEN_SECRET= +# Breez SDK +NEXT_PUBLIC_BREEZ_SDK_API_KEY= + ######################################## # SNDEV STUFF WE PRESET # # which you can override in .env.local # diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 061ec837b0..70ba020807 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -119,6 +119,11 @@ const typeDefs = gql` noffer: String! ): WalletRecvClink! + upsertWalletSendBreezSpark( + ${shared}, + mnemonic: VaultEntryInput! + ): WalletSendBreezSpark! + # tests testWalletRecvNWC( url: String! @@ -235,6 +240,7 @@ const typeDefs = gql` | WalletSendLNC | WalletSendCLNRest | WalletSendClink + | WalletSendBreezSpark | WalletRecvNWC | WalletRecvLNbits | WalletRecvPhoenixd @@ -309,6 +315,11 @@ const typeDefs = gql` secretKey: VaultEntry! } + type WalletSendBreezSpark { + id: ID! + mnemonic: VaultEntry! + } + type WalletRecvNWC { id: ID! url: String! diff --git a/package-lock.json b/package-lock.json index 9779d4db78..636048fbd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@apollo/server": "^4.11.0", "@as-integrations/next": "^3.1.0", "@auth/prisma-adapter": "^2.7.0", + "@breeztech/breez-sdk-spark": "^0.2.1", "@cashu/cashu-ts": "^2.4.1", "@graphql-tools/schema": "^10.0.6", "@lightninglabs/lnc-web": "^0.3.2-alpha", @@ -31,6 +32,8 @@ "async-retry": "^1.3.3", "aws-sdk": "^2.1691.0", "bech32": "^2.0.0", + "better-sqlite3": "^9.6.0", + "bip39": "^3.1.0", "bolt11": "^1.4.1", "bootstrap": "^5.3.3", "canonical-json": "0.0.4", @@ -2438,6 +2441,15 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@breeztech/breez-sdk-spark": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@breeztech/breez-sdk-spark/-/breez-sdk-spark-0.2.1.tgz", + "integrity": "sha512-x0EILZvkgmTzowKlSTNDtmWnKKvdGdVb9ToLN54sxqS9F6m5CdYZlatDZkZBFP/nvpIKNEq41YQWMUFbyFkoyA==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, "node_modules/@cashu/cashu-ts": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-2.4.1.tgz", @@ -7384,6 +7396,17 @@ "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" }, + "node_modules/better-sqlite3": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz", + "integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -7397,6 +7420,15 @@ "resolved": "https://registry.npmjs.org/bigi/-/bigi-1.4.2.tgz", "integrity": "sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw==" }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bip-schnorr": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/bip-schnorr/-/bip-schnorr-0.6.4.tgz", @@ -7439,6 +7471,27 @@ "node": ">=8.0.0" } }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/bip66": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", @@ -7505,6 +7558,41 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==" }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -8029,6 +8117,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -8943,6 +9037,15 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -10335,6 +10438,15 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -10542,6 +10654,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -10805,6 +10923,12 @@ "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -10986,6 +11110,12 @@ "assert-plus": "^1.0.0" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -11740,6 +11870,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", @@ -16100,6 +16236,12 @@ "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==" }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -16141,6 +16283,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -16293,6 +16441,30 @@ "integrity": "sha512-GSCXyoZBUaaPwVWdYncMEmzlSUjF9J/YeEHpklYJCyg8wPuJP3NzDx0BkiwArzINkdX2HJHvUJhL6vVWPOQQcg==", "deprecated": "Switch to namespaced @noble/secp256k1 for security and feature updates" }, + "node_modules/node-abi": { + "version": "3.77.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -17379,6 +17551,41 @@ "preact": ">=10" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/detect-libc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -17732,6 +17939,30 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -18968,6 +19199,51 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sister": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", @@ -19844,6 +20120,34 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", diff --git a/package.json b/package.json index 82bb6dfde4..adaf5294cb 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@apollo/server": "^4.11.0", "@as-integrations/next": "^3.1.0", "@auth/prisma-adapter": "^2.7.0", + "@breeztech/breez-sdk-spark": "^0.2.1", "@cashu/cashu-ts": "^2.4.1", "@graphql-tools/schema": "^10.0.6", "@lightninglabs/lnc-web": "^0.3.2-alpha", @@ -36,6 +37,8 @@ "async-retry": "^1.3.3", "aws-sdk": "^2.1691.0", "bech32": "^2.0.0", + "better-sqlite3": "^9.6.0", + "bip39": "^3.1.0", "bolt11": "^1.4.1", "bootstrap": "^5.3.3", "canonical-json": "0.0.4", diff --git a/prisma/migrations/20250923052230_breez_spark/migration.sql b/prisma/migrations/20250923052230_breez_spark/migration.sql new file mode 100644 index 0000000000..6968514267 --- /dev/null +++ b/prisma/migrations/20250923052230_breez_spark/migration.sql @@ -0,0 +1,44 @@ +-- AlterEnum +ALTER TYPE "WalletName" ADD VALUE 'BREEZ'; COMMIT; + +-- AlterEnum +ALTER TYPE "WalletProtocolName" ADD VALUE 'BREEZ_SPARK'; COMMIT; + +-- AlterEnum +ALTER TYPE "WalletSendProtocolName" ADD VALUE 'BREEZ_SPARK'; COMMIT; + +INSERT INTO "WalletTemplate" ("name", "sendProtocols", "recvProtocols") +VALUES ('BREEZ', '{BREEZ_SPARK}', '{LN_ADDR}'); + +-- CreateTable +CREATE TABLE "WalletSendBreezSpark" ( + "id" SERIAL NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "protocolId" INTEGER NOT NULL, + "mnemonicVaultId" INTEGER NOT NULL, + + CONSTRAINT "WalletSendBreezSpark_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletSendBreezSpark_protocolId_key" ON "WalletSendBreezSpark"("protocolId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletSendBreezSpark_mnemonicVaultId_key" ON "WalletSendBreezSpark"("mnemonicVaultId"); + +-- AddForeignKey +ALTER TABLE "WalletSendBreezSpark" ADD CONSTRAINT "WalletSendBreezSpark_protocolId_fkey" FOREIGN KEY ("protocolId") REFERENCES "WalletProtocol"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WalletSendBreezSpark" ADD CONSTRAINT "WalletSendBreezSpark_mnemonicVaultId_fkey" FOREIGN KEY ("mnemonicVaultId") REFERENCES "Vault"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +CREATE TRIGGER wallet_to_jsonb + AFTER INSERT OR UPDATE ON "WalletSendBreezSpark" + FOR EACH ROW + EXECUTE PROCEDURE wallet_to_jsonb(); + +CREATE TRIGGER wallet_clear_vault + AFTER DELETE ON "WalletSendBreezSpark" + FOR EACH ROW + EXECUTE PROCEDURE wallet_clear_vault(); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 88e141e023..3e03114144 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -211,18 +211,19 @@ model Vault { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") - walletSendNWC WalletSendNWC? - walletSendLNbits WalletSendLNbits? - walletSendPhoenixd WalletSendPhoenixd? - walletSendBlinkApiKey WalletSendBlink? @relation("blinkApiKeySend") - walletSendBlinkCurrency WalletSendBlink? @relation("blinkCurrencySend") - walletSendLNCPairingPhrase WalletSendLNC? @relation("lncPairingPhrase") - walletSendLNCLocalKey WalletSendLNC? @relation("lncLocalKey") - walletSendLNCRemoteKey WalletSendLNC? @relation("lncRemoteKey") - walletSendLNCServerHost WalletSendLNC? @relation("lncServerHost") - walletSendCLNRestRune WalletSendCLNRest? @relation("clnRune") - walletSendClink WalletSendClink? @relation("clinkNdebit") - walletSendClinkSecretKey WalletSendClink? @relation("clinkSecretKey") + walletSendNWC WalletSendNWC? + walletSendLNbits WalletSendLNbits? + walletSendPhoenixd WalletSendPhoenixd? + walletSendBlinkApiKey WalletSendBlink? @relation("blinkApiKeySend") + walletSendBlinkCurrency WalletSendBlink? @relation("blinkCurrencySend") + walletSendLNCPairingPhrase WalletSendLNC? @relation("lncPairingPhrase") + walletSendLNCLocalKey WalletSendLNC? @relation("lncLocalKey") + walletSendLNCRemoteKey WalletSendLNC? @relation("lncRemoteKey") + walletSendLNCServerHost WalletSendLNC? @relation("lncServerHost") + walletSendCLNRestRune WalletSendCLNRest? @relation("clnRune") + walletSendClink WalletSendClink? @relation("clinkNdebit") + walletSendClinkSecretKey WalletSendClink? @relation("clinkSecretKey") + WalletSendBreezSparkMnemonic WalletSendBreezSpark? @relation("breezSparkMnemonic") } model WalletLog { @@ -1216,6 +1217,7 @@ enum WalletProtocolName { CLN_REST LND_GRPC CLINK + BREEZ_SPARK } enum WalletSendProtocolName { @@ -1227,6 +1229,7 @@ enum WalletSendProtocolName { LNC CLN_REST CLINK + BREEZ_SPARK } enum WalletRecvProtocolName { @@ -1273,6 +1276,7 @@ enum WalletName { LN_ADDR CASH_APP BLITZ + BREEZ } model WalletTemplate { @@ -1323,14 +1327,15 @@ model WalletProtocol { invoiceForward InvoiceForward[] logs WalletLog[] - walletSendNWC WalletSendNWC? - walletSendLNbits WalletSendLNbits? - walletSendPhoenixd WalletSendPhoenixd? - walletSendBlink WalletSendBlink? - walletSendWebLN WalletSendWebLN? - walletSendLNC WalletSendLNC? - walletSendCLNRest WalletSendCLNRest? - walletSendClink WalletSendClink? + walletSendNWC WalletSendNWC? + walletSendLNbits WalletSendLNbits? + walletSendPhoenixd WalletSendPhoenixd? + walletSendBlink WalletSendBlink? + walletSendWebLN WalletSendWebLN? + walletSendLNC WalletSendLNC? + walletSendCLNRest WalletSendCLNRest? + walletSendClink WalletSendClink? + walletSendBreezSpark WalletSendBreezSpark? walletRecvNWC WalletRecvNWC? walletRecvLNbits WalletRecvLNbits? @@ -1436,6 +1441,16 @@ model WalletSendClink { secretKey Vault? @relation("clinkSecretKey", fields: [secretKeyVaultId], references: [id]) } +model WalletSendBreezSpark { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + protocolId Int @unique + protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade) + mnemonicVaultId Int @unique + mnemonic Vault? @relation("breezSparkMnemonic", fields: [mnemonicVaultId], references: [id]) +} + model WalletRecvNWC { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) @map("created_at") diff --git a/public/wallets/breez-dark.svg b/public/wallets/breez-dark.svg new file mode 100644 index 0000000000..57ba8fcdc5 --- /dev/null +++ b/public/wallets/breez-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/wallets/breez.svg b/public/wallets/breez.svg new file mode 100644 index 0000000000..c7b98772a2 --- /dev/null +++ b/public/wallets/breez.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/wallets/spark-dark.svg b/public/wallets/spark-dark.svg new file mode 100644 index 0000000000..302b527916 --- /dev/null +++ b/public/wallets/spark-dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/public/wallets/spark.svg b/public/wallets/spark.svg new file mode 100644 index 0000000000..80627e4ce3 --- /dev/null +++ b/public/wallets/spark.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/wallets/client/components/form/hooks.js b/wallets/client/components/form/hooks.js index 579ed176e8..48f3c53b43 100644 --- a/wallets/client/components/form/hooks.js +++ b/wallets/client/components/form/hooks.js @@ -84,8 +84,11 @@ function useProtocolFormState (protocol) { export function useProtocolForm (protocol) { const [formState, setFormState] = useProtocolFormState(protocol) + const [complementaryFormState] = useProtocolFormState({ name: protocol.name, send: !protocol.send }) const [nwcSendFormState] = useProtocolFormState({ name: 'NWC', send: true }) + const [breezSparkFormState] = useProtocolFormState({ name: 'BREEZ_SPARK', send: true }) + const wallet = useWallet() const lud16Domain = walletLud16Domain(wallet.name) const fields = protocolFields(protocol) @@ -94,15 +97,22 @@ export function useProtocolForm (protocol) { // after init, we use formState as the source of truth everywhere let value = formState?.config?.[field.name] ?? protocol.config?.[field.name] + if (!value && field.initial) { + value = typeof field.initial === 'function' ? field.initial() : field.initial + } + if (!value && field.share) { value = complementaryFormState?.config?.[field.name] } if (protocol.name === 'LN_ADDR' && field.name === 'address' && lud16Domain) { - // automatically set lightning addresses from NWC urls if lud16 parameter is present if (nwcSendFormState?.config?.url) { + // automatically set lightning addresses from NWC urls if lud16 parameter is present ... const { lud16 } = parseNwcUrl(nwcSendFormState.config.url) if (lud16?.split('@')[1] === lud16Domain) value = lud16 + } else if (breezSparkFormState?.config?.username) { + // ... or from spark usernames + value = `${breezSparkFormState.config.username}@${lud16Domain}` } // remove domain part since we will append it automatically if lud16Domain is set if (lud16Domain && value) { diff --git a/wallets/client/components/form/index.js b/wallets/client/components/form/index.js index 22acb24ece..bcf7a7e4f0 100644 --- a/wallets/client/components/form/index.js +++ b/wallets/client/components/form/index.js @@ -189,7 +189,7 @@ function WalletProtocolFormField ({ type, ...props }) { const [protocol] = useProtocol() const formik = useFormikContext() - function transform ({ validate, encrypt, editable, help, share, ...props }) { + function transform ({ validate, encrypt, editable, help, share, initial, ...props }) { const [upperHint, bottomHint] = Array.isArray(props.hint) ? props.hint : [null, props.hint] const parseHelpText = text => Array.isArray(text) ? text.join('\n\n') : text diff --git a/wallets/client/fragments/protocol.js b/wallets/client/fragments/protocol.js index 6d2175e386..51089186cf 100644 --- a/wallets/client/fragments/protocol.js +++ b/wallets/client/fragments/protocol.js @@ -265,6 +265,20 @@ export const UPSERT_WALLET_RECEIVE_CLINK = gql` } ` +export const UPSERT_WALLET_SEND_BREEZ_SPARK = gql` + mutation upsertWalletSendBreezSpark( + ${shared.variables}, + $mnemonic: VaultEntryInput! + ) { + upsertWalletSendBreezSpark( + ${shared.arguments}, + mnemonic: $mnemonic + ) { + id + } + } +` + // tests export const TEST_WALLET_RECEIVE_NWC = gql` diff --git a/wallets/client/fragments/wallet.js b/wallets/client/fragments/wallet.js index f018e1f549..13de86b503 100644 --- a/wallets/client/fragments/wallet.js +++ b/wallets/client/fragments/wallet.js @@ -87,6 +87,12 @@ const WALLET_PROTOCOL_FIELDS = gql` ...VaultEntryFields } } + ... on WalletSendBreezSpark { + id + encryptedMnemonic: mnemonic { + ...VaultEntryFields + } + } ... on WalletRecvNWC { id url diff --git a/wallets/client/hooks/query.js b/wallets/client/hooks/query.js index 83ece5eac1..40fc03a272 100644 --- a/wallets/client/hooks/query.js +++ b/wallets/client/hooks/query.js @@ -21,6 +21,7 @@ import { UPSERT_WALLET_SEND_WEBLN, UPSERT_WALLET_SEND_CLN_REST, UPSERT_WALLET_SEND_CLINK, + UPSERT_WALLET_SEND_BREEZ_SPARK, WALLETS, UPDATE_WALLET_ENCRYPTION, RESET_WALLETS, @@ -321,6 +322,8 @@ function protocolUpsertMutation (protocol) { return protocol.send ? UPSERT_WALLET_SEND_WEBLN : NOOP_MUTATION case 'CLINK': return protocol.send ? UPSERT_WALLET_SEND_CLINK : UPSERT_WALLET_RECEIVE_CLINK + case 'BREEZ_SPARK': + return protocol.send ? UPSERT_WALLET_SEND_BREEZ_SPARK : NOOP_MUTATION default: return NOOP_MUTATION } diff --git a/wallets/client/protocols/breezSpark.js b/wallets/client/protocols/breezSpark.js new file mode 100644 index 0000000000..23adcc20ad --- /dev/null +++ b/wallets/client/protocols/breezSpark.js @@ -0,0 +1,95 @@ +import init, { defaultConfig, connect } from '@breeztech/breez-sdk-spark' +import { withTimeout } from '@/lib/time' +import { getUsername } from '@/wallets/lib/protocols/breezSpark' + +export const name = 'BREEZ_SPARK' + +const SDK_SYNC_TIMEOUT_MS = 30_000 + +async function withSdk (mnemonic, cb) { + await init() + + const config = defaultConfig('mainnet') + // this API key should be kept a secret on a best effort basis + config.apiKey = process.env.NEXT_PUBLIC_BREEZ_SDK_API_KEY + config.lnurlDomain = 'breez.tips' + + const sdk = await connect({ + config, + seed: { type: 'mnemonic', mnemonic, passphrase: undefined }, + // the SDK will create a IndexedDB with this name to store data + storageDir: 'breez-sdk-spark' + }) + + try { + await waitForSync(sdk) + return await cb(sdk) + } finally { + sdk.disconnect() + } +} + +async function waitForSync (sdk) { + const syncPromise = new Promise( + resolve => sdk.addEventListener(new SyncEventListener(resolve)) + ) + await withTimeout(syncPromise, SDK_SYNC_TIMEOUT_MS) +} + +class SdkEventListener { + constructor (event, resolve) { + this.event = event + this.resolve = resolve + } + + onEvent (event) { + if (event.type === this.event) { + this.resolve() + } + } +} + +class SyncEventListener extends SdkEventListener { + constructor (resolve) { + super('synced', resolve) + } +} + +export async function sendPayment (bolt11, { mnemonic }, { signal }) { + return await withSdk( + mnemonic, + async sdk => { + const prepareResponse = await sdk.prepareSendPayment({ + paymentRequest: bolt11 + }) + + const sendResponse = await sdk.sendPayment({ + prepareResponse, + options: { type: 'bolt11Invoice', preferSpark: false } + }) + + return sendResponse.payment + } + ) +} + +export async function testSendPayment ({ mnemonic }, { signal }) { + return await withSdk( + mnemonic, + async sdk => { + const { paymentRequest: sparkAddress } = await sdk.receivePayment({ + // this will always return the same spark address for the same seed + paymentMethod: { type: 'sparkAddress' } + }) + + const username = getUsername(sparkAddress) + + const available = await sdk.checkLightningAddressAvailable({ username }) + if (available) { + await sdk.registerLightningAddress({ username, description: 'Running Spark SDK' }) + } + + return { username } + } + ) +} diff --git a/wallets/client/protocols/index.js b/wallets/client/protocols/index.js index 29efb9036c..1c448d7fc8 100644 --- a/wallets/client/protocols/index.js +++ b/wallets/client/protocols/index.js @@ -6,6 +6,7 @@ import * as webln from './webln' import * as lnc from './lnc' import * as clnRest from './clnRest' import * as clink from './clink' +import * as breezSpark from './breezSpark' export * from './util' @@ -56,5 +57,6 @@ export default [ webln, lnc, clnRest, - clink + clink, + breezSpark ] diff --git a/wallets/lib/crypto.js b/wallets/lib/crypto.js index e59b7fc99b..5e9b536de4 100644 --- a/wallets/lib/crypto.js +++ b/wallets/lib/crypto.js @@ -1,4 +1,5 @@ import bip39Words from '@/lib/bip39-words' +import * as bip39 from 'bip39' export async function deriveKey (passphrase, salt) { const enc = new TextEncoder() @@ -77,7 +78,5 @@ export async function decrypt (key, { iv, value }) { } export function generateRandomPassphrase () { - const rand = new Uint32Array(12) - window.crypto.getRandomValues(rand) - return Array.from(rand).map(i => bip39Words[i % bip39Words.length]).join(' ') + return bip39.generateMnemonic(128, null, bip39Words) } diff --git a/wallets/lib/protocols/breezSpark.js b/wallets/lib/protocols/breezSpark.js new file mode 100644 index 0000000000..4eaa0607ca --- /dev/null +++ b/wallets/lib/protocols/breezSpark.js @@ -0,0 +1,43 @@ +import { bip39Validator } from '@/wallets/lib/validate' +import { generateRandomPassphrase } from '@/wallets/lib/crypto' +import { bech32m } from 'bech32' + +// Spark +// https://github.com/breez/spark-sdk +// https://sdk-doc-spark.breez.technology/ +// https://breez.github.io/spark-sdk/breez_sdk_spark/ + +export default { + name: 'BREEZ_SPARK', + send: true, + displayName: 'Spark', + fields: [ + { + name: 'mnemonic', + label: 'mnemonic', + type: 'password', + required: true, + validate: bip39Validator(), + encrypt: true, + initial: generateRandomPassphrase, + disabled: true + }, + { + name: 'username' + } + ], + relationName: 'walletSendBreezSpark' +} + +function getIdentityPublicKey (sparkAddress) { + const decoded = bech32m.decode(sparkAddress) + const pubkey = Buffer.from(bech32m.fromWords(decoded.words)).toString('hex').slice(4) + return pubkey +} + +export function getUsername (sparkAddress) { + const identityPublicKey = getIdentityPublicKey(sparkAddress) + // max lnurl username length is 64 characters + // https://github.com/breez/spark-sdk/blob/71b8cb097d2bc846be427599b1bf44eae897786f/crates/breez-sdk/lnurl/src/routes.rs#L326 + return identityPublicKey.slice(0, 64) +} diff --git a/wallets/lib/protocols/index.js b/wallets/lib/protocols/index.js index f89a59af5e..40e516dc6b 100644 --- a/wallets/lib/protocols/index.js +++ b/wallets/lib/protocols/index.js @@ -8,10 +8,11 @@ import phoenixdSuite from './phoenixd' import blinkSuite from './blink' import webln from './webln' import clinkSuite from './clink' +import breezSpark from './breezSpark' /** * Protocol names as used in the database - * @typedef {'NWC'|'LNBITS'|'PHOENIXD'|'BLINK'|'WEBLN'|'LN_ADDR'|'LNC'|'CLN_REST'|'LND_GRPC'|'CLINK'} ProtocolName + * @typedef {'NWC'|'LNBITS'|'PHOENIXD'|'BLINK'|'WEBLN'|'LN_ADDR'|'LNC'|'CLN_REST'|'LND_GRPC'|'CLINK'|'BREEZ_SPARK'} ProtocolName * @typedef {'text'|'password'} InputType */ @@ -49,5 +50,6 @@ export default [ ...lnbitsSuite, ...blinkSuite, webln, - ...clinkSuite + ...clinkSuite, + breezSpark ] diff --git a/wallets/lib/validate.js b/wallets/lib/validate.js index 9c3945749d..3b545eca1a 100644 --- a/wallets/lib/validate.js +++ b/wallets/lib/validate.js @@ -1,4 +1,5 @@ import bip39Words from '@/lib/bip39-words' +import * as bip39 from 'bip39' import { decodeRune } from '@/lib/cln' import { B64_URL_REGEX } from '@/lib/format' import { isInvoicableMacaroon, isInvoiceMacaroon } from '@/lib/macaroon' @@ -147,6 +148,12 @@ export const bip39Validator = ({ min = 12, max = 24 } = {}) => .test({ name: 'bip39', test: async (value, context) => { + // legacy mnemonics don't include a checksum so if validation with the + // bip39 library fails we assume it's an old mnemonic and use legacy validation + if (bip39.validateMnemonic(value, bip39Words)) { + return true + } + const words = value ? value.trim().split(/[\s]+/) : [] for (const w of words) { try { diff --git a/wallets/lib/wallets.json b/wallets/lib/wallets.json index 1975a9d26b..8164943a7f 100644 --- a/wallets/lib/wallets.json +++ b/wallets/lib/wallets.json @@ -168,5 +168,13 @@ "displayName": "Blitz Wallet", "image": "/wallets/blitz.png", "url": "https://blitz-wallet.com/" + }, + { + "name": "BREEZ", + "displayName": "Breez", + "image": "/wallets/breez.svg", + "url": { + "lud16Domain": "breez.tips" + } } ] diff --git a/wallets/server/resolvers/util.js b/wallets/server/resolvers/util.js index 5553ef4dfe..8a350bfa0d 100644 --- a/wallets/server/resolvers/util.js +++ b/wallets/server/resolvers/util.js @@ -21,6 +21,8 @@ export function mapWalletResolveTypes (wallet) { return 'WalletRecvLNDGRPC' case 'CLINK': return send ? 'WalletSendClink' : 'WalletRecvClink' + case 'BREEZ_SPARK': + return 'WalletSendBreezSpark' default: return null }