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
}