Compare commits
6 Commits
c3d709b025
...
b8e153a4be
Author | SHA1 | Date | |
---|---|---|---|
|
b8e153a4be | ||
|
4a14e0342b | ||
|
2da3762d40 | ||
|
da71103e42 | ||
|
b9d30b4076 | ||
|
10e58d41c7 |
@ -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')
|
||||
|
@ -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) {
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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?.())
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -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?.())
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -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?.())
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -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;
|
||||
$$;
|
Loading…
x
Reference in New Issue
Block a user