From 0f553b1aefe64af7db5e5863c277ac3662940fb7 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Mon, 3 Jun 2024 17:41:15 -0500 Subject: [PATCH] refactor webln --- components/webln/index.js | 1 + components/webln/index2.js | 22 +++++++ components/webln/lnbits.js | 30 +++++----- components/webln/lnbits2.js | 112 ++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 components/webln/index2.js create mode 100644 components/webln/lnbits2.js diff --git a/components/webln/index.js b/components/webln/index.js index 4eed911c..27156b07 100644 --- a/components/webln/index.js +++ b/components/webln/index.js @@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } import { LNbitsProvider, useLNbits } from './lnbits' import { NWCProvider, useNWC } from './nwc' import { LNCProvider, useLNC } from './lnc' +import lnbits from './lnbits2' const WebLNContext = createContext({}) diff --git a/components/webln/index2.js b/components/webln/index2.js new file mode 100644 index 00000000..5d8a0b86 --- /dev/null +++ b/components/webln/index2.js @@ -0,0 +1,22 @@ +import { createContext, useContext, useMemo, useState } from 'react' +import lnbits from './lnbits2' + +const storageKey = 'webln:providers' + +const WebLNContext = createContext({}) + +export function useWebLN () { + const { provider } = useContext(WebLNContext) + return provider +} + +export function RawWebLNProvider ({ children }) { + const [provider, setProvider] = useState() + + const value = useMemo(() => ({ provider, setProvider }), []) + return ( + + {children} + + ) +} diff --git a/components/webln/lnbits.js b/components/webln/lnbits.js index 68eddcd9..b00a7763 100644 --- a/components/webln/lnbits.js +++ b/components/webln/lnbits.js @@ -67,8 +67,7 @@ const getPayment = async (baseUrl, adminKey, paymentHash) => { export function LNbitsProvider ({ children }) { const me = useMe() - const [url, setUrl] = useState('') - const [adminKey, setAdminKey] = useState('') + const [config, setConfig] = useState({}) const [status, setStatus] = useState() const { logger } = useWalletLogger(Wallet.LNbits) @@ -78,7 +77,7 @@ export function LNbitsProvider ({ children }) { } const getInfo = useCallback(async () => { - const response = await getWallet(url, adminKey) + const response = await getWallet(config.url, config.adminKey) return { node: { alias: response.name, @@ -92,9 +91,11 @@ export function LNbitsProvider ({ children }) { version: '1.0', supports: ['lightning'] } - }, [url, adminKey]) + }, [config]) const sendPayment = useCallback(async (bolt11) => { + const { url, adminKey } = config + const hash = bolt11Tags(bolt11).payment_hash logger.info('sending payment:', `payment_hash=${hash}`) @@ -111,7 +112,7 @@ export function LNbitsProvider ({ children }) { logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.()) throw err } - }, [logger, url, adminKey]) + }, [logger, config]) const loadConfig = useCallback(async () => { let configStr = window.localStorage.getItem(storageKey) @@ -129,20 +130,17 @@ export function LNbitsProvider ({ children }) { } const config = JSON.parse(configStr) - - const { url, adminKey } = config - setUrl(url) - setAdminKey(adminKey) + setConfig(config) logger.info( 'loaded wallet config: ' + 'adminKey=****** ' + - `url=${url}`) + `url=${config.url}`) try { // validate config by trying to fetch wallet logger.info('trying to fetch wallet') - await getWallet(url, adminKey) + await getWallet(config.url, config.adminKey) logger.ok('wallet found') setStatus(Status.Enabled) logger.ok('wallet enabled') @@ -156,8 +154,7 @@ export function LNbitsProvider ({ children }) { const saveConfig = useCallback(async (config) => { // immediately store config so it's not lost even if config is invalid - setUrl(config.url) - setAdminKey(config.adminKey) + setConfig(config) // XXX This is insecure, XSS vulns could lead to loss of funds! // -> check how mutiny encrypts their wallet and/or check if we can leverage web workers @@ -186,8 +183,7 @@ export function LNbitsProvider ({ children }) { const clearConfig = useCallback(() => { window.localStorage.removeItem(storageKey) - setUrl('') - setAdminKey('') + setConfig({}) setStatus(undefined) }, []) @@ -196,8 +192,8 @@ export function LNbitsProvider ({ children }) { }, []) const value = useMemo( - () => ({ name: 'LNbits', url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment }), - [url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment]) + () => ({ name: 'LNbits', ...config, status, saveConfig, clearConfig, getInfo, sendPayment }), + [config, status, saveConfig, clearConfig, getInfo, sendPayment]) return ( {children} diff --git a/components/webln/lnbits2.js b/components/webln/lnbits2.js new file mode 100644 index 00000000..c7b5e6c1 --- /dev/null +++ b/components/webln/lnbits2.js @@ -0,0 +1,112 @@ +import { bolt11Tags } from '@/lib/bolt11' + +export const name = 'LNbits' + +let config, logger + +export function setConfig (_config) { + config = _config +} + +export function setLogger (_logger) { + logger = _logger +} + +export async function getInfo () { + const response = await getWallet(config.url, config.adminKey) + return { + node: { + alias: response.name, + pubkey: '' + }, + methods: [ + 'getInfo', + 'getBalance', + 'sendPayment' + ], + version: '1.0', + supports: ['lightning'] + } +} + +export async function sendPayment (bolt11) { + const { url, adminKey } = config + + const hash = bolt11Tags(bolt11).payment_hash + logger.info('sending payment:', `payment_hash=${hash}`) + + try { + const response = await postPayment(url, adminKey, bolt11) + + const checkResponse = await getPayment(url, adminKey, response.payment_hash) + if (!checkResponse.preimage) { + throw new Error('No preimage') + } + + const preimage = checkResponse.preimage + logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`) + return { preimage } + } catch (err) { + logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.()) + throw err + } +} + +const getWallet = async (baseUrl, adminKey) => { + const url = baseUrl.replace(/\/+$/, '') + const path = '/api/v1/wallet' + + const headers = new Headers() + headers.append('Accept', 'application/json') + headers.append('Content-Type', 'application/json') + headers.append('X-Api-Key', adminKey) + + const res = await fetch(url + path, { method: 'GET', headers }) + if (!res.ok) { + const errBody = await res.json() + throw new Error(errBody.detail) + } + + const wallet = await res.json() + return wallet +} + +async function postPayment (baseUrl, adminKey, bolt11) { + const url = baseUrl.replace(/\/+$/, '') + const path = '/api/v1/payments' + + const headers = new Headers() + headers.append('Accept', 'application/json') + headers.append('Content-Type', 'application/json') + headers.append('X-Api-Key', adminKey) + + const body = JSON.stringify({ bolt11, out: true }) + + const res = await fetch(url + path, { method: 'POST', headers, body }) + if (!res.ok) { + const errBody = await res.json() + throw new Error(errBody.detail) + } + + const payment = await res.json() + return payment +} + +async function getPayment (baseUrl, adminKey, paymentHash) { + const url = baseUrl.replace(/\/+$/, '') + const path = `/api/v1/payments/${paymentHash}` + + const headers = new Headers() + headers.append('Accept', 'application/json') + headers.append('Content-Type', 'application/json') + headers.append('X-Api-Key', adminKey) + + const res = await fetch(url + path, { method: 'GET', headers }) + if (!res.ok) { + const errBody = await res.json() + throw new Error(errBody.detail) + } + + const payment = await res.json() + return payment +}