diff --git a/lib/nostr.js b/lib/nostr.js index 349d2efe..a51da057 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -22,10 +22,10 @@ export const RELAYS_BLACKLIST = [] /** * @import {NDKSigner} from '@nostr-dev-kit/ndk' * @import { NDK } from '@nostr-dev-kit/ndk' - * @import {NDKNwc} from '@nostr-dev-kit/ndk' + * @import {NDKNWCWallet} from '@nostr-dev-kit/ndk-wallet' * @typedef {Object} Nostr * @property {NDK} ndk - * @property {function(string, {logger: Object}): Promise} nwc + * @property {function(string, {logger: Object}): Promise} nwc * @property {function(Object, {privKey: string, signer: NDKSigner}): Promise} sign * @property {function(Object, {relays: Array, privKey: string, signer: NDKSigner}): Promise} publish */ diff --git a/package-lock.json b/package-lock.json index e073bed2..22c5ae19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,12 @@ "@apollo/server": "^4.11.0", "@as-integrations/next": "^3.1.0", "@auth/prisma-adapter": "^2.7.0", + "@cashu/cashu-ts": "^2.4.1", "@graphql-tools/schema": "^10.0.6", "@lightninglabs/lnc-web": "^0.3.2-alpha", "@noble/curves": "^1.6.0", - "@nostr-dev-kit/ndk": "^2.10.5", + "@nostr-dev-kit/ndk": "^2.12.2", + "@nostr-dev-kit/ndk-wallet": "^0.5.0", "@opensearch-project/opensearch": "^2.12.0", "@prisma/client": "^5.20.0", "@slack/web-api": "^7.6.0", @@ -2430,6 +2432,91 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cashu/cashu-ts": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-2.4.1.tgz", + "integrity": "sha512-9lDHP5GtWvC/mIDPRg5KdRQAsqoYYm93efPyfgDtRd9eW1BhWrzLuS0sN1WVkL9noAXCZoWjjpX8TElMXhpFhA==", + "license": "MIT", + "dependencies": { + "@cashu/crypto": "^0.3.4", + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "buffer": "^6.0.3" + } + }, + "node_modules/@cashu/cashu-ts/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cashu/crypto": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.4.tgz", + "integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "buffer": "^6.0.3" + } + }, + "node_modules/@cashu/crypto/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cashu/crypto/node_modules/@scure/base": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz", + "integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cashu/crypto/node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cashu/crypto/node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -4338,11 +4425,12 @@ } }, "node_modules/@noble/curves": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", - "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", "dependencies": { - "@noble/hashes": "1.5.0" + "@noble/hashes": "1.7.1" }, "engines": { "node": "^14.21.3 || >=16" @@ -4352,9 +4440,10 @@ } }, "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", "engines": { "node": "^14.21.3 || >=16" }, @@ -4418,9 +4507,9 @@ } }, "node_modules/@nostr-dev-kit/ndk": { - "version": "2.10.5", - "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.5.tgz", - "integrity": "sha512-QEnarJL9BGCxeenSIE9jxNSDyYQYjzD30YL3sVJ9cNybNZX8tl/I1/vhEUeRRMBz/qjROLtt0M2RV68rZ205tg==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.12.2.tgz", + "integrity": "sha512-uvautgwbpk3AgddoFpew67/FiaV/zpKwwvSnjCvbE/tAdJBpUUS+VjWR5WfUnJvxTy/ZZpPW+X2TkwVFHhUdvA==", "license": "MIT", "dependencies": { "@noble/curves": "^1.6.0", @@ -4439,6 +4528,69 @@ "node": ">=16" } }, + "node_modules/@nostr-dev-kit/ndk-wallet": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-wallet/-/ndk-wallet-0.5.0.tgz", + "integrity": "sha512-iHMh5kRKbEEw42fmE89KIH+4cnsRQzpW4SrbtpLCbdGFeu8iUOE2C1bBAWWW3HdtcjuRxa83iYXI+eo/Wake2w==", + "license": "MIT", + "dependencies": { + "@nostr-dev-kit/ndk": "2.13.0-rc2", + "debug": "^4.3.4", + "light-bolt11-decoder": "^3.0.0", + "tseep": "^1.1.1", + "typescript": "^5.4.4", + "webln": "^0.3.2" + }, + "peerDependencies": { + "@cashu/cashu-ts": "*", + "@cashu/crypto": "*" + } + }, + "node_modules/@nostr-dev-kit/ndk-wallet/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nostr-dev-kit/ndk-wallet/node_modules/@nostr-dev-kit/ndk": { + "version": "2.13.0-rc2", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.13.0-rc2.tgz", + "integrity": "sha512-oTj19rqcft6VCxvlkosIuCl8aQNrQzU0NJ/yi0XRbtlVReAyJhZ7iT6rZixO9aL4pAG9ZCBp+ej3GRcU3OIEeQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@noble/secp256k1": "^2.1.0", + "@scure/base": "^1.1.9", + "debug": "^4.3.6", + "light-bolt11-decoder": "^3.2.0", + "tseep": "^1.2.2", + "typescript-lru-cache": "^2.0.0", + "utf8-buffer": "^1.0.0", + "websocket-polyfill": "^0.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "nostr-tools": "^2.7.1" + } + }, + "node_modules/@nostr-dev-kit/ndk-wallet/node_modules/@scure/base": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz", + "integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nostr-dev-kit/ndk/node_modules/@noble/hashes": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", @@ -7341,6 +7493,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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.2.1" + } + }, "node_modules/buffer-compare": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-compare/-/buffer-compare-1.1.1.tgz", @@ -11176,6 +11352,26 @@ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -19780,6 +19976,19 @@ "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", "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==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/typescript-lru-cache": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/typescript-lru-cache/-/typescript-lru-cache-2.0.0.tgz", diff --git a/package.json b/package.json index 4a81f087..d27c39a5 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,12 @@ "@apollo/server": "^4.11.0", "@as-integrations/next": "^3.1.0", "@auth/prisma-adapter": "^2.7.0", + "@cashu/cashu-ts": "^2.4.1", "@graphql-tools/schema": "^10.0.6", "@lightninglabs/lnc-web": "^0.3.2-alpha", "@noble/curves": "^1.6.0", - "@nostr-dev-kit/ndk": "^2.10.5", + "@nostr-dev-kit/ndk": "^2.12.2", + "@nostr-dev-kit/ndk-wallet": "^0.5.0", "@opensearch-project/opensearch": "^2.12.0", "@prisma/client": "^5.20.0", "@slack/web-api": "^7.6.0", diff --git a/scripts/test-nip57.sh b/scripts/test-nip57.sh new file mode 100644 index 00000000..ae20dfc2 --- /dev/null +++ b/scripts/test-nip57.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +# https://github.com/nostr-protocol/nips/blob/master/57.md +set -e + +# test user with attached wallet +# TODO: attach wallet to test01 via psql if not already attached? +USERNAME=test01 + +# XXX this should match NOSTR_PRIVATE_KEY in .env.development +NOSTR_PRIVATE_KEY=5f30b7e7714360f51f2be2e30c1d93b7fdf67366e730658e85777dfcc4e4245f +NOSTR_PUBLIC_KEY=$(nak key public $NOSTR_PRIVATE_KEY) + +SINCE=$(date +%s) + +function create_event() { + nak event -k 9734 \ + --tag p=$NOSTR_PUBLIC_KEY \ + --tag 'relays=wss://relay.primal.net' \ + --tag amount=100000 +} + +function url_encode() { + cat - | jq -sRr @uri +} + +function test_exit() { + if [ $1 -eq 0 ]; then + echo "worker publishes nip-57 zap receipts: PASSED" + else + echo "worker publishes nip-57 zap receipts: FAILED" + fi + exit $1 +} + +create_event | nak verify + +# create a zap request event (kind 9734) +EVENT="$(create_event)" + +echo "generated zap request event:" +echo "$EVENT" | jq + +echo $EVENT | nak verify + +# XXX make sure amount is higher than dust limit of receiver's wallet +echo -n "sending zap request event LNURL endpoint ... " +PR=$(curl -s "http://localhost:3000/api/lnurlp/$USERNAME/pay?amount=100000&nostr=$(echo $EVENT | url_encode)" | jq -r .pr) +echo "OK" + +[ "$PR" == "null" ] && echo "error: LNURL endpoint did not return bolt11" && test_exit 1 +echo $PR + +sndev fund --cln $PR + +# subscribe to zap receipt event (kind 9735) +echo -n "waiting for zap receipt event ... " +sleep 3 +PR2=$(nak -q req -k 9735 -p $NOSTR_PUBLIC_KEY --limit 1 wss://relay.primal.net | jq -r '.tags[] | select(.[0] == "bolt11") | .[1]') +echo "OK" + +[ "$PR" == "$PR2" ] && test_exit 0 || test_exit 1 diff --git a/wallets/nwc/client.js b/wallets/nwc/client.js index f6fc3829..19391cc4 100644 --- a/wallets/nwc/client.js +++ b/wallets/nwc/client.js @@ -9,6 +9,6 @@ export async function testSendPayment ({ nwcUrl }, { signal }) { } export async function sendPayment (bolt11, { nwcUrl }, { signal }) { - const result = await nwcTryRun(nwc => nwc.payInvoice(bolt11), { nwcUrl }, { signal }) + const result = await nwcTryRun(nwc => nwc.lnPay({ pr: bolt11 }), { nwcUrl }, { signal }) return result.preimage } diff --git a/wallets/nwc/index.js b/wallets/nwc/index.js index 1597d419..4c6e3f63 100644 --- a/wallets/nwc/index.js +++ b/wallets/nwc/index.js @@ -1,10 +1,7 @@ import Nostr from '@/lib/nostr' import { string } from '@/lib/yup' import { parseNwcUrl } from '@/lib/url' -import { NDKNwc } from '@nostr-dev-kit/ndk' -import { TimeoutError } from '@/lib/time' - -const NWC_CONNECT_TIMEOUT_MS = 15_000 +import { NDKNWCWallet } from '@nostr-dev-kit/ndk-wallet' export const name = 'nwc' export const walletType = 'NWC' @@ -39,23 +36,11 @@ export const card = { async function getNwc (nostr, nwcUrl, { signal }) { const ndk = nostr.ndk const { walletPubkey, secret, relayUrls } = parseNwcUrl(nwcUrl) - const nwc = new NDKNwc({ - ndk, + const nwc = new NDKNWCWallet(ndk, { pubkey: walletPubkey, relayUrls, secret }) - - // TODO: support AbortSignal - try { - await nwc.blockUntilReady(NWC_CONNECT_TIMEOUT_MS) - } catch (err) { - if (err.message === 'Timeout') { - throw new TimeoutError(NWC_CONNECT_TIMEOUT_MS) - } - throw err - } - return nwc } @@ -69,9 +54,7 @@ export async function nwcTryRun (fun, { nwcUrl }, { signal }) { const nostr = new Nostr() try { const nwc = await getNwc(nostr, nwcUrl, { signal }) - const { error, result } = await fun(nwc) - if (error) throw new Error(error.message || error.code) - return result + return await fun(nwc) } catch (e) { if (e.error) throw new Error(e.error.message || e.error.code) throw e