Compare commits
7 Commits
fd2008e5d1
...
a34c8dc7e9
Author | SHA1 | Date | |
---|---|---|---|
|
a34c8dc7e9 | ||
|
64de3c3b94 | ||
|
98a27caaa9 | ||
|
4961cc045b | ||
|
e5f8c4e8e8 | ||
|
6aa5991520 | ||
|
990128da86 |
@ -7,7 +7,7 @@ import { SELECT } from './item'
|
||||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '@/lib/format'
|
||||
import { CLNAutowithdrawSchema, LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '@/lib/validate'
|
||||
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '@/lib/constants'
|
||||
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT, Wallet } from '@/lib/constants'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import assertGofacYourself from './ofac'
|
||||
import assertApiKeyNotPermitted from './apiKey'
|
||||
@ -434,11 +434,11 @@ export default {
|
||||
data.macaroon = ensureB64(data.macaroon)
|
||||
data.cert = ensureB64(data.cert)
|
||||
|
||||
const walletType = 'LND'
|
||||
const wallet = Wallet.LND
|
||||
return await upsertWallet(
|
||||
{
|
||||
schema: LNDAutowithdrawSchema,
|
||||
walletType,
|
||||
wallet,
|
||||
testConnect: async ({ cert, macaroon, socket }) => {
|
||||
try {
|
||||
const { lnd } = await authenticatedLndGrpc({
|
||||
@ -453,12 +453,12 @@ export default {
|
||||
expires_at: new Date()
|
||||
})
|
||||
// we wrap both calls in one try/catch since connection attempts happen on RPC calls
|
||||
await addWalletLog({ wallet: walletType, level: 'SUCCESS', message: 'connected to LND' }, { me, models })
|
||||
await addWalletLog({ wallet, level: 'SUCCESS', message: 'connected to LND' }, { me, models })
|
||||
return inv
|
||||
} catch (err) {
|
||||
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
|
||||
const details = err[2]?.err?.details || err.message || err.toString?.()
|
||||
await addWalletLog({ wallet: walletType, level: 'ERROR', message: `could not connect to LND: ${details}` }, { me, models })
|
||||
await addWalletLog({ wallet, level: 'ERROR', message: `could not connect to LND: ${details}` }, { me, models })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@ -468,11 +468,11 @@ export default {
|
||||
upsertWalletCLN: async (parent, { settings, ...data }, { me, models }) => {
|
||||
data.cert = ensureB64(data.cert)
|
||||
|
||||
const walletType = 'CLN'
|
||||
const wallet = Wallet.CLN
|
||||
return await upsertWallet(
|
||||
{
|
||||
schema: CLNAutowithdrawSchema,
|
||||
walletType,
|
||||
wallet,
|
||||
testConnect: async ({ socket, rune, cert }) => {
|
||||
try {
|
||||
const inv = await createInvoiceCLN({
|
||||
@ -483,11 +483,11 @@ export default {
|
||||
msats: 'any',
|
||||
expiry: 0
|
||||
})
|
||||
await addWalletLog({ wallet: walletType, level: 'SUCCESS', message: 'connected to CLN' }, { me, models })
|
||||
await addWalletLog({ wallet, level: 'SUCCESS', message: 'connected to CLN' }, { me, models })
|
||||
return inv
|
||||
} catch (err) {
|
||||
const details = err.details || err.message || err.toString?.()
|
||||
await addWalletLog({ wallet: walletType, level: 'ERROR', message: `could not connect to CLN: ${details}` }, { me, models })
|
||||
await addWalletLog({ wallet, level: 'ERROR', message: `could not connect to CLN: ${details}` }, { me, models })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@ -495,14 +495,14 @@ export default {
|
||||
{ settings, data }, { me, models })
|
||||
},
|
||||
upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => {
|
||||
const walletType = 'LIGHTNING_ADDRESS'
|
||||
const wallet = Wallet.LnAddr
|
||||
return await upsertWallet(
|
||||
{
|
||||
schema: lnAddrAutowithdrawSchema,
|
||||
walletType,
|
||||
wallet,
|
||||
testConnect: async ({ address }) => {
|
||||
const options = await lnAddrOptions(address)
|
||||
await addWalletLog({ wallet: walletType, level: 'SUCCESS', message: 'fetched payment details' }, { me, models })
|
||||
await addWalletLog({ wallet, level: 'SUCCESS', message: 'fetched payment details' }, { me, models })
|
||||
return options
|
||||
}
|
||||
},
|
||||
@ -523,6 +523,15 @@ export default {
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet deleted' } })
|
||||
])
|
||||
|
||||
return true
|
||||
},
|
||||
deleteWalletLogs: async (parent, { wallet }, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
|
||||
}
|
||||
|
||||
await models.walletLog.deleteMany({ where: { userId: me.id, wallet } })
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
@ -557,14 +566,14 @@ export default {
|
||||
|
||||
export const addWalletLog = async ({ wallet, level, message }, { me, models }) => {
|
||||
try {
|
||||
await models.walletLog.create({ data: { userId: me.id, wallet, level, message } })
|
||||
await models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level, message } })
|
||||
} catch (err) {
|
||||
console.error('error creating wallet log:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertWallet (
|
||||
{ schema, walletType, testConnect }, { settings, data }, { me, models }) {
|
||||
{ schema, wallet, testConnect }, { settings, data }, { me, models }) {
|
||||
if (!me) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
|
||||
}
|
||||
@ -577,7 +586,7 @@ async function upsertWallet (
|
||||
await testConnect(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
await addWalletLog({ wallet: walletType, level: 'ERROR', message: 'failed to attach wallet' }, { me, models })
|
||||
await addWalletLog({ wallet, level: 'ERROR', message: 'failed to attach wallet' }, { me, models })
|
||||
throw new GraphQLError('failed to connect to wallet', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
}
|
||||
@ -607,16 +616,13 @@ async function upsertWallet (
|
||||
}))
|
||||
}
|
||||
|
||||
const walletName = walletType === 'LND'
|
||||
? 'walletLND'
|
||||
: walletType === 'CLN' ? 'walletCLN' : 'walletLightningAddress'
|
||||
if (id) {
|
||||
txs.push(
|
||||
models.wallet.update({
|
||||
where: { id: Number(id), userId: me.id },
|
||||
data: {
|
||||
priority: priority ? 1 : 0,
|
||||
[walletName]: {
|
||||
[wallet.field]: {
|
||||
update: {
|
||||
where: { walletId: Number(id) },
|
||||
data: walletData
|
||||
@ -624,7 +630,7 @@ async function upsertWallet (
|
||||
}
|
||||
}
|
||||
}),
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: walletType, level: 'SUCCESS', message: 'wallet updated' } })
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet updated' } })
|
||||
)
|
||||
} else {
|
||||
txs.push(
|
||||
@ -632,13 +638,13 @@ async function upsertWallet (
|
||||
data: {
|
||||
priority: Number(priority),
|
||||
userId: me.id,
|
||||
type: walletType,
|
||||
[walletName]: {
|
||||
type: wallet.type,
|
||||
[wallet.field]: {
|
||||
create: walletData
|
||||
}
|
||||
}
|
||||
}),
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: walletType, level: 'SUCCESS', message: 'wallet created' } })
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet created' } })
|
||||
)
|
||||
}
|
||||
|
||||
@ -751,7 +757,7 @@ export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...
|
||||
if (autoWithdraw && decoded.destination === ourPubkey && process.env.NODE_ENV === 'production') {
|
||||
// unset lnaddr so we don't trigger another withdrawal with same destination
|
||||
await models.wallet.deleteMany({
|
||||
where: { userId: me.id, type: 'LIGHTNING_ADDRESS' }
|
||||
where: { userId: me.id, type: Wallet.LnAddr.type }
|
||||
})
|
||||
throw new Error('automated withdrawals to other stackers are not allowed')
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ export default gql`
|
||||
upsertWalletCLN(id: ID, socket: String!, rune: String!, cert: String, settings: AutowithdrawSettings!): Boolean
|
||||
upsertWalletLNAddr(id: ID, address: String!, settings: AutowithdrawSettings!): Boolean
|
||||
removeWallet(id: ID!): Boolean
|
||||
deleteWalletLogs(wallet: String): Boolean
|
||||
}
|
||||
|
||||
type Wallet {
|
||||
|
@ -331,7 +331,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
||||
}}
|
||||
onSuccess={({ url, name }) => {
|
||||
let text = innerRef.current.value
|
||||
text = text.replace(`![Uploading ${name}…]()`, ``)
|
||||
text = text.replace(`![Uploading ${name}…]()`, ``)
|
||||
helpers.setValue(text)
|
||||
const s3Keys = [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1]))
|
||||
updateImageFeesInfo({ variables: { s3Keys } })
|
||||
|
@ -90,6 +90,7 @@ a.link:visited {
|
||||
color: var(--theme-grey);
|
||||
vertical-align: text-top;
|
||||
margin-bottom: .125rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.other svg {
|
||||
@ -156,6 +157,7 @@ a.link:visited {
|
||||
.main {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.children {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useMe } from './me'
|
||||
import fancyNames from '@/lib/fancy-names.json'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||
import { WALLET_LOGS } from '@/fragments/wallet'
|
||||
import { getWalletBy } from '@/lib/constants'
|
||||
|
||||
const generateFancyName = () => {
|
||||
// 100 adjectives * 100 nouns * 10000 = 100M possible names
|
||||
@ -157,21 +158,6 @@ const initIndexedDB = async (storeName) => {
|
||||
})
|
||||
}
|
||||
|
||||
const renameWallet = (wallet) => {
|
||||
switch (wallet) {
|
||||
case 'walletLightningAddress':
|
||||
case 'LIGHTNING_ADDRESS':
|
||||
return 'lnAddr'
|
||||
case 'walletLND':
|
||||
case 'LND':
|
||||
return 'lnd'
|
||||
case 'walletCLN':
|
||||
case 'CLN':
|
||||
return 'cln'
|
||||
}
|
||||
return wallet
|
||||
}
|
||||
|
||||
const WalletLoggerProvider = ({ children }) => {
|
||||
const [logs, setLogs] = useState([])
|
||||
const idbStoreName = 'wallet_logs'
|
||||
@ -187,12 +173,33 @@ const WalletLoggerProvider = ({ children }) => {
|
||||
const existingIds = prevLogs.map(({ id }) => id)
|
||||
const logs = walletLogs
|
||||
.filter(({ id }) => !existingIds.includes(id))
|
||||
.map(({ createdAt, wallet, ...log }) => ({ ts: +new Date(createdAt), wallet: renameWallet(wallet), ...log }))
|
||||
.map(({ createdAt, wallet: walletType, ...log }) => {
|
||||
return {
|
||||
ts: +new Date(createdAt),
|
||||
wallet: getWalletBy('type', walletType).logTag,
|
||||
...log
|
||||
}
|
||||
})
|
||||
return [...prevLogs, ...logs].sort((a, b) => a.ts - b.ts)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const [deleteServerWalletLogs] = useMutation(
|
||||
gql`
|
||||
mutation deleteWalletLogs($wallet: String) {
|
||||
deleteWalletLogs(wallet: $wallet)
|
||||
}
|
||||
`,
|
||||
{
|
||||
onCompleted: (_, { variables: { wallet: walletType } }) => {
|
||||
setLogs((logs) => {
|
||||
return logs.filter(l => walletType ? l.wallet !== getWalletBy('type', walletType).logTag : false)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const saveLog = useCallback((log) => {
|
||||
if (!idb.current) {
|
||||
// IDB may not be ready yet
|
||||
@ -234,14 +241,36 @@ const WalletLoggerProvider = ({ children }) => {
|
||||
}, [])
|
||||
|
||||
const appendLog = useCallback((wallet, level, message) => {
|
||||
const log = { wallet, level, message, ts: +new Date() }
|
||||
const log = { wallet: wallet.logTag, level, message, ts: +new Date() }
|
||||
saveLog(log)
|
||||
setLogs((prevLogs) => [...prevLogs, log])
|
||||
}, [saveLog])
|
||||
|
||||
const deleteLogs = useCallback(async (wallet) => {
|
||||
if (!wallet || wallet.server) {
|
||||
await deleteServerWalletLogs({ variables: { wallet: wallet?.type } })
|
||||
}
|
||||
if (!wallet || !wallet.server) {
|
||||
const tx = idb.current.transaction(idbStoreName, 'readwrite')
|
||||
const objectStore = tx.objectStore(idbStoreName)
|
||||
const idx = objectStore.index('wallet_ts')
|
||||
const request = wallet ? idx.openCursor(window.IDBKeyRange.bound([wallet.logTag, -Infinity], [wallet.logTag, Infinity])) : idx.openCursor()
|
||||
request.onsuccess = function (event) {
|
||||
const cursor = event.target.result
|
||||
if (cursor) {
|
||||
cursor.delete()
|
||||
cursor.continue()
|
||||
} else {
|
||||
// finished
|
||||
setLogs((logs) => logs.filter(l => wallet ? l.wallet !== wallet.logTag : false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [setLogs])
|
||||
|
||||
return (
|
||||
<WalletLogsContext.Provider value={logs}>
|
||||
<WalletLoggerContext.Provider value={appendLog}>
|
||||
<WalletLoggerContext.Provider value={{ appendLog, deleteLogs }}>
|
||||
{children}
|
||||
</WalletLoggerContext.Provider>
|
||||
</WalletLogsContext.Provider>
|
||||
@ -249,14 +278,14 @@ const WalletLoggerProvider = ({ children }) => {
|
||||
}
|
||||
|
||||
export function useWalletLogger (wallet) {
|
||||
const appendLog = useContext(WalletLoggerContext)
|
||||
const { appendLog, deleteLogs: innerDeleteLogs } = useContext(WalletLoggerContext)
|
||||
|
||||
const log = useCallback(level => message => {
|
||||
// TODO:
|
||||
// also send this to us if diagnostics was enabled,
|
||||
// very similar to how the service worker logger works.
|
||||
appendLog(wallet, level, message)
|
||||
console[level !== 'error' ? 'info' : 'error'](`[${wallet}]`, message)
|
||||
console[level !== 'error' ? 'info' : 'error'](`[${wallet.logTag}]`, message)
|
||||
}, [appendLog, wallet])
|
||||
|
||||
const logger = useMemo(() => ({
|
||||
@ -265,10 +294,12 @@ export function useWalletLogger (wallet) {
|
||||
error: (...message) => log('error')(message.join(' '))
|
||||
}), [log, wallet])
|
||||
|
||||
return logger
|
||||
const deleteLogs = useCallback((w) => innerDeleteLogs(w || wallet), [innerDeleteLogs, wallet])
|
||||
|
||||
return { logger, deleteLogs }
|
||||
}
|
||||
|
||||
export function useWalletLogs (wallet) {
|
||||
const logs = useContext(WalletLogsContext)
|
||||
return logs.filter(l => !wallet || l.wallet === wallet)
|
||||
return logs.filter(l => !wallet || l.wallet === wallet.logTag)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import BackArrow from '../../svgs/arrow-left-line.svg'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Price from '../price'
|
||||
import SubSelect from '../sub-select'
|
||||
import { ANON_USER_ID, BALANCE_LIMIT_MSATS } from '../../lib/constants'
|
||||
import { ANON_USER_ID, BALANCE_LIMIT_MSATS, Wallet } from '../../lib/constants'
|
||||
import Head from 'next/head'
|
||||
import NoteIcon from '../../svgs/notification-4-fill.svg'
|
||||
import { useMe } from '../me'
|
||||
@ -22,6 +22,7 @@ import SearchIcon from '../../svgs/search-line.svg'
|
||||
import classNames from 'classnames'
|
||||
import SnIcon from '@/svgs/sn.svg'
|
||||
import { useHasNewNotes } from '../use-has-new-notes'
|
||||
import { useWalletLogger } from '../logger'
|
||||
|
||||
export function Brand ({ className }) {
|
||||
return (
|
||||
@ -162,6 +163,7 @@ export function NavWalletSummary ({ className }) {
|
||||
export function MeDropdown ({ me, dropNavKey }) {
|
||||
if (!me) return null
|
||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||
const { deleteLogs } = useWalletLogger()
|
||||
return (
|
||||
<div className='position-relative'>
|
||||
<Dropdown className={styles.dropdown} align='end'>
|
||||
@ -202,16 +204,16 @@ export function MeDropdown ({ me, dropNavKey }) {
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
try {
|
||||
// order is important because we need to be logged in to delete push subscription on server
|
||||
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
||||
if (pushSubscription) {
|
||||
await togglePushSubscription()
|
||||
}
|
||||
} catch (err) {
|
||||
// order is important because we need to be logged in to delete push subscription on server
|
||||
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
||||
if (pushSubscription) {
|
||||
// don't prevent signout because of an unsubscription error
|
||||
console.error(err)
|
||||
await togglePushSubscription().catch(console.error)
|
||||
}
|
||||
// delete client wallet logs to prevent leak of private data if a shared device was used
|
||||
await deleteLogs(Wallet.NWC).catch(console.error)
|
||||
await deleteLogs(Wallet.LNbits).catch(console.error)
|
||||
await deleteLogs(Wallet.LNC).catch(console.error)
|
||||
await signOut({ callbackUrl: '/' })
|
||||
}}
|
||||
>logout
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import LogMessage from './log-message'
|
||||
import { useWalletLogs } from './logger'
|
||||
import { useWalletLogger, useWalletLogs } from './logger'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Checkbox, Form } from './form'
|
||||
import { useField } from 'formik'
|
||||
import styles from '@/styles/log.module.css'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useToast } from './toast'
|
||||
import { useShowModal } from './modal'
|
||||
|
||||
const FollowCheckbox = ({ value, ...props }) => {
|
||||
const [,, helpers] = useField(props.name)
|
||||
@ -26,6 +29,7 @@ export default function WalletLogs ({ wallet, embedded }) {
|
||||
const [follow, setFollow] = useState(defaultFollow ?? true)
|
||||
const tableRef = useRef()
|
||||
const scrollY = useRef()
|
||||
const showModal = useShowModal()
|
||||
|
||||
useEffect(() => {
|
||||
if (follow) {
|
||||
@ -57,12 +61,21 @@ export default function WalletLogs ({ wallet, embedded }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form initial={{ follow: true }}>
|
||||
<FollowCheckbox
|
||||
label='follow logs' name='follow' value={follow}
|
||||
handleChange={setFollow}
|
||||
/>
|
||||
</Form>
|
||||
<div className='d-flex w-100 align-items-center mb-3'>
|
||||
<Form initial={{ follow: true }}>
|
||||
<FollowCheckbox
|
||||
label='follow logs' name='follow' value={follow}
|
||||
handleChange={setFollow} groupClassName='mb-0'
|
||||
/>
|
||||
</Form>
|
||||
<span
|
||||
style={{ cursor: 'pointer' }}
|
||||
className='text-muted fw-bold nav-link' onClick={() => {
|
||||
showModal(onClose => <DeleteWalletLogsObstacle wallet={wallet} onClose={onClose} />)
|
||||
}}
|
||||
>clear
|
||||
</span>
|
||||
</div>
|
||||
<div ref={tableRef} className={`${styles.logTable} ${embedded ? styles.embedded : ''}`}>
|
||||
<div className='w-100 text-center'>------ start of logs ------</div>
|
||||
{logs.length === 0 && <div className='w-100 text-center'>empty</div>}
|
||||
@ -75,3 +88,34 @@ export default function WalletLogs ({ wallet, embedded }) {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DeleteWalletLogsObstacle ({ wallet, onClose }) {
|
||||
const toaster = useToast()
|
||||
const { deleteLogs } = useWalletLogger(wallet)
|
||||
|
||||
const prompt = `Do you really want to delete all ${wallet ? '' : 'wallet'} logs ${wallet ? 'of this wallet' : ''}?`
|
||||
return (
|
||||
<div className='text-center'>
|
||||
{prompt}
|
||||
<div className='d-flex justify-center align-items-center mt-3 mx-auto'>
|
||||
<span style={{ cursor: 'pointer' }} className='d-flex ms-auto text-muted fw-bold nav-link mx-3' onClick={onClose}>cancel</span>
|
||||
<Button
|
||||
className='d-flex me-auto mx-3' variant='danger'
|
||||
onClick={
|
||||
async () => {
|
||||
try {
|
||||
await deleteLogs()
|
||||
onClose()
|
||||
toaster.success('deleted wallet logs')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toaster.danger('failed to delete wallet logs')
|
||||
}
|
||||
}
|
||||
}
|
||||
>delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
|
||||
import { useWalletLogger } from '../logger'
|
||||
import { Status } from '.'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
// Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts
|
||||
|
||||
@ -67,7 +68,7 @@ export function LNbitsProvider ({ children }) {
|
||||
const [url, setUrl] = useState('')
|
||||
const [adminKey, setAdminKey] = useState('')
|
||||
const [status, setStatus] = useState()
|
||||
const logger = useWalletLogger('lnbits')
|
||||
const { logger } = useWalletLogger(Wallet.LNbits)
|
||||
|
||||
const name = 'LNbits'
|
||||
const storageKey = 'webln:provider:lnbits'
|
||||
|
@ -7,6 +7,7 @@ import useModal from '../modal'
|
||||
import { Form, PasswordInput, SubmitButton } from '../form'
|
||||
import CancelButton from '../cancel-button'
|
||||
import { Mutex } from 'async-mutex'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
const LNCContext = createContext()
|
||||
const mutex = new Mutex()
|
||||
@ -32,8 +33,7 @@ function validateNarrowPerms (lnc) {
|
||||
}
|
||||
|
||||
export function LNCProvider ({ children }) {
|
||||
const name = 'lnc'
|
||||
const logger = useWalletLogger(name)
|
||||
const logger = useWalletLogger(Wallet.LNC)
|
||||
const [config, setConfig] = useState({})
|
||||
const [lnc, setLNC] = useState()
|
||||
const [status, setStatus] = useState()
|
||||
@ -188,7 +188,7 @@ export function LNCProvider ({ children }) {
|
||||
}, [setStatus, setConfig, logger])
|
||||
|
||||
return (
|
||||
<LNCContext.Provider value={{ name, status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }}>
|
||||
<LNCContext.Provider value={{ name: 'lnc', status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }}>
|
||||
{children}
|
||||
{modal}
|
||||
</LNCContext.Provider>
|
||||
|
@ -6,6 +6,7 @@ import { parseNwcUrl } from '@/lib/url'
|
||||
import { useWalletLogger } from '../logger'
|
||||
import { Status } from '.'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
const NWCContext = createContext()
|
||||
|
||||
@ -15,7 +16,7 @@ export function NWCProvider ({ children }) {
|
||||
const [relayUrl, setRelayUrl] = useState()
|
||||
const [secret, setSecret] = useState()
|
||||
const [status, setStatus] = useState()
|
||||
const logger = useWalletLogger('nwc')
|
||||
const { logger } = useWalletLogger(Wallet.NWC)
|
||||
|
||||
const name = 'NWC'
|
||||
const storageKey = 'webln:provider:nwc'
|
||||
|
@ -52,6 +52,9 @@ function getClient (uri) {
|
||||
}
|
||||
}
|
||||
},
|
||||
Fact: {
|
||||
keyFields: ['id', 'type']
|
||||
},
|
||||
Query: {
|
||||
fields: {
|
||||
sub: {
|
||||
|
@ -131,3 +131,20 @@ export const FAST_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_FAST_POLL_INTER
|
||||
export const NORMAL_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_NORMAL_POLL_INTERVAL)
|
||||
export const LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_LONG_POLL_INTERVAL)
|
||||
export const EXTRA_LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL)
|
||||
|
||||
// attached wallets
|
||||
export const Wallet = {
|
||||
LND: { logTag: 'lnd', server: true, type: 'LND', field: 'walletLND' },
|
||||
CLN: { logTag: 'cln', server: true, type: 'CLN', field: 'walletCLN' },
|
||||
LnAddr: { logTag: 'lnAddr', server: true, type: 'LIGHTNING_ADDRESS', field: 'walletLightningAddress' },
|
||||
NWC: { logTag: 'nwc', server: false },
|
||||
LNbits: { logTag: 'lnbits', server: false },
|
||||
LNC: { logTag: 'lnc', server: false }
|
||||
}
|
||||
|
||||
export const getWalletBy = (key, value) => {
|
||||
for (const w of Object.values(Wallet)) {
|
||||
if (w[key] === value) return w
|
||||
}
|
||||
throw new Error(`wallet not found: ${key}=${value}`)
|
||||
}
|
||||
|
@ -604,7 +604,7 @@ export const lnbitsSchema = object({
|
||||
url: process.env.NODE_ENV === 'development'
|
||||
? string()
|
||||
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
|
||||
.required('required').trim().https()
|
||||
.required('required').trim()
|
||||
: string().url().required('required').trim().https(),
|
||||
adminKey: string().length(32)
|
||||
})
|
||||
|
@ -19,12 +19,14 @@ export function middleware (request) {
|
||||
resp = referrerMiddleware(request)
|
||||
}
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
|
||||
// we want to load media from other localhost ports during development
|
||||
const devSrc = process.env.NODE_ENV === 'development' ? ' localhost:*' : ''
|
||||
const devSrc = isDev ? ' localhost:* http: ws:' : ''
|
||||
// unsafe-eval is required during development due to react-refresh.js
|
||||
// see https://github.com/vercel/next.js/issues/14221
|
||||
const devScriptSrc = process.env.NODE_ENV === 'development' ? " 'unsafe-eval'" : ''
|
||||
const devScriptSrc = isDev ? " 'unsafe-eval'" : ''
|
||||
|
||||
const cspHeader = [
|
||||
// if something is not explicitly allowed, we don't allow it.
|
||||
@ -47,7 +49,7 @@ export function middleware (request) {
|
||||
// blocks injection of <base> tags
|
||||
"base-uri 'none'",
|
||||
// tell user agents to replace HTTP with HTTPS
|
||||
'upgrade-insecure-requests',
|
||||
isDev ? '' : 'upgrade-insecure-requests',
|
||||
// prevents any domain from framing the content (defense against clickjacking attacks)
|
||||
"frame-ancestors 'none'"
|
||||
].join('; ')
|
||||
|
@ -12,8 +12,9 @@ import { REMOVE_WALLET, UPSERT_WALLET_CLN, WALLET_BY_TYPE } from '@/fragments/wa
|
||||
import WalletLogs from '@/components/wallet-logs'
|
||||
import Info from '@/components/info'
|
||||
import Text from '@/components/text'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
const variables = { type: 'CLN' }
|
||||
const variables = { type: Wallet.CLN.type }
|
||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
|
||||
|
||||
export default function CLN ({ ssrData }) {
|
||||
@ -118,7 +119,7 @@ export default function CLN ({ ssrData }) {
|
||||
/>
|
||||
</Form>
|
||||
<div className='mt-3 w-100'>
|
||||
<WalletLogs wallet='cln' embedded />
|
||||
<WalletLogs wallet={Wallet.CLN} embedded />
|
||||
</div>
|
||||
</CenterLayout>
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ import { useQuery } from '@apollo/client'
|
||||
import PageLoading from '@/components/page-loading'
|
||||
import { LNCCard } from './lnc'
|
||||
import Link from 'next/link'
|
||||
import { Wallet as W } from '@/lib/constants'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true })
|
||||
|
||||
@ -20,9 +21,9 @@ export default function Wallet ({ ssrData }) {
|
||||
|
||||
if (!data && !ssrData) return <PageLoading />
|
||||
const { wallets } = data || ssrData
|
||||
const lnd = wallets.find(w => w.type === 'LND')
|
||||
const lnaddr = wallets.find(w => w.type === 'LIGHTNING_ADDRESS')
|
||||
const cln = wallets.find(w => w.type === 'CLN')
|
||||
const lnd = wallets.find(w => w.type === W.LND.type)
|
||||
const lnaddr = wallets.find(w => w.type === W.LnAddr.type)
|
||||
const cln = wallets.find(w => w.type === W.CLN.type)
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
@ -10,8 +10,9 @@ import { useRouter } from 'next/router'
|
||||
import { AutowithdrawSettings, autowithdrawInitial } from '@/components/autowithdraw-shared'
|
||||
import { REMOVE_WALLET, UPSERT_WALLET_LNADDR, WALLET_BY_TYPE } from '@/fragments/wallet'
|
||||
import WalletLogs from '@/components/wallet-logs'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
const variables = { type: 'LIGHTNING_ADDRESS' }
|
||||
const variables = { type: Wallet.LnAddr.type }
|
||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
|
||||
|
||||
export default function LightningAddress ({ ssrData }) {
|
||||
@ -87,7 +88,7 @@ export default function LightningAddress ({ ssrData }) {
|
||||
/>
|
||||
</Form>
|
||||
<div className='mt-3 w-100'>
|
||||
<WalletLogs wallet='lnAddr' embedded />
|
||||
<WalletLogs wallet={Wallet.LnAddr} embedded />
|
||||
</div>
|
||||
</CenterLayout>
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ import { useLNbits } from '@/components/webln/lnbits'
|
||||
import { WalletSecurityBanner } from '@/components/banners'
|
||||
import { useWebLNConfigurator } from '@/components/webln'
|
||||
import WalletLogs from '@/components/wallet-logs'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||
|
||||
@ -79,7 +80,7 @@ export default function LNbits () {
|
||||
/>
|
||||
</Form>
|
||||
<div className='mt-3 w-100'>
|
||||
<WalletLogs wallet='lnbits' embedded />
|
||||
<WalletLogs wallet={Wallet.LNbits} embedded />
|
||||
</div>
|
||||
</CenterLayout>
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ import { XXX_DEFAULT_PASSWORD, useLNC } from '@/components/webln/lnc'
|
||||
import { lncSchema } from '@/lib/validate'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||
|
||||
@ -62,7 +63,7 @@ export default function LNC () {
|
||||
<div className='d-flex align-items-center'>pairing phrase
|
||||
<Info label='help'>
|
||||
<Text>
|
||||
{'We only need permissions for the uri `/lnrpc.Lightning/SendPaymentSync`\n\nCreate a budgeted account with narrow permissions:\n\n```$ litcli accounts create --balance <budget>```\n\n```$ litcli sessions add --type custom --account_id <account_id> --uri /lnrpc.Lightning/SendPaymentSync```'}
|
||||
{'We only need permissions for the uri `/lnrpc.Lightning/SendPaymentSync`\n\nCreate a budgeted account with narrow permissions:\n\n```$ litcli accounts create --balance <budget>```\n\n```$ litcli sessions add --type custom --label <your label> --account_id <account_id> --uri /lnrpc.Lightning/SendPaymentSync```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.'}
|
||||
</Text>
|
||||
</Info>
|
||||
</div>
|
||||
@ -102,7 +103,7 @@ export default function LNC () {
|
||||
/>
|
||||
</Form>
|
||||
<div className='mt-3 w-100'>
|
||||
<WalletLogs wallet='lnc' embedded />
|
||||
<WalletLogs wallet={Wallet.LNC} embedded />
|
||||
</div>
|
||||
</CenterLayout>
|
||||
)
|
||||
|
@ -12,8 +12,9 @@ import { REMOVE_WALLET, UPSERT_WALLET_LND, WALLET_BY_TYPE } from '@/fragments/wa
|
||||
import Info from '@/components/info'
|
||||
import Text from '@/components/text'
|
||||
import WalletLogs from '@/components/wallet-logs'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
const variables = { type: 'LND' }
|
||||
const variables = { type: Wallet.LND.type }
|
||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
|
||||
|
||||
export default function LND ({ ssrData }) {
|
||||
@ -118,7 +119,7 @@ export default function LND ({ ssrData }) {
|
||||
/>
|
||||
</Form>
|
||||
<div className='mt-3 w-100'>
|
||||
<WalletLogs wallet='lnd' embedded />
|
||||
<WalletLogs wallet={Wallet.LND} embedded />
|
||||
</div>
|
||||
</CenterLayout>
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ import { useNWC } from '@/components/webln/nwc'
|
||||
import { WalletSecurityBanner } from '@/components/banners'
|
||||
import { useWebLNConfigurator } from '@/components/webln'
|
||||
import WalletLogs from '@/components/wallet-logs'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||
|
||||
@ -72,7 +73,7 @@ export default function NWC () {
|
||||
/>
|
||||
</Form>
|
||||
<div className='mt-3 w-100'>
|
||||
<WalletLogs wallet='nwc' embedded />
|
||||
<WalletLogs wallet={Wallet.NWC} embedded />
|
||||
</div>
|
||||
</CenterLayout>
|
||||
)
|
||||
|
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Changed the type of `wallet` on the `WalletLog` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||
|
||||
*/
|
||||
|
||||
UPDATE "WalletLog"
|
||||
SET wallet = CASE
|
||||
WHEN wallet = 'walletLND' THEN 'LND'
|
||||
WHEN wallet = 'walletCLN' THEN 'CLN'
|
||||
WHEN wallet = 'walletLightningAddress' THEN 'LIGHTNING_ADDRESS'
|
||||
ELSE wallet
|
||||
END;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "WalletLog" ALTER COLUMN "wallet" TYPE "WalletType" USING "wallet"::"WalletType";
|
@ -166,7 +166,7 @@ model WalletLog {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
wallet String
|
||||
wallet WalletType
|
||||
level LogLevel
|
||||
message String
|
||||
|
||||
|
10
sndev
10
sndev
@ -340,6 +340,15 @@ sndev__help_stacker_clncli() {
|
||||
docker__stacker_cln help
|
||||
}
|
||||
|
||||
sndev__stacker_litcli() {
|
||||
shift
|
||||
docker__exec -t litd litcli -n regtest --rpcserver localhost:8444 "$@"
|
||||
}
|
||||
|
||||
sndev__help_stacker_litcli() {
|
||||
docker__exec -t litd litcli -h
|
||||
}
|
||||
|
||||
__sndev__pr_track() {
|
||||
json=$(curl -fsSH "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/stackernews/stacker.news/pulls/$1")
|
||||
case $(git config --get remote.origin.url) in
|
||||
@ -493,6 +502,7 @@ COMMANDS
|
||||
sn_lndcli lncli passthrough on sn_lnd
|
||||
stacker_lndcli lncli passthrough on stacker_lnd
|
||||
stacker_clncli lightning-cli passthrough on stacker_cln
|
||||
stacker_litcli litcli passthrough on litd
|
||||
"
|
||||
echo "$help"
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { msatsToSats, satsToMsats } from '@/lib/format'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import { createWithdrawal, sendToLnAddr, addWalletLog } from '@/api/resolvers/wallet'
|
||||
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
|
||||
import { Wallet } from '@/lib/constants'
|
||||
|
||||
export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
||||
const user = await models.user.findUnique({ where: { id } })
|
||||
@ -46,15 +47,15 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
||||
|
||||
for (const wallet of wallets) {
|
||||
try {
|
||||
if (wallet.type === 'LND') {
|
||||
if (wallet.type === Wallet.LND.type) {
|
||||
await autowithdrawLND(
|
||||
{ amount, maxFee },
|
||||
{ models, me: user, lnd })
|
||||
} else if (wallet.type === 'CLN') {
|
||||
} else if (wallet.type === Wallet.CLN.type) {
|
||||
await autowithdrawCLN(
|
||||
{ amount, maxFee },
|
||||
{ models, me: user, lnd })
|
||||
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
|
||||
} else if (wallet.type === Wallet.LnAddr.type) {
|
||||
await autowithdrawLNAddr(
|
||||
{ amount, maxFee },
|
||||
{ models, me: user, lnd })
|
||||
@ -66,7 +67,7 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
||||
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
|
||||
const details = error[2]?.err?.details || error.message || error.toString?.()
|
||||
await addWalletLog({
|
||||
wallet: wallet.type,
|
||||
wallet,
|
||||
level: 'ERROR',
|
||||
message: 'autowithdrawal failed: ' + details
|
||||
}, { me: user, models })
|
||||
@ -86,7 +87,7 @@ async function autowithdrawLNAddr (
|
||||
const wallet = await models.wallet.findFirst({
|
||||
where: {
|
||||
userId: me.id,
|
||||
type: 'LIGHTNING_ADDRESS'
|
||||
type: Wallet.LnAddr.type
|
||||
},
|
||||
include: {
|
||||
walletLightningAddress: true
|
||||
@ -109,7 +110,7 @@ async function autowithdrawLND ({ amount, maxFee }, { me, models, lnd }) {
|
||||
const wallet = await models.wallet.findFirst({
|
||||
where: {
|
||||
userId: me.id,
|
||||
type: 'LND'
|
||||
type: Wallet.LND.type
|
||||
},
|
||||
include: {
|
||||
walletLND: true
|
||||
@ -145,7 +146,7 @@ async function autowithdrawCLN ({ amount, maxFee }, { me, models, lnd }) {
|
||||
const wallet = await models.wallet.findFirst({
|
||||
where: {
|
||||
userId: me.id,
|
||||
type: 'CLN'
|
||||
type: Wallet.CLN.type
|
||||
},
|
||||
include: {
|
||||
walletCLN: true
|
||||
|
@ -13,8 +13,10 @@ export async function earn ({ name }) {
|
||||
try {
|
||||
// compute how much sn earned yesterday
|
||||
const [{ sum: sumDecimal }] = await models.$queryRaw`
|
||||
SELECT total as sum
|
||||
FROM rewards(now() AT TIME ZONE 'America/Chicago' - interval '1 day', now() AT TIME ZONE 'America/Chicago' - interval '1 day', '1 day'::INTERVAL, 'day')`
|
||||
SELECT sum(total) as sum
|
||||
FROM rewards(
|
||||
date_trunc('day', now() AT TIME ZONE 'America/Chicago' - interval '1 day'),
|
||||
date_trunc('day', now() AT TIME ZONE 'America/Chicago' - interval '1 day'), '1 day'::INTERVAL, 'day')`
|
||||
|
||||
// XXX primsa will return a Decimal (https://mikemcl.github.io/decimal.js)
|
||||
// because sum of a BIGINT returns a NUMERIC type (https://www.postgresql.org/docs/13/functions-aggregate.html)
|
||||
@ -52,7 +54,7 @@ export async function earn ({ name }) {
|
||||
// get earners { userId, id, type, rank, proportion }
|
||||
const earners = await models.$queryRaw`
|
||||
SELECT id AS "userId", proportion, ROW_NUMBER() OVER (ORDER BY proportion DESC) as rank
|
||||
FROM user_values_days(now() AT TIME ZONE 'America/Chicago' - interval '1 day', now() AT TIME ZONE 'America/Chicago' - interval '1 day', '1 day'::INTERVAL, 'day')
|
||||
FROM user_values(date_trunc('day', now() AT TIME ZONE 'America/Chicago' - interval '1 day'), date_trunc('day', now() AT TIME ZONE 'America/Chicago' - interval '1 day'), '1 day'::INTERVAL, 'day')
|
||||
WHERE NOT (id = ANY (${SN_NO_REWARDS_IDS}))
|
||||
ORDER BY proportion DESC
|
||||
LIMIT 100`
|
||||
|
Loading…
x
Reference in New Issue
Block a user