Compare commits

...

4 Commits

Author SHA1 Message Date
SatsAllDay
dc0370ba17
random zap amounts (#1263)
* add random zapping support

adds an option to enable random zap amounts per stacker

configurable in settings, you can enable this feature and provide
an upper and lower range of your random zap amount

* rename github eslint check to lint

this has been bothering me since we aren't using eslint for linting

* fixup! add random zapping support

* fixup! rename github eslint check to lint

* fixup! fixup! add random zapping support

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-07-26 22:37:03 -05:00
keyan
4964e2c7d1 order all zap recipients 2024-07-26 12:25:48 -05:00
keyan
3d1f7834ca fix potential zap deadlock 2024-07-25 20:46:17 -05:00
ekzyis
628a0466fd
Use validation for WebLN wallet (#1277)
* remove available prop
* 'enabled' checkbox is now always enabled but uses validation
* CheckboxGroup was missing to show error message
2024-07-24 10:08:09 -05:00
16 changed files with 245 additions and 87 deletions

View File

@ -1,8 +1,8 @@
name: Eslint Check name: Lint Check
on: [pull_request] on: [pull_request]
jobs: jobs:
eslint-run: lint-run:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

View File

@ -202,3 +202,68 @@ From the [postgres docs](https://www.postgresql.org/docs/current/transaction-iso
From the [postgres source docs](https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/executor/README#l350): From the [postgres source docs](https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/executor/README#l350):
> It is also possible that there are relations in the query that are not to be locked (they are neither the UPDATE/DELETE/MERGE target nor specified to be locked in SELECT FOR UPDATE/SHARE). When re-running the test query ***we want to use the same rows*** from these relations that were joined to the locked rows. > It is also possible that there are relations in the query that are not to be locked (they are neither the UPDATE/DELETE/MERGE target nor specified to be locked in SELECT FOR UPDATE/SHARE). When re-running the test query ***we want to use the same rows*** from these relations that were joined to the locked rows.
## `IMPORTANT: deadlocks`
Deadlocks can occur when two transactions are waiting for each other to release locks. This can happen when two transactions lock rows in different orders whether explicit or implicit.
### Incorrect
```sql
-- transaction 1
BEGIN;
UPDATE users set msats = msats + 1 WHERE id = 1;
-- transaction 2
BEGIN;
UPDATE users set msats = msats + 1 WHERE id = 2;
-- transaction 1 (blocks here until transaction 2 commits)
UPDATE users set msats = msats + 1 WHERE id = 2;
-- transaction 2 (blocks here until transaction 1 commits)
UPDATE users set msats = msats + 1 WHERE id = 1;
-- deadlock occurs because neither transaction can proceed to here
```
If both transactions lock the rows in the same order, the deadlock is avoided.
Most often this occurs when selecting multiple rows for update in different orders. Recently, we had a deadlock when spliting zaps to multiple users. The solution was to select the rows for update in the same order.
### Incorrect
```sql
WITH forwardees AS (
SELECT "userId", (($1::BIGINT * pct) / 100)::BIGINT AS msats
FROM "ItemForward"
WHERE "itemId" = $2::INTEGER
),
UPDATE users
SET
msats = users.msats + forwardees.msats,
"stackedMsats" = users."stackedMsats" + forwardees.msats
FROM forwardees
WHERE users.id = forwardees."userId";
```
If forwardees are selected in a different order in two concurrent transactions, e.g. (1,2) in tx 1 and (2,1) in tx 2, a deadlock can occur. To avoid this, always select rows for update in the same order.
### Correct
We fixed the deadlock by selecting the forwardees in the same order in these transactions.
```sql
WITH forwardees AS (
SELECT "userId", (($1::BIGINT * pct) / 100)::BIGINT AS msats
FROM "ItemForward"
WHERE "itemId" = $2::INTEGER
ORDER BY "userId" ASC
),
UPDATE users
SET
msats = users.msats + forwardees.msats,
"stackedMsats" = users."stackedMsats" + forwardees.msats
FROM forwardees
WHERE users.id = forwardees."userId";
```
### More resources
- https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS

View File

@ -77,19 +77,19 @@ export async function onPaid ({ invoice, actIds }, { models, tx }) {
), total_forwarded AS ( ), total_forwarded AS (
SELECT COALESCE(SUM(msats), 0) as msats SELECT COALESCE(SUM(msats), 0) as msats
FROM forwardees FROM forwardees
), forward AS ( ), recipients AS (
UPDATE users SELECT "userId", msats FROM forwardees
SET UNION
msats = users.msats + forwardees.msats, SELECT ${itemAct.item.userId}::INTEGER as "userId",
"stackedMsats" = users."stackedMsats" + forwardees.msats ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT as msats
FROM forwardees ORDER BY "userId" ASC -- order to prevent deadlocks
WHERE users.id = forwardees."userId"
) )
UPDATE users UPDATE users
SET SET
msats = msats + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT, msats = users.msats + recipients.msats,
"stackedMsats" = "stackedMsats" + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT "stackedMsats" = users."stackedMsats" + recipients.msats
WHERE id = ${itemAct.item.userId}::INTEGER` FROM recipients
WHERE users.id = recipients."userId"`
// perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt // perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt
// NOTE: for the rows that might be updated by a concurrent zap, we use UPDATE for implicit locking // NOTE: for the rows that might be updated by a concurrent zap, we use UPDATE for implicit locking

View File

@ -1004,6 +1004,12 @@ export default {
}) })
return relays?.map(r => r.nostrRelayAddr) return relays?.map(r => r.nostrRelayAddr)
},
tipRandom: async (user, args, { me }) => {
if (!me || me.id !== user.id) {
return false
}
return !!user.tipRandomMin && !!user.tipRandomMax
} }
}, },

