diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 5a5c9600..c817cb65 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -5,7 +5,7 @@ import serialize from './serial' import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor' import { SELECT, itemQueryWithMeta } from './item' import { msatsToSats, msatsToSatsDecimal } from '@/lib/format' -import { amountSchema, ssValidate, withdrawlSchema, lnAddrSchema, formikValidate } from '@/lib/validate' +import { amountSchema, ssValidate, withdrawlSchema, lnAddrSchema, walletValidate } from '@/lib/validate' import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '@/lib/constants' import { datePivot } from '@/lib/time' import assertGofacYourself from './ofac' @@ -19,20 +19,14 @@ import { lnAddrOptions } from '@/lib/lnurl' function injectResolvers (resolvers) { console.group('injected GraphQL resolvers:') for (const w of walletDefs) { - const { fieldValidation, walletType, walletField, testConnectServer } = w - const resolverName = generateResolverName(walletField) + const resolverName = generateResolverName(w.walletField) console.log(resolverName) - // check if wallet uses the form-level validation built into Formik or a Yup schema - const validateArgs = typeof fieldValidation === 'function' - ? { formikValidate: fieldValidation } - : { schema: fieldValidation } - resolvers.Mutation[resolverName] = async (parent, { settings, ...data }, { me, models }) => { + await walletValidate(w, { ...data, ...settings }) return await upsertWallet({ - ...validateArgs, - wallet: { field: walletField, type: walletType }, - testConnectServer: (data) => testConnectServer(data, { me, models }) + wallet: { field: w.walletField, type: w.walletType }, + testConnectServer: (data) => w.testConnectServer(data, { me, models }) }, { settings, data }, { me, models }) } } @@ -353,7 +347,7 @@ const resolvers = { }, WalletDetails: { __resolveType (wallet) { - return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : 'WalletCLN' + return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : wallet.rune ? 'WalletCLN' : 'WalletLNbits' } }, Mutation: { @@ -462,7 +456,8 @@ const resolvers = { await models.$transaction([ models.wallet.delete({ where: { userId: me.id, id: Number(id) } }), - models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached' } }) + models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'INFO', message: 'receives disabled' } }), + models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached for receives' } }) ]) return true @@ -557,26 +552,20 @@ export const addWalletLog = async ({ wallet, level, message }, { me, models }) = } async function upsertWallet ( - { schema, formikValidate: validate, wallet, testConnectServer }, { settings, data }, { me, models }) { + { wallet, testConnectServer }, { settings, data }, { me, models }) { if (!me) { throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } }) } assertApiKeyNotPermitted({ me }) - if (schema) { - await ssValidate(schema, { ...data, ...settings }, { me, models }) - } - if (validate) { - await formikValidate(validate, { ...data, ...settings }) - } - if (testConnectServer) { try { await testConnectServer(data) } catch (err) { console.error(err) - const message = err.message || err.toString?.() - await addWalletLog({ wallet, level: 'ERROR', message: 'failed to attach: ' + message }, { me, models }) + const message = 'failed to create test invoice: ' + (err.message || err.toString?.()) + await addWalletLog({ wallet, level: 'ERROR', message }, { me, models }) + await addWalletLog({ wallet, level: 'INFO', message: 'receives disabled' }, { me, models }) throw new GraphQLError(message, { extensions: { code: 'BAD_INPUT' } }) } } @@ -632,7 +621,7 @@ async function upsertWallet ( userId: me.id, wallet: wallet.type, level: 'SUCCESS', - message: id ? 'wallet updated' : 'wallet attached' + message: id ? 'receive details updated' : 'wallet attached for receives' } }), models.walletLog.create({ @@ -640,7 +629,7 @@ async function upsertWallet ( userId: me.id, wallet: wallet.type, level: enabled ? 'SUCCESS' : 'INFO', - message: enabled ? 'wallet enabled' : 'wallet disabled' + message: enabled ? 'receives enabled' : 'receives disabled' } }) ) diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index b8e9935a..c37f35c2 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -2,19 +2,22 @@ import { gql } from 'graphql-tag' import { generateResolverName } from '@/lib/wallet' import walletDefs from 'wallets/server' +import { isServerField } from 'wallets' function injectTypeDefs (typeDefs) { console.group('injected GraphQL type defs:') const injected = walletDefs.map( (w) => { let args = 'id: ID, ' - args += w.fields.map(f => { - let arg = `${f.name}: String` - if (!f.optional) { - arg += '!' - } - return arg - }).join(', ') + args += w.fields + .filter(isServerField) + .map(f => { + let arg = `${f.name}: String` + if (!f.optional) { + arg += '!' + } + return arg + }).join(', ') args += ', settings: AutowithdrawSettings!' const resolverName = generateResolverName(w.walletField) const typeDef = `${resolverName}(${args}): Boolean` @@ -74,7 +77,12 @@ const typeDefs = ` cert: String } - union WalletDetails = WalletLNAddr | WalletLND | WalletCLN + type WalletLNbits { + url: String! + invoiceKey: String! + } + + union WalletDetails = WalletLNAddr | WalletLND | WalletCLN | WalletLNbits input AutowithdrawSettings { autoWithdrawThreshold: Int! diff --git a/components/autowithdraw-shared.js b/components/autowithdraw-shared.js index e5f28244..ac0db019 100644 --- a/components/autowithdraw-shared.js +++ b/components/autowithdraw-shared.js @@ -25,10 +25,15 @@ export function AutowithdrawSettings ({ wallet }) { setSendThreshold(Math.max(Math.floor(threshold / 10), 1)) }, [autoWithdrawThreshold]) + const [mounted, setMounted] = useState(false) + useEffect(() => { + setMounted(true) + }, []) + return ( <> !value || HEX_REGEX.test(value) + }) +}) + const titleValidator = string().required('required').trim().max( MAX_TITLE_LENGTH, ({ max, value }) => `-${Math.abs(max - value.length)} characters remaining` @@ -621,7 +637,7 @@ export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) => return accum }, {}))) -export const lnbitsSchema = object({ +export const lnbitsSchema = object().shape({ url: process.env.NODE_ENV === 'development' ? string() .or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url') @@ -643,8 +659,25 @@ export const lnbitsSchema = object({ } return true }), - adminKey: string().length(32).required('required') -}) + adminKey: string().length(32).hex() + .when(['invoiceKey'], ([invoiceKey], schema) => { + if (!invoiceKey) return schema.required('required if invoice key not set') + return schema.test({ + test: adminKey => adminKey !== invoiceKey, + message: 'admin key cannot be the same as invoice key' + }) + }), + invoiceKey: string().length(32).hex() + .when(['adminKey'], ([adminKey], schema) => { + if (!adminKey) return schema.required('required if admin key not set') + return schema.test({ + test: invoiceKey => adminKey !== invoiceKey, + message: 'invoice key cannot be the same as admin key' + }) + }) + // need to set order to avoid cyclic dependencies in Yup schema + // see https://github.com/jquense/yup/issues/176#issuecomment-367352042 +}, ['adminKey', 'invoiceKey']) export const nwcSchema = object({ nwcUrl: string() diff --git a/pages/settings/wallets/[wallet].js b/pages/settings/wallets/[wallet].js index 89e3652a..c4121ffc 100644 --- a/pages/settings/wallets/[wallet].js +++ b/pages/settings/wallets/[wallet].js @@ -57,15 +57,11 @@ export default function WalletSettings () { await wallet.save(values) - if (values.enabled) wallet.enable() - else wallet.disable() - toaster.success('saved settings') router.push('/settings/wallets') } catch (err) { console.error(err) - const message = 'failed to attach: ' + err.message || err.toString?.() - toaster.danger(message) + toaster.danger(err.message || err.toString?.()) } }} > @@ -106,12 +102,12 @@ export default function WalletSettings () { function WalletFields ({ wallet: { config, fields, isConfigured } }) { return fields - .map(({ name, label, type, help, optional, editable, ...props }, i) => { + .map(({ name, label, type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => { const rawProps = { ...props, name, initialValue: config?.[name], - readOnly: isConfigured && editable === false, + readOnly: isConfigured && editable === false && !!config?.[name], groupClassName: props.hidden ? 'd-none' : undefined, label: label ? ( diff --git a/prisma/migrations/20240729195320_lnbits_recv/migration.sql b/prisma/migrations/20240729195320_lnbits_recv/migration.sql new file mode 100644 index 00000000..c795f371 --- /dev/null +++ b/prisma/migrations/20240729195320_lnbits_recv/migration.sql @@ -0,0 +1,24 @@ +-- AlterEnum +ALTER TYPE "WalletType" ADD VALUE 'LNBITS'; + +-- CreateTable +CREATE TABLE "WalletLNbits" ( + "int" SERIAL NOT NULL, + "walletId" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "url" TEXT NOT NULL, + "invoiceKey" TEXT NOT NULL, + + CONSTRAINT "WalletLNbits_pkey" PRIMARY KEY ("int") +); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletLNbits_walletId_key" ON "WalletLNbits"("walletId"); + +-- AddForeignKey +ALTER TABLE "WalletLNbits" ADD CONSTRAINT "WalletLNbits_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE TRIGGER wallet_lnbits_as_jsonb +AFTER INSERT OR UPDATE ON "WalletLNbits" +FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb(); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6a80a66b..8f5b52b9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -167,6 +167,7 @@ enum WalletType { LIGHTNING_ADDRESS LND CLN + LNBITS } model Wallet { @@ -190,6 +191,7 @@ model Wallet { walletLightningAddress WalletLightningAddress? walletLND WalletLND? walletCLN WalletCLN? + walletLNbits WalletLNbits? withdrawals Withdrawl[] @@index([userId]) @@ -238,6 +240,16 @@ model WalletCLN { cert String? } +model WalletLNbits { + int Int @id @default(autoincrement()) + walletId Int @unique + wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + url String + invoiceKey String +} + model Mute { muterId Int mutedId Int diff --git a/wallets/index.js b/wallets/index.js index c449ca54..57332666 100644 --- a/wallets/index.js +++ b/wallets/index.js @@ -1,6 +1,6 @@ import { useCallback } from 'react' import { useMe } from '@/components/me' -import useLocalConfig from '@/components/use-local-state' +import useClientConfig from '@/components/use-local-state' import { useWalletLogger } from '@/components/wallet-logger' import { SSR } from '@/lib/constants' import { bolt11Tags } from '@/lib/bolt11' @@ -12,6 +12,7 @@ import { autowithdrawInitial } from '@/components/autowithdraw-shared' import { useShowModal } from '@/components/modal' import { useToast } from '../components/toast' import { generateResolverName } from '@/lib/wallet' +import { walletValidate } from '@/lib/validate' export const Status = { Initialized: 'Initialized', @@ -32,6 +33,22 @@ export function useWallet (name) { const hasConfig = wallet?.fields.length > 0 const _isConfigured = isConfigured({ ...wallet, config }) + const enablePayments = useCallback(() => { + enableWallet(name, me) + logger.ok('payments enabled') + }, [name, me, logger]) + + const disablePayments = useCallback(() => { + disableWallet(name, me) + logger.info('payments disabled') + }, [name, me, logger]) + + if (wallet) { + wallet.isConfigured = _isConfigured + wallet.enablePayments = enablePayments + wallet.disablePayments = disablePayments + } + const status = config?.enabled ? Status.Enabled : Status.Initialized const enabled = status === Status.Enabled const priority = config?.priority @@ -49,53 +66,40 @@ export function useWallet (name) { } }, [me, wallet, config, logger, status]) - const enable = useCallback(() => { - enableWallet(name, me) - logger.ok('wallet enabled') - }, [name, me, logger]) - - const disable = useCallback(() => { - disableWallet(name, me) - logger.info('wallet disabled') - }, [name, me, logger]) - const setPriority = useCallback(async (priority) => { if (_isConfigured && priority !== config.priority) { try { - await saveConfig({ ...config, priority }) + await saveConfig({ ...config, priority }, { logger }) } catch (err) { toaster.danger(`failed to change priority of ${wallet.name} wallet: ${err.message}`) } } - }, [wallet, config, logger, toaster]) + }, [wallet, config, toaster]) const save = useCallback(async (newConfig) => { + // testConnectClient should log custom INFO and OK message + // testConnectClient is optional since validation might happen during save on server + // TODO: add timeout + let validConfig try { - // testConnectClient should log custom INFO and OK message - // testConnectClient is optional since validation might happen during save on server - // TODO: add timeout - const validConfig = await wallet.testConnectClient?.(newConfig, { me, logger }) - await saveConfig(validConfig ?? newConfig) - logger.ok(_isConfigured ? 'wallet updated' : 'wallet attached') + validConfig = await wallet.testConnectClient?.(newConfig, { me, logger }) } catch (err) { - const message = err.message || err.toString?.() - logger.error('failed to attach: ' + message) + logger.error(err.message) throw err } - }, [_isConfigured, saveConfig, me, logger]) + await saveConfig(validConfig ?? newConfig, { logger }) + }, [saveConfig, me, logger]) // delete is a reserved keyword const delete_ = useCallback(async () => { try { - await clearConfig() - logger.ok('wallet detached') - disable() + await clearConfig({ logger }) } catch (err) { const message = err.message || err.toString?.() logger.error(message) throw err } - }, [clearConfig, logger, disable]) + }, [clearConfig, logger, disablePayments]) if (!wallet) return null @@ -106,11 +110,8 @@ export function useWallet (name) { save, delete: delete_, deleteLogs, - enable, - disable, setPriority, hasConfig, - isConfigured: _isConfigured, status, enabled, priority, @@ -118,34 +119,111 @@ export function useWallet (name) { } } +function extractConfig (fields, config, client) { + return Object.entries(config).reduce((acc, [key, value]) => { + const field = fields.find(({ name }) => name === key) + + // filter server config which isn't specified as wallet fields + if (client && (key.startsWith('autoWithdraw') || key === 'id')) return acc + + // field might not exist because config.enabled doesn't map to a wallet field + if (!field || (client ? isClientField(field) : isServerField(field))) { + return { + ...acc, + [key]: value + } + } else { + return acc + } + }, {}) +} + +export function isServerField (f) { + return f.serverOnly || !f.clientOnly +} + +export function isClientField (f) { + return f.clientOnly || !f.serverOnly +} + +function extractClientConfig (fields, config) { + return extractConfig(fields, config, true) +} + +function extractServerConfig (fields, config) { + return extractConfig(fields, config, false) +} + function useConfig (wallet) { const me = useMe() const storageKey = getStorageKey(wallet?.name, me) - const [localConfig, setLocalConfig, clearLocalConfig] = useLocalConfig(storageKey) + const [clientConfig, setClientConfig, clearClientConfig] = useClientConfig(storageKey) const [serverConfig, setServerConfig, clearServerConfig] = useServerConfig(wallet) - const hasLocalConfig = !!wallet?.sendPayment + const hasClientConfig = !!wallet?.sendPayment const hasServerConfig = !!wallet?.walletType - const config = { - // only include config if it makes sense for this wallet - // since server config always returns default values for autowithdraw settings - // which might be confusing to have for wallets that don't support autowithdraw - ...(hasLocalConfig ? localConfig : {}), - ...(hasServerConfig ? serverConfig : {}) + let config = {} + if (hasClientConfig) config = clientConfig + if (hasServerConfig) { + const { enabled } = config || {} + config = { + ...config, + ...serverConfig + } + // wallet is enabled if enabled is set in client or server config + config.enabled ||= enabled } - const saveConfig = useCallback(async (config) => { - if (hasLocalConfig) setLocalConfig(config) - if (hasServerConfig) await setServerConfig(config) - }, [wallet]) + const saveConfig = useCallback(async (newConfig, { logger }) => { + // NOTE: + // verifying the client/server configuration before saving it + // prevents unsetting just one configuration if both are set. + // This means there is no way of unsetting just one configuration + // since 'detach' detaches both. + // Not optimal UX but the trade-off is saving invalid configurations + // and maybe it's not that big of an issue. + if (hasClientConfig) { + const newClientConfig = extractClientConfig(wallet.fields, newConfig) - const clearConfig = useCallback(async () => { - if (hasLocalConfig) clearLocalConfig() + let valid = true + try { + await walletValidate(wallet, newClientConfig) + } catch { + valid = false + } + + if (valid) { + setClientConfig(newClientConfig) + logger.ok(wallet.isConfigured ? 'payment details updated' : 'wallet attached for payments') + if (newConfig.enabled) wallet.enablePayments() + else wallet.disablePayments() + } + } + if (hasServerConfig) { + const newServerConfig = extractServerConfig(wallet.fields, newConfig) + + let valid = true + try { + await walletValidate(wallet, newServerConfig) + } catch { + valid = false + } + + if (valid) await setServerConfig(newServerConfig) + } + }, [hasClientConfig, hasServerConfig, setClientConfig, setServerConfig, wallet]) + + const clearConfig = useCallback(async ({ logger }) => { + if (hasClientConfig) { + clearClientConfig() + wallet.disablePayments() + logger.ok('wallet detached for payments') + } if (hasServerConfig) await clearServerConfig() - }, [wallet]) + }, [hasClientConfig, hasServerConfig, clearClientConfig, clearServerConfig, wallet]) return [config, saveConfig, clearConfig] } @@ -174,6 +252,8 @@ function useServerConfig (wallet) { enabled: data?.walletByType?.enabled, ...data?.walletByType?.wallet } + delete serverConfig.__typename + const autowithdrawSettings = autowithdrawInitial({ me }) const config = { ...serverConfig, ...autowithdrawSettings } @@ -189,8 +269,8 @@ function useServerConfig (wallet) { return await client.mutate({ mutation, variables: { - id: walletId, ...config, + id: walletId, settings: { autoWithdrawThreshold: Number(autoWithdrawThreshold), autoWithdrawMaxFeePercent: Number(autoWithdrawMaxFeePercent), @@ -206,6 +286,9 @@ function useServerConfig (wallet) { }, [client, walletId]) const clearConfig = useCallback(async () => { + // only remove wallet if there is a wallet to remove + if (!walletId) return + try { await client.mutate({ mutation: REMOVE_WALLET, @@ -224,17 +307,21 @@ function generateMutation (wallet) { const resolverName = generateResolverName(wallet.walletField) let headerArgs = '$id: ID, ' - headerArgs += wallet.fields.map(f => { - let arg = `$${f.name}: String` - if (!f.optional) { - arg += '!' - } - return arg - }).join(', ') + headerArgs += wallet.fields + .filter(isServerField) + .map(f => { + let arg = `$${f.name}: String` + if (!f.optional) { + arg += '!' + } + return arg + }).join(', ') headerArgs += ', $settings: AutowithdrawSettings!' let inputArgs = 'id: $id, ' - inputArgs += wallet.fields.map(f => `${f.name}: $${f.name}`).join(', ') + inputArgs += wallet.fields + .filter(isServerField) + .map(f => `${f.name}: $${f.name}`).join(', ') inputArgs += ', settings: $settings' return gql`mutation ${resolverName}(${headerArgs}) { diff --git a/wallets/lnbits/ATTACH.md b/wallets/lnbits/ATTACH.md new file mode 100644 index 00000000..b34a213b --- /dev/null +++ b/wallets/lnbits/ATTACH.md @@ -0,0 +1,27 @@ +For testing LNbits, you need to create a LNbits account first via the web interface. + +By default, you can access it at `localhost:5001` (see `LNBITS_WEB_PORT` in .env.development). + +After you created a wallet, you should find the invoice and admin key under `Node URL, API keys and API docs`. + +> [!IMPORTANT] +> +> Since your browser is running on your host machine but the server is running inside a docker container, the server will not be able to reach LNbits with `localhost:5001` to create invoices. This makes it hard to test send+receive at the same time. +> +> For now, you need to patch the `_createInvoice` function in wallets/lnbits/server.js to always use `lnbits:5000` as the URL: +> +> ```diff +> diff --git a/wallets/lnbits/server.js b/wallets/lnbits/server.js +> index 39949775..e3605c45 100644 +> --- a/wallets/lnbits/server.js +> +++ b/wallets/lnbits/server.js +> @@ -11,6 +11,7 @@ async function _createInvoice ({ url, invoiceKey, amount, expiry }, { me }) { +> const memo = me.hideInvoiceDesc ? undefined : 'autowithdraw to LNbits from SN' +> const body = JSON.stringify({ amount, unit: 'sat', expiry, memo, out: false }) +> +> + url = 'http://lnbits:5000' +> const res = await fetch(url + path, { method: 'POST', headers, body }) +> if (!res.ok) { +> const errBody = await res.json() +> ``` +> diff --git a/wallets/lnbits/client.js b/wallets/lnbits/client.js index 38c74f66..8b88f79a 100644 --- a/wallets/lnbits/client.js +++ b/wallets/lnbits/client.js @@ -1,10 +1,10 @@ export * from 'wallets/lnbits' -export async function testConnectClient ({ url, adminKey }, { logger }) { +export async function testConnectClient ({ url, adminKey, invoiceKey }, { logger }) { logger.info('trying to fetch wallet') url = url.replace(/\/+$/, '') - await getWallet({ url, adminKey }) + await getWallet({ url, adminKey, invoiceKey }) logger.ok('wallet found') } @@ -23,13 +23,13 @@ export async function sendPayment (bolt11, { url, adminKey }) { return { preimage } } -async function getWallet ({ url, adminKey }) { +async function getWallet ({ url, adminKey, invoiceKey }) { 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) + headers.append('X-Api-Key', adminKey || invoiceKey) const res = await fetch(url + path, { method: 'GET', headers }) if (!res.ok) { diff --git a/wallets/lnbits/index.js b/wallets/lnbits/index.js index 7d228e33..3473f47e 100644 --- a/wallets/lnbits/index.js +++ b/wallets/lnbits/index.js @@ -8,17 +8,32 @@ export const fields = [ label: 'lnbits url', type: 'text' }, + { + name: 'invoiceKey', + label: 'invoice key', + type: 'password', + optional: 'for receiving', + serverOnly: true, + editable: false + }, { name: 'adminKey', label: 'admin key', - type: 'password' + type: 'password', + optional: 'for sending', + clientOnly: true, + editable: false } ] export const card = { title: 'LNbits', subtitle: 'use [LNbits](https://lnbits.com/) for payments', - badges: ['send only'] + badges: ['send & receive'] } export const fieldValidation = lnbitsSchema + +export const walletType = 'LNBITS' + +export const walletField = 'walletLNbits' diff --git a/wallets/lnbits/server.js b/wallets/lnbits/server.js new file mode 100644 index 00000000..39949775 --- /dev/null +++ b/wallets/lnbits/server.js @@ -0,0 +1,30 @@ +export * from 'wallets/lnbits' + +async function _createInvoice ({ url, invoiceKey, amount, expiry }, { me }) { + 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', invoiceKey) + + const memo = me.hideInvoiceDesc ? undefined : 'autowithdraw to LNbits from SN' + const body = JSON.stringify({ amount, unit: 'sat', expiry, memo, out: false }) + + 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.payment_request +} + +export async function testConnectServer ({ url, invoiceKey }, { me }) { + return await _createInvoice({ url, invoiceKey, amount: 1, expiry: 1 }, { me }) +} + +export async function createInvoice ({ amount, maxFee }, { url, invoiceKey }, { me }) { + return await _createInvoice({ url, invoiceKey, amount, expiry: 360 }, { me }) +} diff --git a/wallets/server.js b/wallets/server.js index b625824d..89dad61e 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -1,5 +1,6 @@ import * as lnd from 'wallets/lnd/server' import * as cln from 'wallets/cln/server' import * as lnAddr from 'wallets/lightning-address/server' +import * as lnbits from 'wallets/lnbits/server' -export default [lnd, cln, lnAddr] +export default [lnd, cln, lnAddr, lnbits]