import { createContext, useCallback, useContext, useEffect, useState } from 'react' import { useWalletLogger } from '../logger' import { Status, migrateLocalStorage } from '.' import { bolt11Tags } from '@/lib/bolt11' import { Wallet } from '@/lib/constants' import { useMe } from '../me' // Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts const LNbitsContext = createContext() 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 } const postPayment = async (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 } const getPayment = async (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 } export function LNbitsProvider ({ children }) { const me = useMe() const [url, setUrl] = useState('') const [adminKey, setAdminKey] = useState('') const [status, setStatus] = useState() const { logger } = useWalletLogger(Wallet.LNbits) const name = 'LNbits' let storageKey = 'webln:provider:lnbits' if (me) { storageKey = `${storageKey}:${me.id}` } const getInfo = useCallback(async () => { const response = await getWallet(url, adminKey) return { node: { alias: response.name, pubkey: '' }, methods: [ 'getInfo', 'getBalance', 'sendPayment' ], version: '1.0', supports: ['lightning'] } }, [url, adminKey]) const sendPayment = useCallback(async (bolt11) => { 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 } }, [logger, url, adminKey]) const loadConfig = useCallback(async () => { let configStr = window.localStorage.getItem(storageKey) setStatus(Status.Initialized) if (!configStr) { if (me) { // backwards compatibility: try old storageKey const oldStorageKey = storageKey.split(':').slice(0, -1).join(':') configStr = migrateLocalStorage(oldStorageKey, storageKey) } if (!configStr) { logger.info('no existing config found') return } } const config = JSON.parse(configStr) const { url, adminKey } = config setUrl(url) setAdminKey(adminKey) logger.info( 'loaded wallet config: ' + 'adminKey=****** ' + `url=${url}`) try { // validate config by trying to fetch wallet logger.info('trying to fetch wallet') await getWallet(url, adminKey) logger.ok('wallet found') setStatus(Status.Enabled) logger.ok('wallet enabled') } catch (err) { logger.error('invalid config:', err) setStatus(Status.Error) logger.info('wallet disabled') throw err } }, [me, logger]) const saveConfig = useCallback(async (config) => { // immediately store config so it's not lost even if config is invalid setUrl(config.url) setAdminKey(config.adminKey) // 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 // https://thenewstack.io/leveraging-web-workers-to-safely-store-access-tokens/ window.localStorage.setItem(storageKey, JSON.stringify(config)) logger.info( 'saved wallet config: ' + 'adminKey=****** ' + `url=${config.url}`) try { // validate config by trying to fetch wallet logger.info('trying to fetch wallet') await getWallet(config.url, config.adminKey) logger.ok('wallet found') setStatus(Status.Enabled) logger.ok('wallet enabled') } catch (err) { logger.error('invalid config:', err) setStatus(Status.Error) logger.info('wallet disabled') throw err } }, []) const clearConfig = useCallback(() => { window.localStorage.removeItem(storageKey) setUrl('') setAdminKey('') setStatus(undefined) }, []) useEffect(() => { loadConfig().catch(console.error) }, []) const value = { name, url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment } return ( {children} ) } export function useLNbits () { return useContext(LNbitsContext) }