diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index ce690feb..6284b821 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -108,6 +108,11 @@ const typeDefs = gql` ${shared} ): WalletSendWebLN! + upsertWalletRecvClink( + ${shared}, + noffer: String! + ): WalletRecvClink! + # tests testWalletRecvNWC( url: String! @@ -144,6 +149,10 @@ const typeDefs = gql` apiKey: String! ): Boolean! + testWalletRecvClink( + noffer: String! + ): Boolean! + # delete deleteWallet(id: ID!): Boolean @@ -226,6 +235,7 @@ const typeDefs = gql` | WalletRecvLightningAddress | WalletRecvCLNRest | WalletRecvLNDGRPC + | WalletRecvClink type WalletSettings { receiveCreditsBelowSats: Int! @@ -328,6 +338,11 @@ const typeDefs = gql` cert: String } + type WalletRecvClink { + id: ID! + noffer: String! + } + input AutowithdrawSettings { autoWithdrawThreshold: Int! autoWithdrawMaxFeePercent: Float! diff --git a/docker-compose.yml b/docker-compose.yml index 2100fe57..8b970bed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -843,6 +843,27 @@ services: CONNECT: "localhost:${LNBITS_WEB_PORT_V1}" TORDIR: "/app/.tor" cpu_shares: "${CPU_SHARES_LOW}" + lnpub: + image: ghcr.io/shocknet/lightning-pub@sha256:cd7bb9298d09a2cdaf1b6456ef6154e3ba24f7b902ad29cda2c08c2a4fa2af6e + container_name: lnpub + profiles: + - wallets + restart: unless-stopped + volumes: + - lnpub:/app/data + - lnd:/app/.lnd + environment: + - LND_ADDRESS=lnd:10009 + - LND_CERT_PATH=/app/.lnd/tls.cert + - LND_MACAROON_PATH=/app/.lnd/data/chain/bitcoin/regtest/admin.macaroon + ports: + - ${LNPUB_PORT_1776:-1776}:1776 + - ${LNPUB_PORT_1777:-1777}:1777 + depends_on: + lnd: + condition: service_healthy + restart: true + cpu_shares: "${CPU_SHARES_LOW}" dnsmasq: image: 4km3/dnsmasq:2.90-r3 profiles: @@ -879,6 +900,7 @@ volumes: tordata: eclair: dnsmasq: + lnpub: networks: default: {} diff --git a/package-lock.json b/package-lock.json index 0d48857f..3f1b0091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@nostr-dev-kit/ndk-wallet": "^0.5.0", "@opensearch-project/opensearch": "^2.12.0", "@prisma/client": "^5.20.0", + "@shocknet/clink-sdk": "^1.4.0", "@slack/web-api": "^7.6.0", "@svgr/webpack": "^8.1.0", "@yudiel/react-qr-scanner": "^2.0.8", @@ -3236,11 +3237,31 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3257,7 +3278,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "engines": { "node": ">=12" }, @@ -3269,7 +3289,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -3280,14 +3299,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -3304,7 +3321,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3319,7 +3335,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -5269,6 +5284,137 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@shocknet/clink-sdk": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@shocknet/clink-sdk/-/clink-sdk-1.4.0.tgz", + "integrity": "sha512-J0PWE8CVRJrFF1Zi/UhChhvOrlmDj7LRJTpR6rbHlFPmjC5TGIW6891tVWWv+JmUR0jzez9QHFrHnc8DgIJYCQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.8.0", + "@scure/base": "^1.2.5", + "nostr-tools": "^2.13.0", + "rimraf": "^6.0.1", + "typescript": "^5.8.3" + } + }, + "node_modules/@shocknet/clink-sdk/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/@shocknet/clink-sdk/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/lru-cache": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -8308,10 +8454,10 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -8993,8 +9139,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ecc-jsbn": { "version": "0.1.2", @@ -10466,12 +10611,12 @@ } }, "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -10485,7 +10630,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -12082,8 +12226,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isomorphic-ws": { "version": "5.0.0", @@ -15874,7 +16017,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -16157,19 +16299,18 @@ } }, "node_modules/nostr-tools": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.8.0.tgz", - "integrity": "sha512-aumZBa9Ok/cAJLovSBCIA/DkJjLjF/Hs5DpQGEjmyfaUkGBqd5jZjzalcVMyy/9HkkRZfJmbTPtqHTKFNvBSHQ==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.16.2.tgz", + "integrity": "sha512-ZxH9EbSt5ypURZj2TGNJxZd0Omb5ag5KZSu8IyJMCdLyg2KKz+2GA0sP/cSawCQEkyviIN4eRT4G2gB/t9lMRw==", + "license": "Unlicense", "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" - }, - "optionalDependencies": { - "nostr-wasm": "v0.1.0" + "@scure/bip39": "1.2.1", + "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" @@ -16205,8 +16346,7 @@ "node_modules/nostr-wasm": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", - "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", - "optional": true + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==" }, "node_modules/npm-run-path": { "version": "4.0.1", @@ -16586,6 +16726,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", @@ -16681,7 +16827,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -18699,7 +18844,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -18711,7 +18855,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -19368,7 +19511,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -19488,7 +19630,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -20320,9 +20461,9 @@ "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -21185,7 +21326,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -21538,7 +21678,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -21555,7 +21694,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -21570,7 +21708,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -21581,8 +21718,7 @@ "node_modules/wrap-ansi-cjs/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", diff --git a/package.json b/package.json index faa2c4e2..bc640437 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@nostr-dev-kit/ndk-wallet": "^0.5.0", "@opensearch-project/opensearch": "^2.12.0", "@prisma/client": "^5.20.0", + "@shocknet/clink-sdk": "^1.4.0", "@slack/web-api": "^7.6.0", "@svgr/webpack": "^8.1.0", "@yudiel/react-qr-scanner": "^2.0.8", diff --git a/prisma/migrations/20250905014333_clink_recv/migration.sql b/prisma/migrations/20250905014333_clink_recv/migration.sql new file mode 100644 index 00000000..24d7b0d9 --- /dev/null +++ b/prisma/migrations/20250905014333_clink_recv/migration.sql @@ -0,0 +1,31 @@ +-- AlterEnum +ALTER TYPE "WalletProtocolName" ADD VALUE 'CLINK'; COMMIT; + +-- AlterEnum +ALTER TYPE "WalletRecvProtocolName" ADD VALUE 'CLINK'; COMMIT; + +UPDATE "WalletTemplate" +SET "recvProtocols" = array_prepend('CLINK', "recvProtocols") +WHERE "name" = 'SHOCKWALLET'; + +-- CreateTable +CREATE TABLE "WalletRecvClink" ( + "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, + "noffer" TEXT NOT NULL, + + CONSTRAINT "WalletRecvClink_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletRecvClink_protocolId_key" ON "WalletRecvClink"("protocolId"); + +-- AddForeignKey +ALTER TABLE "WalletRecvClink" ADD CONSTRAINT "WalletRecvClink_protocolId_fkey" FOREIGN KEY ("protocolId") REFERENCES "WalletProtocol"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE TRIGGER wallet_to_jsonb + AFTER INSERT OR UPDATE ON "WalletRecvClink" + FOR EACH ROW + EXECUTE PROCEDURE wallet_to_jsonb(); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7aea842b..4f3cb9e2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1213,6 +1213,7 @@ enum WalletProtocolName { LNC CLN_REST LND_GRPC + CLINK } enum WalletSendProtocolName { @@ -1233,6 +1234,7 @@ enum WalletRecvProtocolName { LN_ADDR CLN_REST LND_GRPC + CLINK } enum WalletProtocolStatus { @@ -1333,6 +1335,7 @@ model WalletProtocol { walletRecvLightningAddress WalletRecvLightningAddress? walletRecvCLNRest WalletRecvCLNRest? walletRecvLNDGRPC WalletRecvLNDGRPC? + walletRecvClink WalletRecvClink? @@unique(name: "WalletProtocol_walletId_send_name_key", [walletId, send, name]) @@index([walletId]) @@ -1486,3 +1489,12 @@ model WalletRecvLNDGRPC { macaroon String cert String? } + +model WalletRecvClink { + 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) + noffer String +} diff --git a/wallets/client/fragments/protocol.js b/wallets/client/fragments/protocol.js index 04d107e7..8b132e82 100644 --- a/wallets/client/fragments/protocol.js +++ b/wallets/client/fragments/protocol.js @@ -235,6 +235,20 @@ export const UPSERT_WALLET_SEND_WEBLN = gql` } ` +export const UPSERT_WALLET_RECEIVE_CLINK = gql` + mutation upsertWalletRecvClink( + ${shared.variables}, + $noffer: String! + ) { + upsertWalletRecvClink( + ${shared.arguments}, + noffer: $noffer + ) { + id + } + } +` + // tests export const TEST_WALLET_RECEIVE_NWC = gql` @@ -278,3 +292,9 @@ export const TEST_WALLET_RECEIVE_BLINK = gql` testWalletRecvBlink(currency: $currency, apiKey: $apiKey) } ` + +export const TEST_WALLET_RECEIVE_CLINK = gql` + mutation testWalletRecvClink($noffer: String!) { + testWalletRecvClink(noffer: $noffer) + } +` diff --git a/wallets/client/fragments/wallet.js b/wallets/client/fragments/wallet.js index b8acf0e1..6d8676cc 100644 --- a/wallets/client/fragments/wallet.js +++ b/wallets/client/fragments/wallet.js @@ -113,6 +113,10 @@ const WALLET_PROTOCOL_FIELDS = gql` macaroon cert } + ... on WalletRecvClink { + id + noffer + } } } ` diff --git a/wallets/client/hooks/query.js b/wallets/client/hooks/query.js index 56ad860f..acb06678 100644 --- a/wallets/client/hooks/query.js +++ b/wallets/client/hooks/query.js @@ -6,6 +6,7 @@ import { UPSERT_WALLET_RECEIVE_LND_GRPC, UPSERT_WALLET_RECEIVE_NWC, UPSERT_WALLET_RECEIVE_PHOENIXD, + UPSERT_WALLET_RECEIVE_CLINK, UPSERT_WALLET_SEND_BLINK, UPSERT_WALLET_SEND_LNBITS, UPSERT_WALLET_SEND_LNC, @@ -26,6 +27,7 @@ import { TEST_WALLET_RECEIVE_NWC, TEST_WALLET_RECEIVE_CLN_REST, TEST_WALLET_RECEIVE_LND_GRPC, + TEST_WALLET_RECEIVE_CLINK, DELETE_WALLET } from '@/wallets/client/fragments' import { gql, useApolloClient, useMutation, useQuery } from '@apollo/client' @@ -315,6 +317,8 @@ function protocolUpsertMutation (protocol) { return protocol.send ? UPSERT_WALLET_SEND_LNC : NOOP_MUTATION case 'WEBLN': return protocol.send ? UPSERT_WALLET_SEND_WEBLN : NOOP_MUTATION + case 'CLINK': + return protocol.send ? NOOP_MUTATION : UPSERT_WALLET_RECEIVE_CLINK default: return NOOP_MUTATION } @@ -338,6 +342,8 @@ function protocolTestMutation (protocol) { return TEST_WALLET_RECEIVE_CLN_REST case 'LND_GRPC': return TEST_WALLET_RECEIVE_LND_GRPC + case 'CLINK': + return TEST_WALLET_RECEIVE_CLINK default: return NOOP_MUTATION } diff --git a/wallets/lib/protocols/clink.js b/wallets/lib/protocols/clink.js new file mode 100644 index 00000000..3b5a55ef --- /dev/null +++ b/wallets/lib/protocols/clink.js @@ -0,0 +1,21 @@ +import { clinkValidator } from '@/wallets/lib/validate' + +// CLINK: Common Lightning Interface for Nostr Keys +// https://github.com/shocknet/CLINK/ + +export default { + name: 'CLINK', + displayName: 'CLINK', + send: false, + fields: [ + { + name: 'noffer', + label: 'noffer', + type: 'password', + placeholder: 'noffer...', + required: true, + validate: clinkValidator('noffer') + } + ], + relationName: 'walletRecvClink' +} diff --git a/wallets/lib/protocols/docs/dev/clink.md b/wallets/lib/protocols/docs/dev/clink.md new file mode 100644 index 00000000..b1d75c36 --- /dev/null +++ b/wallets/lib/protocols/docs/dev/clink.md @@ -0,0 +1,17 @@ +Testing CLINK is done with Lightning.Pub and Shockwallet. + +Shockwallet PWA: https://my.shockwallet.app/ + +Steps: + +1. Run this command to get `nprofile` of the lnpub container + +``` +$ sndev logs --since 0 lnpub | grep -oE 'nprofile1\w+' +``` + +2. Go to https://my.shockwallet.app/sources +3. Add a new source and paste `nprofile` +4. Go to https://my.shockwallet.app/offers +5. Reload page to make sure the offer is correctly updated +6. Copy offer and paste into SN diff --git a/wallets/lib/protocols/index.js b/wallets/lib/protocols/index.js index c13a5541..5a944124 100644 --- a/wallets/lib/protocols/index.js +++ b/wallets/lib/protocols/index.js @@ -7,6 +7,7 @@ import lnbitsSuite from './lnbits' import phoenixdSuite from './phoenixd' import blinkSuite from './blink' import webln from './webln' +import clink from './clink' /** * Protocol names as used in the database @@ -45,5 +46,6 @@ export default [ ...phoenixdSuite, ...lnbitsSuite, ...blinkSuite, - webln + webln, + clink ] diff --git a/wallets/lib/validate.js b/wallets/lib/validate.js index 49d17c0a..237b0d9a 100644 --- a/wallets/lib/validate.js +++ b/wallets/lib/validate.js @@ -5,6 +5,7 @@ import { isInvoicableMacaroon, isInvoiceMacaroon } from '@/lib/macaroon' import { NOSTR_PUBKEY_HEX } from '@/lib/nostr' import { TOR_REGEXP } from '@/lib/url' import { lightningAddressValidator } from '@/lib/validate' +import { decodeBech32 as clinkDecodeBech32, OfferPriceType } from '@shocknet/clink-sdk' import { string, array } from 'yup' export const externalLightningAddressValidator = lightningAddressValidator @@ -65,6 +66,35 @@ export function parseNwcUrl (walletConnectUrl) { } } +export const clinkValidator = (type) => + string() + .matches(new RegExp(`^${type}1`), { message: `must start with ${type}1` }) + .matches(/^(noffer|ndebit)1[02-9ac-hj-np-z]+$/, { message: 'invalid bech32 encoding' }) + .test({ + name: 'decode', + test: (v, context) => { + let decoded + try { + decoded = clinkDecodeBech32(v) + } catch (e) { + return context.createError({ message: `failed to decode bech32: ${e.message}` }) + } + + if (decoded.type !== type) { + return context.createError({ message: `must be ${type}` }) + } + + const { data } = decoded + if (!data) return context.createError({ message: 'no data' }) + + if (type === 'noffer' && data.priceType && data.priceType !== OfferPriceType.Spontaneous) { + return context.createError({ message: 'offer must be for spontaneous payments' }) + } + + return true + } + }) + export const socketValidator = (msg = 'invalid socket') => string() .test({ diff --git a/wallets/server/protocols/clink.js b/wallets/server/protocols/clink.js new file mode 100644 index 00000000..5c8ba61c --- /dev/null +++ b/wallets/server/protocols/clink.js @@ -0,0 +1,49 @@ +import { WALLET_CREATE_INVOICE_TIMEOUT_MS } from '@/lib/constants' +import { msatsToSats } from '@/lib/format' +import { decodeBech32, generateSecretKey, SendNofferRequest, SimplePool } from '@shocknet/clink-sdk' + +export const name = 'CLINK' + +// https://clinkme.dev/specs.html +const ERR_INVALID_AMOUNT = 5 + +export async function createInvoice ( + { msats, description, expiry }, + { noffer }, + { signal }) { + const { data: { offer, relay, pubkey } } = decodeBech32(noffer) + + const pool = new SimplePool() + const sk = generateSecretKey() + const request = { offer, amount_sats: msatsToSats(msats), expires_in_seconds: expiry, description } + + let response + try { + const timeout = Math.floor(WALLET_CREATE_INVOICE_TIMEOUT_MS / 1000) + // CLINK does not support a custom invoice description or expiry + response = await SendNofferRequest(pool, sk, [relay], pubkey, request, timeout) + } catch (e) { + throw typeof e === 'string' ? new Error(e) : e + } finally { + pool.close([relay]) + } + + if ('bolt11' in response && typeof response.bolt11 === 'string') { + return response.bolt11 + } + + if (response.code === ERR_INVALID_AMOUNT) { + const { min, max } = response.range + throw new Error(`invalid amount: amount must be between ${min} and ${max} sats`) + } + + throw new Error(response.error) +} + +export async function testCreateInvoice ({ noffer }, { signal }) { + return await createInvoice( + // lnpub minimum range seems to be 10 sats by default so we use 100 sats + { msats: 100e3, description: 'SN test invoice', expiry: 1 }, + { noffer }, + { signal }) +} diff --git a/wallets/server/protocols/index.js b/wallets/server/protocols/index.js index 26c292d9..6bf8ca04 100644 --- a/wallets/server/protocols/index.js +++ b/wallets/server/protocols/index.js @@ -5,6 +5,7 @@ import * as clnRest from './clnRest' import * as phoenixd from './phoenixd' import * as blink from './blink' import * as lndGrpc from './lndGrpc' +import * as clink from './clink' export * from './util' @@ -56,5 +57,6 @@ export default [ clnRest, phoenixd, blink, - lndGrpc + lndGrpc, + clink ] diff --git a/wallets/server/resolvers/util.js b/wallets/server/resolvers/util.js index f0ffa4af..e11ee3e1 100644 --- a/wallets/server/resolvers/util.js +++ b/wallets/server/resolvers/util.js @@ -19,6 +19,8 @@ export function mapWalletResolveTypes (wallet) { return send ? 'WalletSendCLNRest' : 'WalletRecvCLNRest' case 'LND_GRPC': return 'WalletRecvLNDGRPC' + case 'CLINK': + return 'WalletRecvClink' default: return null }