Add NWC wallet
This commit is contained in:
parent
b8b0a4f985
commit
034cb4e8b2
|
@ -7,7 +7,8 @@ import { bolt11Tags } from '@/lib/bolt11'
|
||||||
|
|
||||||
// wallet definitions
|
// wallet definitions
|
||||||
export const WALLET_DEFS = [
|
export const WALLET_DEFS = [
|
||||||
await import('@/components/wallet/lnbits')
|
await import('@/components/wallet/lnbits'),
|
||||||
|
await import('@/components/wallet/nwc')
|
||||||
]
|
]
|
||||||
|
|
||||||
export const Status = {
|
export const Status = {
|
||||||
|
@ -29,7 +30,7 @@ export function useWallet (name) {
|
||||||
const hash = bolt11Tags(bolt11).payment_hash
|
const hash = bolt11Tags(bolt11).payment_hash
|
||||||
logger.info('sending payment:', `payment_hash=${hash}`)
|
logger.info('sending payment:', `payment_hash=${hash}`)
|
||||||
try {
|
try {
|
||||||
const { preimage } = await wallet.sendPayment({ bolt11, config })
|
const { preimage } = await wallet.sendPayment({ bolt11, config, logger })
|
||||||
logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`)
|
logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.message || err.toString?.()
|
const message = err.message || err.toString?.()
|
||||||
|
@ -41,6 +42,7 @@ export function useWallet (name) {
|
||||||
const validate = useCallback(async (values) => {
|
const validate = useCallback(async (values) => {
|
||||||
try {
|
try {
|
||||||
// validate should log custom INFO and OK message
|
// validate should log custom INFO and OK message
|
||||||
|
// TODO: add timeout
|
||||||
return await wallet.validate({ logger, ...values })
|
return await wallet.validate({ logger, ...values })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.message || err.toString?.()
|
const message = err.message || err.toString?.()
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const fields = [
|
||||||
|
|
||||||
export const card = {
|
export const card = {
|
||||||
title: 'LNbits',
|
title: 'LNbits',
|
||||||
|
subtitle: 'use LNbits for payments',
|
||||||
badges: ['send only', 'non-custodialish']
|
badges: ['send only', 'non-custodialish']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { NOSTR_PUBKEY_HEX } from '@/lib/nostr'
|
||||||
|
import { parseNwcUrl } from '@/lib/url'
|
||||||
|
import { Relay, finalizeEvent, nip04 } 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, 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendPayment ({ bolt11, config, logger }) {
|
||||||
|
const { relayUrl, walletPubkey, secret } = parseNwcUrl(config.nwcUrl)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const ret = await new Promise(function (resolve, reject) {
|
||||||
|
(async function () {
|
||||||
|
const payload = {
|
||||||
|
method: 'pay_invoice',
|
||||||
|
params: { invoice: bolt11 }
|
||||||
|
}
|
||||||
|
const content = await nip04.encrypt(secret, walletPubkey, JSON.stringify(payload))
|
||||||
|
|
||||||
|
const request = finalizeEvent({
|
||||||
|
kind: 23194,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', walletPubkey]],
|
||||||
|
content
|
||||||
|
}, secret)
|
||||||
|
await relay.publish(request)
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
kinds: [23195],
|
||||||
|
authors: [walletPubkey],
|
||||||
|
'#e': [request.id]
|
||||||
|
}
|
||||||
|
relay.subscribe([filter], {
|
||||||
|
async onevent (response) {
|
||||||
|
try {
|
||||||
|
const content = JSON.parse(await nip04.decrypt(secret, walletPubkey, response.content))
|
||||||
|
if (content.error) return reject(new Error(content.error.message))
|
||||||
|
if (content.result) return resolve({ preimage: content.result.preimage })
|
||||||
|
} catch (err) {
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})().catch(reject)
|
||||||
|
})
|
||||||
|
return ret
|
||||||
|
} finally {
|
||||||
|
// For some reason, websocket is already in CLOSING or CLOSED state.
|
||||||
|
// relay?.close()
|
||||||
|
if (relay) logger.info(`closed connection to ${relayUrl}`)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX } from './
|
||||||
import * as usersFragments from '@/fragments/users'
|
import * as usersFragments from '@/fragments/users'
|
||||||
import * as subsFragments from '@/fragments/subs'
|
import * as subsFragments from '@/fragments/subs'
|
||||||
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
||||||
import { parseNwcUrl } from './url'
|
|
||||||
import { datePivot } from './time'
|
import { datePivot } from './time'
|
||||||
import { decodeRune } from '@/lib/cln'
|
import { decodeRune } from '@/lib/cln'
|
||||||
import bip39Words from './bip39-words'
|
import bip39Words from './bip39-words'
|
||||||
|
@ -600,32 +599,6 @@ export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) =>
|
||||||
return accum
|
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({
|
export const lncSchema = object({
|
||||||
pairingPhrase: array()
|
pairingPhrase: array()
|
||||||
.transform(function (value, originalValue) {
|
.transform(function (value, originalValue) {
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default function WalletSettings () {
|
||||||
return (
|
return (
|
||||||
<CenterLayout>
|
<CenterLayout>
|
||||||
<h2 className='pb-2'>{wallet.card.title}</h2>
|
<h2 className='pb-2'>{wallet.card.title}</h2>
|
||||||
<h6 className='text-muted text-center pb-3'>use {wallet.card.title} for payments</h6>
|
<h6 className='text-muted text-center pb-3'>{wallet.card.subtitle}</h6>
|
||||||
<WalletSecurityBanner />
|
<WalletSecurityBanner />
|
||||||
<Form
|
<Form
|
||||||
initial={initial}
|
initial={initial}
|
||||||
|
|
Loading…
Reference in New Issue