From 6d3f7d42305b80d9c4dbb3d822282c18e2155d8b Mon Sep 17 00:00:00 2001 From: ekzyis Date: Thu, 20 Jun 2024 21:52:07 +0200 Subject: [PATCH] wip: nwc --- components/wallet/index.js | 4 +- components/wallet/lnbits.js | 1 + components/wallet/nwc.js | 103 +++++++++++++++++++++++++++++ lib/validate.js | 26 -------- pages/settings/wallets/[wallet].js | 2 +- 5 files changed, 108 insertions(+), 28 deletions(-) create mode 100644 components/wallet/nwc.js diff --git a/components/wallet/index.js b/components/wallet/index.js index ad748c59..99e6ca24 100644 --- a/components/wallet/index.js +++ b/components/wallet/index.js @@ -7,7 +7,8 @@ import { bolt11Tags } from '@/lib/bolt11' // wallet definitions export const WALLET_DEFS = [ - await import('@/components/wallet/lnbits') + await import('@/components/wallet/lnbits'), + await import('@/components/wallet/nwc') ] export const Status = { @@ -41,6 +42,7 @@ export function useWallet (name) { const validate = useCallback(async (values) => { try { // validate should log custom INFO and OK message + // TODO: add timeout return await wallet.validate({ logger, ...values }) } catch (err) { const message = err.message || err.toString?.() diff --git a/components/wallet/lnbits.js b/components/wallet/lnbits.js index c6349d57..b5bf0714 100644 --- a/components/wallet/lnbits.js +++ b/components/wallet/lnbits.js @@ -18,6 +18,7 @@ export const fields = [ export const card = { title: 'LNbits', + subtitle: 'use LNbits for payments', badges: ['send only', 'non-custodialish'] } diff --git a/components/wallet/nwc.js b/components/wallet/nwc.js new file mode 100644 index 00000000..b32bb1ff --- /dev/null +++ b/components/wallet/nwc.js @@ -0,0 +1,103 @@ +import { NOSTR_PUBKEY_HEX } from '@/lib/nostr' +import { parseNwcUrl } from '@/lib/url' +import { Relay } from 'nostr-tools' +import { object, string } from 'yup' + +export const name = 'nwc' + +export const fields = [ + { + name: 'nwcUrl', + label: 'connection', + type: 'password' + } +] + +export const card = { + title: 'NWC', + subtitle: 'use Nostr Wallet Connect for payments', + badges: ['send only', 'non-custodialish'] +} + +export const schema = object({ + nwcUrl: string() + .required('required') + .test(async (nwcUrl, context) => { + // run validation in sequence to control order of errors + // inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180 + try { + await string().required('required').validate(nwcUrl) + await string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validate(nwcUrl) + let relayUrl, walletPubkey, secret + try { + ({ relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl)) + } catch { + // invalid URL error. handle as if pubkey validation failed to not confuse user. + throw new Error('pubkey must be 64 hex chars') + } + await string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validate(walletPubkey) + await string().required('relay url required').trim().wss('relay must use wss://').validate(relayUrl) + await string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validate(secret) + } catch (err) { + return context.createError({ message: err.message }) + } + return true + }) +}) + +export async function validate ({ logger, ...config }) { + return await getInfo({ logger, ...config }) +} + +async function getInfo ({ logger, nwcUrl }) { + const { relayUrl, walletPubkey } = parseNwcUrl(nwcUrl) + + logger.info(`requesting info event from ${relayUrl}`) + const relay = await Relay + .connect(relayUrl) + .catch(() => { + // NOTE: passed error is undefined for some reason + const msg = `failed to connect to ${relayUrl}` + logger.error(msg) + throw new Error(msg) + }) + logger.ok(`connected to ${relayUrl}`) + + try { + return await new Promise((resolve, reject) => { + let found = false + const sub = relay.subscribe([ + { + kinds: [13194], + authors: [walletPubkey] + } + ], { + onevent (event) { + found = true + logger.ok(`received info event from ${relayUrl}`) + resolve(event) + }, + onclose (reason) { + if (!['closed by caller', 'relay connection closed by us'].includes(reason)) { + // only log if not closed by us (caller) + const msg = 'connection closed: ' + (reason || 'unknown reason') + logger.error(msg) + reject(new Error(msg)) + } + }, + oneose () { + if (!found) { + const msg = 'EOSE received without info event' + logger.error(msg) + reject(new Error(msg)) + } + sub?.close() + } + }) + }) + } finally { + // For some reason, websocket is already in CLOSING or CLOSED state. + // relay?.close() + if (relay) logger.info(`closed connection to ${relayUrl}`) + } +} diff --git a/lib/validate.js b/lib/validate.js index c84fb48b..2b3e6666 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -600,32 +600,6 @@ export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) => return accum }, {}))) -export const nwcSchema = object({ - nwcUrl: string() - .required('required') - .test(async (nwcUrl, context) => { - // run validation in sequence to control order of errors - // inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180 - try { - await string().required('required').validate(nwcUrl) - await string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validate(nwcUrl) - let relayUrl, walletPubkey, secret - try { - ({ relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl)) - } catch { - // invalid URL error. handle as if pubkey validation failed to not confuse user. - throw new Error('pubkey must be 64 hex chars') - } - await string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validate(walletPubkey) - await string().required('relay url required').trim().wss('relay must use wss://').validate(relayUrl) - await string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validate(secret) - } catch (err) { - return context.createError({ message: err.message }) - } - return true - }) -}) - export const lncSchema = object({ pairingPhrase: array() .transform(function (value, originalValue) { diff --git a/pages/settings/wallets/[wallet].js b/pages/settings/wallets/[wallet].js index c8b95069..f44a5061 100644 --- a/pages/settings/wallets/[wallet].js +++ b/pages/settings/wallets/[wallet].js @@ -28,7 +28,7 @@ export default function WalletSettings () { return (

{wallet.card.title}

-
use {wallet.card.title} for payments
+
{wallet.card.subtitle}