View File

@ -98,6 +98,8 @@ export default gql`
noteItemMentions: Boolean! noteItemMentions: Boolean!
nsfwMode: Boolean! nsfwMode: Boolean!
tipDefault: Int! tipDefault: Int!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean! turboTipping: Boolean!
zapUndos: Int zapUndos: Int
wildWestMode: Boolean! wildWestMode: Boolean!
@ -165,6 +167,9 @@ export default gql`
noteItemMentions: Boolean! noteItemMentions: Boolean!
nsfwMode: Boolean! nsfwMode: Boolean!
tipDefault: Int! tipDefault: Int!
tipRandom: Boolean!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean! turboTipping: Boolean!
zapUndos: Int zapUndos: Int
wildWestMode: Boolean! wildWestMode: Boolean!

View File

@ -7,7 +7,7 @@ import UpBolt from '@/svgs/bolt.svg'
import { amountSchema } from '@/lib/validate' import { amountSchema } from '@/lib/validate'
import { useToast } from './toast' import { useToast } from './toast'
import { useLightning } from './lightning' import { useLightning } from './lightning'
import { nextTip } from './upvote' import { nextTip, defaultTipIncludingRandom } from './upvote'
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants' import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
import { usePaidMutation } from './use-paid-mutation' import { usePaidMutation } from './use-paid-mutation'
import { ACT_MUTATION } from '@/fragments/paidAction' import { ACT_MUTATION } from '@/fragments/paidAction'
@ -101,7 +101,7 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
return ( return (
<Form <Form
initial={{ initial={{
amount: me?.privates?.tipDefault || defaultTips[0], amount: defaultTipIncludingRandom(me?.privates) || defaultTips[0],
default: false default: false
}} }}
schema={amountSchema} schema={amountSchema}

View File

@ -26,7 +26,7 @@ const UpvotePopover = ({ target, show, handleClose }) => {
<button type='button' className='btn-close' onClick={handleClose}><span className='visually-hidden-focusable'>Close alert</span></button> <button type='button' className='btn-close' onClick={handleClose}><span className='visually-hidden-focusable'>Close alert</span></button>
</Popover.Header> </Popover.Header>
<Popover.Body> <Popover.Body>
<div className='mb-2'>Press the bolt again to zap {me?.privates?.tipDefault || 1} more sat{me?.privates?.tipDefault > 1 ? 's' : ''}.</div> <div className='mb-2'>Press the bolt again to zap {me?.privates?.tipRandom ? 'a random amount of' : `${me?.privates?.tipDefault || 1} more`} sat{me?.privates?.tipDefault > 1 ? 's' : ''}.</div>
<div>Repeatedly press the bolt to zap more sats.</div> <div>Repeatedly press the bolt to zap more sats.</div>
</Popover.Body> </Popover.Body>
</Popover> </Popover>
@ -67,11 +67,20 @@ export function DropdownItemUpVote ({ item }) {
) )
} }
export const nextTip = (meSats, { tipDefault, turboTipping }) => { export const defaultTipIncludingRandom = ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax }) =>
// what should our next tip be? tipRandom
if (!turboTipping) return (tipDefault || 1) ? Math.floor((Math.random() * (tipRandomMax - tipRandomMin)) + tipRandomMin)
: (tipDefault || 1)
let sats = tipDefault || 1 export const nextTip = (meSats, { tipDefault, turboTipping, tipRandom, tipRandomMin, tipRandomMax }) => {
// what should our next tip be?
const calculatedDefault = defaultTipIncludingRandom({ tipDefault, tipRandom, tipRandomMin, tipRandomMax })
if (!turboTipping) {
return calculatedDefault
}
let sats = calculatedDefault
if (turboTipping) { if (turboTipping) {
while (meSats >= sats) { while (meSats >= sats) {
sats *= 10 sats *= 10
@ -138,11 +147,17 @@ export default function UpVote ({ item, className }) {
// what should our next tip be? // what should our next tip be?
const sats = nextTip(meSats, { ...me?.privates }) const sats = nextTip(meSats, { ...me?.privates })
let overlayTextContent
if (me) {
overlayTextContent = me.privates?.tipRandom ? 'random' : numWithUnits(sats, { abbreviate: false })
} else {
overlayTextContent = 'zap it'
}
return [ return [
meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it', meSats, overlayTextContent,
getColor(meSats), getColor(meSats + sats)] getColor(meSats), getColor(meSats + sats)]
}, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault]) }, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault, me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax])
const handleModalClosed = () => { const handleModalClosed = () => {
setHover(false) setHover(false)

View File

@ -41,6 +41,9 @@ export const ME = gql`
noteItemMentions noteItemMentions
sats sats
tipDefault tipDefault
tipRandom
tipRandomMin
tipRandomMax
tipPopover tipPopover
turboTipping turboTipping
zapUndos zapUndos
@ -66,6 +69,9 @@ export const SETTINGS_FIELDS = gql`
fragment SettingsFields on User { fragment SettingsFields on User {
privates { privates {
tipDefault tipDefault
tipRandom
tipRandomMin
tipRandomMax
turboTipping turboTipping
zapUndos zapUndos
fiatCurrency fiatCurrency

View File

@ -540,8 +540,24 @@ export const actSchema = object({
act: string().required('required').oneOf(['TIP', 'DONT_LIKE_THIS']) act: string().required('required').oneOf(['TIP', 'DONT_LIKE_THIS'])
}) })
export const settingsSchema = object({ export const settingsSchema = object().shape({
tipDefault: intValidator.required('required').positive('must be positive'), tipDefault: intValidator.required('required').positive('must be positive'),
tipRandomMin: intValidator.nullable().positive('must be positive')
.when(['tipRandomMax'], ([max], schema) => {
let res = schema
if (max) {
res = schema.required('minimum and maximum must either both be omitted or specified').nonNullable()
}
return res.lessThan(max, 'must be less than maximum')
}),
tipRandomMax: intValidator.nullable().positive('must be positive')
.when(['tipRandomMin'], ([min], schema) => {
let res = schema
if (min) {
res = schema.required('minimum and maximum must either both be omitted or specified').nonNullable()
}
return res.moreThan(min, 'must be more than minimum')
}),
fiatCurrency: string().required('required').oneOf(SUPPORTED_CURRENCIES), fiatCurrency: string().required('required').oneOf(SUPPORTED_CURRENCIES),
withdrawMaxFeeDefault: intValidator.required('required').positive('must be positive'), withdrawMaxFeeDefault: intValidator.required('required').positive('must be positive'),
nostrPubkey: string().nullable() nostrPubkey: string().nullable()
@ -561,7 +577,8 @@ export const settingsSchema = object({
noReferralLinks: boolean(), noReferralLinks: boolean(),
hideIsContributor: boolean(), hideIsContributor: boolean(),
zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0') zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0')
}) // exclude from cyclic analysis. see https://github.com/jquense/yup/issues/720
}, [['tipRandomMax', 'tipRandomMin']])
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again' const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
export const lastAuthRemovalSchema = object({ export const lastAuthRemovalSchema = object({

View File

@ -112,6 +112,9 @@ export default function Settings ({ ssrData }) {
<Form <Form
initial={{ initial={{
tipDefault: settings?.tipDefault || 21, tipDefault: settings?.tipDefault || 21,
tipRandom: settings?.tipRandom,
tipRandomMin: settings?.tipRandomMin || 1,
tipRandomMax: settings?.tipRandomMax || settings?.tipDefault || 21,
turboTipping: settings?.turboTipping, turboTipping: settings?.turboTipping,
zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100), zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100),
zapUndosEnabled: settings?.zapUndos !== null, zapUndosEnabled: settings?.zapUndos !== null,
@ -149,7 +152,7 @@ export default function Settings ({ ssrData }) {
noReferralLinks: settings?.noReferralLinks noReferralLinks: settings?.noReferralLinks
}} }}
schema={settingsSchema} schema={settingsSchema}
onSubmit={async ({ tipDefault, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => { onSubmit={async ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => {
if (nostrPubkey.length === 0) { if (nostrPubkey.length === 0) {
nostrPubkey = null nostrPubkey = null
} else { } else {
@ -166,6 +169,8 @@ export default function Settings ({ ssrData }) {
variables: { variables: {
settings: { settings: {
tipDefault: Number(tipDefault), tipDefault: Number(tipDefault),
tipRandomMin: tipRandom ? Number(tipRandomMin) : null,
tipRandomMax: tipRandom ? Number(tipRandomMax) : null,
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault), withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
zapUndos: zapUndosEnabled ? Number(zapUndos) : null, zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
nostrPubkey, nostrPubkey,
@ -190,7 +195,7 @@ export default function Settings ({ ssrData }) {
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
hint={<small className='text-muted'>note: you can also press and hold the lightning bolt to zap custom amounts</small>} hint={<small className='text-muted'>note: you can also press and hold the lightning bolt to zap custom amounts</small>}
/> />
<div className='mb-2'> <div className='pb-4'>
<AccordianItem <AccordianItem
show={settings?.turboTipping} show={settings?.turboTipping}
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>} header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>}
@ -224,6 +229,7 @@ export default function Settings ({ ssrData }) {
groupClassName='mb-0' groupClassName='mb-0'
/> />
<ZapUndosField /> <ZapUndosField />
<TipRandomField />
</> </>
} }
/> />
@ -995,11 +1001,7 @@ function ApiKeyDeleteObstacle ({ onClose }) {
const ZapUndosField = () => { const ZapUndosField = () => {
const [checkboxField] = useField({ name: 'zapUndosEnabled' }) const [checkboxField] = useField({ name: 'zapUndosEnabled' })
return ( return (
<div className='d-flex flex-row align-items-center'> <>
<Input
name='zapUndos'
disabled={!checkboxField.value}
label={
<Checkbox <Checkbox
name='zapUndosEnabled' name='zapUndosEnabled'
groupClassName='mb-0' groupClassName='mb-0'
@ -1016,10 +1018,62 @@ const ZapUndosField = () => {
</div> </div>
} }
/> />
} {checkboxField.value &&
<Input
name='zapUndos'
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
hint={<small className='text-muted'>threshold at which undo button is shown</small>} hint={<small className='text-muted'>threshold at which undo button is shown</small>}
/> />}
</div> </>
)
}
const TipRandomField = () => {
const [tipRandomField] = useField({ name: 'tipRandom' })
const [tipRandomMinField] = useField({ name: 'tipRandomMin' })
const [tipRandomMaxField] = useField({ name: 'tipRandomMax' })
return (
<>
<Checkbox
name='tipRandom'
groupClassName='mb-0'
label={
<div className='d-flex align-items-center'>
Enable random zap values
<Info>
<ul className='fw-bold'>
<li>Set a minimum and maximum zap amount</li>
<li>Each time you zap something, a random amount of sats between your minimum and maximum will be zapped</li>
<li>If this setting is enabled, it will ignore your default zap amount</li>
</ul>
</Info>
</div>
}
/>
{tipRandomField.value &&
<>
<Input
type='number'
label='minimum random zap'
name='tipRandomMin'
disabled={!tipRandomField.value}
groupClassName='mb-1'
required
autoFocus
max={tipRandomMaxField.value ? tipRandomMaxField.value - 1 : undefined}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<Input
type='number'
label='maximum random zap'
name='tipRandomMax'
disabled={!tipRandomField.value}
required
autoFocus
min={tipRandomMinField.value ? tipRandomMinField.value + 1 : undefined}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
</>}
</>
) )
} }

View File

@ -1,5 +1,5 @@
import { getGetServerSideProps } from '@/api/ssrApollo' import { getGetServerSideProps } from '@/api/ssrApollo'
import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form' import { Form, ClientInput, ClientCheckbox, PasswordInput, CheckboxGroup } from '@/components/form'
import { CenterLayout } from '@/components/layout' import { CenterLayout } from '@/components/layout'
import { WalletSecurityBanner } from '@/components/banners' import { WalletSecurityBanner } from '@/components/banners'
import { WalletLogs } from '@/components/wallet-logger' import { WalletLogs } from '@/components/wallet-logger'
@ -10,7 +10,6 @@ import Info from '@/components/info'
import Text from '@/components/text' import Text from '@/components/text'
import { AutowithdrawSettings } from '@/components/autowithdraw-shared' import { AutowithdrawSettings } from '@/components/autowithdraw-shared'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { useEffect, useState } from 'react'
const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false }) const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false })
@ -22,14 +21,6 @@ export default function WalletSettings () {
const { wallet: name } = router.query const { wallet: name } = router.query
const wallet = useWallet(name) const wallet = useWallet(name)
const [mounted, setMounted] = useState(false)
useEffect(() => {
// mounted is required since available might depend
// on values that are only available on the client (and not during SSR)
// and thus we need to render the component again on the client
setMounted(true)
}, [])
const initial = wallet.fields.reduce((acc, field) => { const initial = wallet.fields.reduce((acc, field) => {
// We still need to run over all wallet fields via reduce // We still need to run over all wallet fields via reduce
// even though we use wallet.config as the initial value // even though we use wallet.config as the initial value
@ -47,8 +38,6 @@ export default function WalletSettings () {
? { validate: wallet.fieldValidation } ? { validate: wallet.fieldValidation }
: { schema: wallet.fieldValidation } : { schema: wallet.fieldValidation }
const available = mounted && wallet.available !== undefined ? wallet.available : wallet.isConfigured
return ( return (
<CenterLayout> <CenterLayout>
<h2 className='pb-2'>{wallet.card.title}</h2> <h2 className='pb-2'>{wallet.card.title}</h2>
@ -84,12 +73,15 @@ export default function WalletSettings () {
{wallet.walletType {wallet.walletType
? <AutowithdrawSettings wallet={wallet} /> ? <AutowithdrawSettings wallet={wallet} />
: ( : (
<CheckboxGroup name='enabled'>
<ClientCheckbox <ClientCheckbox
disabled={!available} disabled={!wallet.isConfigured}
initialValue={wallet.status === Status.Enabled} initialValue={wallet.status === Status.Enabled}
label='enabled' label='enabled'
name='enabled' name='enabled'
groupClassName='mb-0'
/> />
</CheckboxGroup>
)} )}
<WalletButtonBar <WalletButtonBar
wallet={wallet} onDelete={async () => { wallet={wallet} onDelete={async () => {

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "tipRandomMax" INTEGER,
ADD COLUMN "tipRandomMin" INTEGER;

View File

@ -30,6 +30,8 @@ model User {
apiKeyHash String? @unique(map: "users.apikeyhash_unique") @db.Char(64) apiKeyHash String? @unique(map: "users.apikeyhash_unique") @db.Char(64)
apiKeyEnabled Boolean @default(false) apiKeyEnabled Boolean @default(false)
tipDefault Int @default(100) tipDefault Int @default(100)
tipRandomMin Int?
tipRandomMax Int?
bioId Int? bioId Int?
inviteId String? inviteId String?
tipPopover Boolean @default(false) tipPopover Boolean @default(false)

View File

@ -64,10 +64,6 @@ Since `name` will also be used in [wallet logs](https://stacker.news/wallet/logs
Wallet fields define what this wallet requires for configuration and thus are used to construct the forms like the one you can see at [/settings/wallets/lnbits](https://stacker.news/settings/walletslnbits). Wallet fields define what this wallet requires for configuration and thus are used to construct the forms like the one you can see at [/settings/wallets/lnbits](https://stacker.news/settings/walletslnbits).
- `available?: boolean`
This property can be used to override the default behavior of the `enabled` checkbox in the wallet configuration form. By default, it will be clickable when a wallet is configured. However, if a wallet does not have any configuration, this checkbox will always be disabled. You can set `available` to an expression that will determine when a wallet can be enabled.
- `card: WalletCard` - `card: WalletCard`
Wallet cards are the components you can see at [/settings/wallets](https://stacker.news/settings/wallets). This property customizes this card for this wallet. Wallet cards are the components you can see at [/settings/wallets](https://stacker.news/settings/wallets). This property customizes this card for this wallet.

View File

@ -137,11 +137,6 @@ function useConfig (wallet) {
...(hasServerConfig ? serverConfig : {}) ...(hasServerConfig ? serverConfig : {})
} }
if (wallet?.available !== undefined && config.enabled !== undefined) {
// wallet must be available to be enabled
config.enabled &&= wallet.available
}
const saveConfig = useCallback(async (config) => { const saveConfig = useCallback(async (config) => {
if (hasLocalConfig) setLocalConfig(config) if (hasLocalConfig) setLocalConfig(config)
if (hasServerConfig) await setServerConfig(config) if (hasServerConfig) await setServerConfig(config)
@ -265,13 +260,7 @@ export function getEnabledWallet (me) {
const priority = config?.priority const priority = config?.priority
return { ...def, config, priority } return { ...def, config, priority }
}) })
.filter(({ available, config }) => { .filter(({ config }) => config?.enabled)
if (available !== undefined && config?.enabled !== undefined) {
// wallet must be available to be enabled
config.enabled &&= available
}
return config?.enabled
})
.sort(walletPrioritySort)[0] .sort(walletPrioritySort)[0]
} }

View File

@ -1,10 +1,18 @@
import { SSR } from '@/lib/constants'
export const name = 'webln' export const name = 'webln'
export const fields = [] export const fields = []
export const available = SSR ? false : typeof window.webln !== 'undefined' export const fieldValidation = ({ enabled }) => {
if (typeof window.webln === 'undefined') {
// don't prevent disabling WebLN if no WebLN provider found
if (enabled) {
return {
enabled: 'no WebLN provider found'
}
}
}
return {}
}
export const card = { export const card = {
title: 'WebLN', title: 'WebLN',