Compare commits

..

6 Commits

Author SHA1 Message Date
keyan
b8e153a4be deal with webln unlock in sendPayment 2024-04-27 20:00:54 -05:00
keyan
4a14e0342b don't allow lnc edits because they won't work 2024-04-27 19:02:16 -05:00
keyan
2da3762d40 unattach -> detach 2024-04-27 18:37:57 -05:00
keyan
da71103e42 clear lnc state on detach 2024-04-27 18:37:57 -05:00
keyan
b9d30b4076 count pending withdrawals toward balance 2024-04-27 18:37:57 -05:00
ekzyis
10e58d41c7
Remove .env.local from env_file (#1113)
Arrays for env_file are only supported in Docker Compose >=v2.24 which is too new (from January 2024). Most distros distribute older packages.

Since --env-file as defined in the sndev script acts as an override for env_file anyway, we can safely remove it here.

Co-authored-by: ekzyis <ekzyis@ekzy.is>
2024-04-27 18:25:37 -05:00
10 changed files with 110 additions and 61 deletions

View File

@ -67,7 +67,7 @@ export default async function serialize (trx, { models, lnd, me, hash, hmac, fee
bail(new Error('too many pending invoices'))
}
if (error.message.includes('SN_INV_EXCEED_BALANCE')) {
bail(new Error(`pending invoices must not cause balance to exceed ${numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))}`))
bail(new Error(`pending invoices and withdrawals must not cause balance to exceed ${numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))}`))
}
if (error.message.includes('40001') || error.code === 'P2034') {
throw new Error('wallet balance serialization failure - try again')

View File

@ -1,19 +1,21 @@
import QRCode from 'qrcode.react'
import { CopyInput, InputSkeleton } from './form'
import InvoiceStatus from './invoice-status'
import { useEffect } from 'react'
import { useEffect, useRef } from 'react'
import { useWebLN } from './webln'
import SimpleCountdown from './countdown'
import Bolt11Info from './bolt11-info'
export default function Qr ({ asIs, value, webLn, statusVariant, description, status }) {
const qrValue = asIs ? value : 'lightning:' + value.toUpperCase()
const provider = useWebLN()
// XXX antipattern ... we shouldn't be getting multiple renders
const sendPayment = useRef(false)
useEffect(() => {
async function effect () {
if (webLn && provider) {
if (webLn && provider && !sendPayment.current) {
sendPayment.current = true
try {
await provider.sendPayment({ bolt11: value })
} catch (e) {

View File

@ -57,7 +57,7 @@ export function WalletCard ({ title, badges, provider, status }) {
export function WalletButtonBar ({
status, disable,
className, children, onDelete, onCancel, hasCancel = true,
createText = 'attach', deleteText = 'unattach', editText = 'save'
createText = 'attach', deleteText = 'detach', editText = 'save'
}) {
const configured = isConfigured(status)
return (

View File

@ -91,9 +91,6 @@ function RawWebLNProvider ({ children }) {
flowId: flowId || hash,
type: 'payment',
onPending: async () => {
if (provider.status === Status.Locked) {
await provider.unlock()
}
await provider.sendPayment(bolt11)
},
// hash and hmac are only passed for JIT invoices

View File

@ -44,14 +44,57 @@ export function LNCProvider ({ children }) {
return await lnc.lightning.getInfo()
}, [logger, lnc])
const unlock = useCallback(async (connect) => {
if (status === Status.Enabled) return config.password
return await new Promise((resolve, reject) => {
const cancelAndReject = async () => {
reject(new Error('password canceled'))
}
showModal(onClose => {
return (
<Form
initial={{
password: ''
}}
onSubmit={async (values) => {
try {
lnc.credentials.password = values?.password
setStatus(Status.Enabled)
setConfig({ pairingPhrase: lnc.credentials.pairingPhrase, password: values.password })
logger.ok('wallet enabled')
onClose()
resolve(values.password)
} catch (err) {
logger.error('failed attempt to unlock wallet', err)
throw err
}
}}
>
<h4 className='text-center mb-3'>Unlock LNC</h4>
<PasswordInput
label='password'
name='password'
/>
<div className='mt-5 d-flex justify-content-between'>
<CancelButton onClick={() => { onClose(); cancelAndReject() }} />
<SubmitButton variant='primary'>unlock</SubmitButton>
</div>
</Form>
)
}, { onClose: cancelAndReject })
})
}, [logger, showModal, setConfig, lnc, status])
const sendPayment = useCallback(async (bolt11) => {
const hash = bolt11Tags(bolt11).payment_hash
logger.info('sending payment:', `payment_hash=${hash}`)
return await mutex.runExclusive(async () => {
try {
const password = await unlock()
// credentials need to be decrypted before connecting after a disconnect
lnc.credentials.password = config?.password || XXX_DEFAULT_PASSWORD
lnc.credentials.password = password || XXX_DEFAULT_PASSWORD
await lnc.connect()
const { paymentError, paymentPreimage: preimage } =
await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 })
@ -89,15 +132,13 @@ export function LNCProvider ({ children }) {
}
}
})
}, [logger, lnc, config])
}, [logger, lnc, unlock])
const saveConfig = useCallback(async config => {
setConfig(config)
console.log(config)
try {
lnc.credentials.pairingPhrase = config.pairingPhrase
lnc.credentials.password = config?.password || XXX_DEFAULT_PASSWORD
await lnc.connect()
await validateNarrowPerms(lnc)
lnc.credentials.password = config?.password || XXX_DEFAULT_PASSWORD
@ -117,49 +158,10 @@ export function LNCProvider ({ children }) {
await lnc.credentials.clear(false)
if (lnc.isConnected) lnc.disconnect()
setStatus(undefined)
setConfig({})
logger.info('cleared config')
}, [logger, lnc])
const unlock = useCallback(async (connect) => {
return await new Promise((resolve, reject) => {
const cancelAndReject = async () => {
reject(new Error('password canceled'))
}
showModal(onClose => {
return (
<Form
initial={{
password: ''
}}
onSubmit={async (values) => {
try {
lnc.credentials.password = values?.password
setStatus(Status.Enabled)
setConfig({ pairingPhrase: lnc.credentials.pairingPhrase, password: values.password })
logger.ok('wallet enabled')
onClose()
resolve()
} catch (err) {
logger.error('failed attempt to unlock wallet', err)
throw err
}
}}
>
<h4 className='text-center mb-3'>Unlock LNC</h4>
<PasswordInput
label='password'
name='password'
/>
<div className='mt-5 d-flex justify-content-between'>
<CancelButton onClick={() => { onClose(); cancelAndReject() }} />
<SubmitButton variant='primary'>unlock</SubmitButton>
</div>
</Form>
)
}, { onClose: cancelAndReject })
})
}, [logger, showModal, setConfig, lnc])
useEffect(() => {
(async () => {
try {

View File

@ -1,7 +1,4 @@
x-env_file: &env_file
- .env.development
- path: .env.local
required: false
x-env_file: &env_file .env.development
x-healthcheck: &healthcheck
interval: 10s
timeout: 10s

View File

@ -73,7 +73,7 @@ export default function LNbits () {
router.push('/settings/wallets')
} catch (err) {
console.error(err)
toaster.danger('failed to unattach: ' + err.message || err.toString?.())
toaster.danger('failed to detach: ' + err.message || err.toString?.())
}
}}
/>

View File

@ -5,7 +5,7 @@ import Info from '@/components/info'
import { CenterLayout } from '@/components/layout'
import Text from '@/components/text'
import { useToast } from '@/components/toast'
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
import { WalletButtonBar, WalletCard, isConfigured } from '@/components/wallet-card'
import WalletLogs from '@/components/wallet-logs'
import { Status, useWebLNConfigurator } from '@/components/webln'
import { XXX_DEFAULT_PASSWORD, useLNC } from '@/components/webln/lnc'
@ -23,6 +23,7 @@ export default function LNC () {
const { status, clearConfig, saveConfig, config, name, unlock } = lnc
const isDefault = provider?.name === name
const unlocking = useRef(false)
const configured = isConfigured(status)
useEffect(() => {
if (!unlocking.current && status === Status.Locked) {
@ -46,7 +47,6 @@ export default function LNC () {
schema={lncSchema}
onSubmit={async ({ isDefault, ...values }) => {
try {
await clearConfig()
await saveConfig(values)
if (isDefault) setProvider(lnc)
toaster.success('saved settings')
@ -70,6 +70,7 @@ export default function LNC () {
name='pairingPhrase'
initialValue={config?.pairingPhrase}
newPass={config?.pairingPhrase === undefined}
readOnly={configured}
required
autoFocus
/>
@ -78,6 +79,7 @@ export default function LNC () {
name='password'
initialValue={defaultPassword ? '' : config?.password}
newPass={config?.password === undefined || defaultPassword}
readOnly={configured}
hint='encrypts your pairing phrase when stored locally'
/>
<ClientCheckbox
@ -94,7 +96,7 @@ export default function LNC () {
router.push('/settings/wallets')
} catch (err) {
console.error(err)
toaster.danger('failed to unattach: ' + err.message || err.toString?.())
toaster.danger('failed to detach: ' + err.message || err.toString?.())
}
}}
/>

View File

@ -66,7 +66,7 @@ export default function NWC () {
router.push('/settings/wallets')
} catch (err) {
console.error(err)
toaster.danger('failed to unattach: ' + err.message || err.toString?.())
toaster.danger('failed to detach: ' + err.message || err.toString?.())
}
}}
/>

View File

@ -0,0 +1,49 @@
CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, preimage TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone,
msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, lud18_data JSONB, inv_limit INTEGER, balance_limit_msats BIGINT)
RETURNS "Invoice"
LANGUAGE plpgsql
AS $$
DECLARE
invoice "Invoice";
inv_limit_reached BOOLEAN;
balance_limit_reached BOOLEAN;
inv_pending_msats BIGINT;
wdwl_pending_msats BIGINT;
BEGIN
PERFORM ASSERT_SERIALIZED();
-- prevent too many pending invoices
SELECT inv_limit > 0 AND count(*) >= inv_limit, COALESCE(sum("msatsRequested"), 0) INTO inv_limit_reached, inv_pending_msats
FROM "Invoice"
WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" IS NULL AND cancelled = false;
IF inv_limit_reached THEN
RAISE EXCEPTION 'SN_INV_PENDING_LIMIT';
END IF;
-- account for pending withdrawals
SELECT COALESCE(sum("msatsPaying"), 0) + COALESCE(sum("msatsFeePaying"), 0) INTO wdwl_pending_msats
FROM "Withdrawl"
WHERE "userId" = user_id AND status IS NULL;
-- prevent pending invoices + msats from exceeding the limit
SELECT balance_limit_msats > 0 AND inv_pending_msats+wdwl_pending_msats+msats_req+msats > balance_limit_msats INTO balance_limit_reached
FROM users
WHERE id = user_id;
IF balance_limit_reached THEN
RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE';
END IF;
-- we good, proceed frens
INSERT INTO "Invoice" (hash, preimage, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at, "desc", comment, "lud18Data")
VALUES (hash, preimage, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc(), idesc, comment, lud18_data) RETURNING * INTO invoice;
IF preimage IS NOT NULL THEN
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
VALUES ('finalizeHodlInvoice', jsonb_build_object('hash', hash), 21, true, expires_at);
END IF;
RETURN invoice;
END;
$$;