Remove useRef for NWC relay (#1014)
* Remove useRef for NWC relay * connect to relay for every payment for more reliable payments * remove getInfoWithRelay method (no longer needed since we no longer use useRef) * fix 'WebSocket is already in CLOSING or CLOSED state.' errors * improve logging * Log connection failures * Fix no error thrown on validation error
This commit is contained in:
parent
9f06692e3e
commit
ebcdc21728
@ -1,6 +1,6 @@
|
|||||||
// https://github.com/getAlby/js-sdk/blob/master/src/webln/NostrWeblnProvider.ts
|
// https://github.com/getAlby/js-sdk/blob/master/src/webln/NostrWeblnProvider.ts
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||||
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
|
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
|
||||||
import { parseNwcUrl } from '@/lib/url'
|
import { parseNwcUrl } from '@/lib/url'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
@ -17,25 +17,83 @@ export function NWCProvider ({ children }) {
|
|||||||
const [initialized, setInitialized] = useState(false)
|
const [initialized, setInitialized] = useState(false)
|
||||||
const logger = useWalletLogger('nwc')
|
const logger = useWalletLogger('nwc')
|
||||||
|
|
||||||
const relayRef = useRef()
|
|
||||||
|
|
||||||
const name = 'NWC'
|
const name = 'NWC'
|
||||||
const storageKey = 'webln:provider:nwc'
|
const storageKey = 'webln:provider:nwc'
|
||||||
|
|
||||||
const updateRelay = async (relayUrl) => {
|
const getInfo = useCallback(async (relayUrl, walletPubkey) => {
|
||||||
|
logger.info(`requesting info event from ${relayUrl}`)
|
||||||
|
|
||||||
|
let relay, sub
|
||||||
try {
|
try {
|
||||||
if (relayRef.current) {
|
relay = await Relay.connect(relayUrl).catch(() => {
|
||||||
relayRef.current.close()
|
// NOTE: passed error is undefined for some reason
|
||||||
logger.info('disconnected from', relayRef.current.url)
|
const msg = `failed to connect to ${relayUrl}`
|
||||||
}
|
logger.error(msg)
|
||||||
if (relayUrl) {
|
throw new Error(msg)
|
||||||
relayRef.current = await Relay.connect(relayUrl)
|
})
|
||||||
logger.ok(`connected to ${relayUrl}`)
|
logger.ok(`connected to ${relayUrl}`)
|
||||||
}
|
return await new Promise((resolve, reject) => {
|
||||||
} catch (err) {
|
const timeout = 5000
|
||||||
console.error(err)
|
const timer = setTimeout(() => {
|
||||||
|
const msg = 'timeout waiting for info event'
|
||||||
|
logger.error(msg)
|
||||||
|
reject(new Error(msg))
|
||||||
|
sub?.close()
|
||||||
|
}, timeout)
|
||||||
|
|
||||||
|
let found = false
|
||||||
|
sub = relay.subscribe([
|
||||||
|
{
|
||||||
|
kinds: [13194],
|
||||||
|
authors: [walletPubkey]
|
||||||
|
}
|
||||||
|
], {
|
||||||
|
onevent (event) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
found = true
|
||||||
|
logger.ok(`received info event from ${relayUrl}`)
|
||||||
|
resolve(event)
|
||||||
|
},
|
||||||
|
onclose (reason) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
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 () {
|
||||||
|
clearTimeout(timer)
|
||||||
|
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}`)
|
||||||
}
|
}
|
||||||
}
|
}, [logger])
|
||||||
|
|
||||||
|
const validateParams = useCallback(async ({ relayUrl, walletPubkey }) => {
|
||||||
|
// validate connection by fetching info event
|
||||||
|
// function needs to throw an error for formik validation to fail
|
||||||
|
const event = await getInfo(relayUrl, walletPubkey)
|
||||||
|
const supported = event.content.split(/[\s,]+/) // handle both spaces and commas
|
||||||
|
logger.info('supported methods:', supported)
|
||||||
|
if (!supported.includes('pay_invoice')) {
|
||||||
|
const msg = 'wallet does not support pay_invoice'
|
||||||
|
logger.error(msg)
|
||||||
|
throw new Error(msg)
|
||||||
|
}
|
||||||
|
logger.ok('wallet supports pay_invoice')
|
||||||
|
}, [logger])
|
||||||
|
|
||||||
const loadConfig = useCallback(async () => {
|
const loadConfig = useCallback(async () => {
|
||||||
const configStr = window.localStorage.getItem(storageKey)
|
const configStr = window.localStorage.getItem(storageKey)
|
||||||
@ -63,14 +121,10 @@ export function NWCProvider ({ children }) {
|
|||||||
`relay=${params.relayUrl}`)
|
`relay=${params.relayUrl}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(`requesting info event from ${params.relayUrl}`)
|
await validateParams(params)
|
||||||
await validateParams({ ...params, logger })
|
|
||||||
logger.ok('info event received')
|
|
||||||
await updateRelay(params.relayUrl)
|
|
||||||
setEnabled(true)
|
setEnabled(true)
|
||||||
logger.ok('wallet enabled')
|
logger.ok('wallet enabled')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('invalid config:', err)
|
|
||||||
setEnabled(false)
|
setEnabled(false)
|
||||||
logger.info('wallet disabled')
|
logger.info('wallet disabled')
|
||||||
throw err
|
throw err
|
||||||
@ -104,19 +158,15 @@ export function NWCProvider ({ children }) {
|
|||||||
`relay=${params.relayUrl}`)
|
`relay=${params.relayUrl}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(`requesting info event from ${params.relayUrl}`)
|
await validateParams(params)
|
||||||
await validateParams({ ...params, logger })
|
|
||||||
logger.ok('info event received')
|
|
||||||
await updateRelay(params.relayUrl)
|
|
||||||
setEnabled(true)
|
setEnabled(true)
|
||||||
logger.ok('wallet enabled')
|
logger.ok('wallet enabled')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('invalid config:', err)
|
|
||||||
setEnabled(false)
|
setEnabled(false)
|
||||||
logger.info('wallet disabled')
|
logger.info('wallet disabled')
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}, [logger])
|
}, [validateParams, logger])
|
||||||
|
|
||||||
const clearConfig = useCallback(() => {
|
const clearConfig = useCallback(() => {
|
||||||
window.localStorage.removeItem(storageKey)
|
window.localStorage.removeItem(storageKey)
|
||||||
@ -131,41 +181,34 @@ export function NWCProvider ({ children }) {
|
|||||||
const inv = lnpr.decode(bolt11)
|
const inv = lnpr.decode(bolt11)
|
||||||
const hash = inv.tagsObject.payment_hash
|
const hash = inv.tagsObject.payment_hash
|
||||||
logger.info('sending payment:', `payment_hash=${hash}`)
|
logger.info('sending payment:', `payment_hash=${hash}`)
|
||||||
try {
|
|
||||||
const ret = await new Promise(function (resolve, reject) {
|
|
||||||
const relay = relayRef.current
|
|
||||||
if (!relay) {
|
|
||||||
return reject(new Error('not connected to relay'))
|
|
||||||
}
|
|
||||||
(async function () {
|
|
||||||
// XXX set this to mock NWC relays
|
|
||||||
const MOCK_NWC_RELAY = false
|
|
||||||
|
|
||||||
|
let relay, sub
|
||||||
|
try {
|
||||||
|
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}`)
|
||||||
|
const ret = await new Promise(function (resolve, reject) {
|
||||||
|
(async function () {
|
||||||
// timeout since NWC is async (user needs to confirm payment in wallet)
|
// timeout since NWC is async (user needs to confirm payment in wallet)
|
||||||
// timeout is same as invoice expiry
|
// timeout is same as invoice expiry
|
||||||
const timeout = MOCK_NWC_RELAY ? 3000 : 180_000
|
const timeout = 180_000
|
||||||
let timer
|
const timer = setTimeout(() => {
|
||||||
const resetTimer = () => {
|
const msg = 'timeout waiting for info event'
|
||||||
clearTimeout(timer)
|
logger.error(msg)
|
||||||
timer = setTimeout(() => {
|
reject(new Error(msg))
|
||||||
sub?.close()
|
sub?.close()
|
||||||
if (MOCK_NWC_RELAY) {
|
}, timeout)
|
||||||
const heads = Math.random() < 0.5
|
|
||||||
if (heads) {
|
|
||||||
return resolve({ preimage: null })
|
|
||||||
}
|
|
||||||
return reject(new Error('mock error'))
|
|
||||||
}
|
|
||||||
return reject(new Error('timeout'))
|
|
||||||
}, timeout)
|
|
||||||
}
|
|
||||||
if (MOCK_NWC_RELAY) return resetTimer()
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
method: 'pay_invoice',
|
method: 'pay_invoice',
|
||||||
params: { invoice: bolt11 }
|
params: { invoice: bolt11 }
|
||||||
}
|
}
|
||||||
const content = await nip04.encrypt(secret, walletPubkey, JSON.stringify(payload))
|
const content = await nip04.encrypt(secret, walletPubkey, JSON.stringify(payload))
|
||||||
|
|
||||||
const request = finalizeEvent({
|
const request = finalizeEvent({
|
||||||
kind: 23194,
|
kind: 23194,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
@ -173,30 +216,27 @@ export function NWCProvider ({ children }) {
|
|||||||
content
|
content
|
||||||
}, secret)
|
}, secret)
|
||||||
await relay.publish(request)
|
await relay.publish(request)
|
||||||
resetTimer()
|
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
kinds: [23195],
|
kinds: [23195],
|
||||||
authors: [walletPubkey],
|
authors: [walletPubkey],
|
||||||
'#e': [request.id]
|
'#e': [request.id]
|
||||||
}
|
}
|
||||||
const sub = relay.subscribe([filter], {
|
sub = relay.subscribe([filter], {
|
||||||
async onevent (response) {
|
async onevent (response) {
|
||||||
resetTimer()
|
clearTimeout(timer)
|
||||||
try {
|
try {
|
||||||
const content = JSON.parse(await nip04.decrypt(secret, walletPubkey, response.content))
|
const content = JSON.parse(await nip04.decrypt(secret, walletPubkey, response.content))
|
||||||
if (content.error) return reject(new Error(content.error.message))
|
if (content.error) return reject(new Error(content.error.message))
|
||||||
if (content.result) return resolve({ preimage: content.result.preimage })
|
if (content.result) return resolve({ preimage: content.result.preimage })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return reject(err)
|
return reject(err)
|
||||||
} finally {
|
|
||||||
clearTimeout(timer)
|
|
||||||
sub.close()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onclose (reason) {
|
onclose (reason) {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
reject(new Error(reason))
|
reject(new Error(reason))
|
||||||
|
sub?.close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})().catch(reject)
|
})().catch(reject)
|
||||||
@ -207,11 +247,13 @@ export function NWCProvider ({ children }) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.())
|
logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.())
|
||||||
throw err
|
throw err
|
||||||
|
} finally {
|
||||||
|
// For some reason, websocket is already in CLOSING or CLOSED state.
|
||||||
|
// relay?.close()
|
||||||
|
if (relay) logger.info(`closed connection to ${relayUrl}`)
|
||||||
}
|
}
|
||||||
}, [walletPubkey, secret, logger])
|
}, [walletPubkey, secret, logger])
|
||||||
|
|
||||||
const getInfo = useCallback(() => getInfoWithRelay(relayRef?.current, walletPubkey), [relayRef?.current, walletPubkey])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadConfig().catch(err => logger.error(err.message || err.toString?.()))
|
loadConfig().catch(err => logger.error(err.message || err.toString?.()))
|
||||||
}, [])
|
}, [])
|
||||||
@ -227,48 +269,3 @@ export function NWCProvider ({ children }) {
|
|||||||
export function useNWC () {
|
export function useNWC () {
|
||||||
return useContext(NWCContext)
|
return useContext(NWCContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateParams ({ relayUrl, walletPubkey, secret, logger }) {
|
|
||||||
let infoRelay
|
|
||||||
try {
|
|
||||||
// validate connection by fetching info event
|
|
||||||
infoRelay = await Relay.connect(relayUrl)
|
|
||||||
logger.ok(`connected to ${relayUrl}`)
|
|
||||||
await getInfoWithRelay(infoRelay, walletPubkey, logger)
|
|
||||||
} finally {
|
|
||||||
infoRelay?.close()
|
|
||||||
logger.info(`closed connection to ${relayUrl}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getInfoWithRelay (relay, walletPubkey) {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const timeout = 5000
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
reject(new Error('timeout waiting for response'))
|
|
||||||
sub?.close()
|
|
||||||
}, timeout)
|
|
||||||
|
|
||||||
const sub = relay.subscribe([
|
|
||||||
{
|
|
||||||
kinds: [13194],
|
|
||||||
authors: [walletPubkey]
|
|
||||||
}
|
|
||||||
], {
|
|
||||||
onevent (event) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
const supported = event.content.split(/[\s,]+/) // handle both spaces and commas
|
|
||||||
supported.includes('pay_invoice') ? resolve() : reject(new Error('wallet does not support pay_invoice'))
|
|
||||||
sub.close()
|
|
||||||
},
|
|
||||||
onclose (reason) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
reject(new Error(reason || 'connection closed: reason unknown'))
|
|
||||||
},
|
|
||||||
oneose () {
|
|
||||||
clearTimeout(timer)
|
|
||||||
reject(new Error('info event not found'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user