Compare commits

..

No commits in common. "6cf16d3da7790e4d36fe42295242506e9a3cc9fd" and "371084016740fc29751e5964e17c687420c6ea57" have entirely different histories.

16 changed files with 44 additions and 66 deletions

View File

@ -104,7 +104,7 @@ COMMANDS
#### Running specific services #### Running specific services
By default all services will be run. If you want to exclude specific services from running, set `COMPOSE_PROFILES` to use one or more of `minimal|images|search|payments|wallets|email|capture`. To only run mininal services without images, search, email, wallets, or payments: By default all services will be run. If you want to exclude specific services from running, set `COMPOSE_PROFILES` to use one or more of `minimal|images|search|payments|email|capture`. To only run mininal services without images, search, or payments:
```sh ```sh
$ COMPOSE_PROFILES=minimal ./sndev start $ COMPOSE_PROFILES=minimal ./sndev start

View File

@ -11,7 +11,7 @@ export async function getCost ({ sats }) {
} }
export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, tx }) { export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, tx }) {
const feeMsats = cost / BigInt(10) // 10% fee const feeMsats = cost / BigInt(100)
const zapMsats = cost - feeMsats const zapMsats = cost - feeMsats
itemId = parseInt(itemId) itemId = parseInt(itemId)
@ -79,16 +79,12 @@ export async function onPaid ({ invoice, actIds }, { models, tx }) {
FROM forwardees FROM forwardees
), forward AS ( ), forward AS (
UPDATE users UPDATE users
SET SET msats = users.msats + forwardees.msats
msats = users.msats + forwardees.msats,
"stackedMsats" = users."stackedMsats" + forwardees.msats
FROM forwardees FROM forwardees
WHERE users.id = forwardees."userId" WHERE users.id = forwardees."userId"
) )
UPDATE users UPDATE users
SET SET msats = msats + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT
msats = msats + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT,
"stackedMsats" = "stackedMsats" + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT
WHERE id = ${itemAct.item.userId}::INTEGER` WHERE id = ${itemAct.item.userId}::INTEGER`
// perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt // perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt

View File

@ -8,6 +8,7 @@ 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 } from './upvote'
import { InvoiceCanceledError } from './payment'
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'
@ -71,7 +72,7 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
} }
} }
} }
const { error } = await act({ await act({
variables: { variables: {
id: item.id, id: item.id,
sats: Number(amount), sats: Number(amount),
@ -94,7 +95,6 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
if (!me) setItemMeAnonSats({ id: item.id, amount }) if (!me) setItemMeAnonSats({ id: item.id, amount })
} }
}) })
if (error) throw error
addCustomTip(Number(amount)) addCustomTip(Number(amount))
}, [me, act, down, item.id, onClose, abortSignal, strike]) }, [me, act, down, item.id, onClose, abortSignal, strike])
@ -219,15 +219,15 @@ export function useZap () {
try { try {
await abortSignal.pause({ me, amount: sats }) await abortSignal.pause({ me, amount: sats })
strike() strike()
const { error } = await act({ variables, optimisticResponse }) await act({ variables, optimisticResponse })
if (error) throw error
} catch (error) { } catch (error) {
if (error instanceof ActCanceledError) { if (error instanceof InvoiceCanceledError || error instanceof ActCanceledError) {
return return
} }
const reason = error?.message || error?.toString?.() const reason = error?.message || error?.toString?.()
toaster.danger(reason)
toaster.danger('zap failed: ' + reason)
} }
}, [me?.id, strike]) }, [me?.id, strike])
} }

View File

@ -24,7 +24,6 @@ import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header'
import UserPopover from './user-popover' import UserPopover from './user-popover'
import { useQrPayment } from './payment' import { useQrPayment } from './payment'
import { useRetryCreateItem } from './use-item-submit' import { useRetryCreateItem } from './use-item-submit'
import { useToast } from './toast'
export default function ItemInfo ({ export default function ItemInfo ({
item, full, commentsText = 'comments', item, full, commentsText = 'comments',
@ -33,7 +32,6 @@ export default function ItemInfo ({
}) { }) {
const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000 const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000
const me = useMe() const me = useMe()
const toaster = useToast()
const router = useRouter() const router = useRouter()
const [canEdit, setCanEdit] = const [canEdit, setCanEdit] =
useState(item.mine && (Date.now() < editThreshold)) useState(item.mine && (Date.now() < editThreshold))
@ -74,14 +72,7 @@ export default function ItemInfo ({
if (me && item.invoice?.actionState && item.invoice?.actionState !== 'PAID') { if (me && item.invoice?.actionState && item.invoice?.actionState !== 'PAID') {
if (item.invoice?.actionState === 'FAILED') { if (item.invoice?.actionState === 'FAILED') {
Component = () => <span className='text-warning'>retry payment</span> Component = () => <span className='text-warning'>retry payment</span>
onClick = async () => { onClick = async () => await retryCreateItem({ variables: { invoiceId: parseInt(item.invoice?.id) } }).catch(console.error)
try {
const { error } = await retryCreateItem({ variables: { invoiceId: parseInt(item.invoice?.id) } })
if (error) throw error
} catch (error) {
toaster.danger(error.message)
}
}
} else { } else {
Component = () => ( Component = () => (
<span <span

View File

@ -36,7 +36,6 @@ import { usePollVote } from './poll'
import { paidActionCacheMods } from './use-paid-mutation' import { paidActionCacheMods } from './use-paid-mutation'
import { useRetryCreateItem } from './use-item-submit' import { useRetryCreateItem } from './use-item-submit'
import { payBountyCacheMods } from './pay-bounty' import { payBountyCacheMods } from './pay-bounty'
import { useToast } from './toast'
function Notification ({ n, fresh }) { function Notification ({ n, fresh }) {
const type = n.__typename const type = n.__typename
@ -335,7 +334,6 @@ function useActRetry ({ invoice }) {
} }
function Invoicification ({ n: { invoice, sortTime } }) { function Invoicification ({ n: { invoice, sortTime } }) {
const toaster = useToast()
const actRetry = useActRetry({ invoice }) const actRetry = useActRetry({ invoice })
const retryCreateItem = useRetryCreateItem({ id: invoice.item?.id }) const retryCreateItem = useRetryCreateItem({ id: invoice.item?.id })
const retryPollVote = usePollVote({ query: RETRY_PAID_ACTION, itemId: invoice.item?.id }) const retryPollVote = usePollVote({ query: RETRY_PAID_ACTION, itemId: invoice.item?.id })
@ -392,13 +390,8 @@ function Invoicification ({ n: { invoice, sortTime } }) {
<Button <Button
size='sm' variant='outline-warning ms-2 border-1 rounded py-0' size='sm' variant='outline-warning ms-2 border-1 rounded py-0'
style={{ '--bs-btn-hover-color': '#fff', '--bs-btn-active-color': '#fff' }} style={{ '--bs-btn-hover-color': '#fff', '--bs-btn-active-color': '#fff' }}
onClick={async () => { onClick={() => {
try { retry({ variables: { invoiceId: parseInt(invoiceId) } }).catch(console.error)
const { error } = await retry({ variables: { invoiceId: parseInt(invoiceId) } })
if (error) throw error
} catch (error) {
toaster.danger(error?.message || error?.toString?.())
}
}} }}
> >
retry retry

View File

@ -7,6 +7,7 @@ import { numWithUnits } from '@/lib/format'
import { useShowModal } from './modal' import { useShowModal } from './modal'
import { useRoot } from './root' import { useRoot } from './root'
import { ActCanceledError, useAct } from './item-act' import { ActCanceledError, useAct } from './item-act'
import { InvoiceCanceledError } from './payment'
import { useLightning } from './lightning' import { useLightning } from './lightning'
import { useToast } from './toast' import { useToast } from './toast'
@ -57,15 +58,15 @@ export default function PayBounty ({ children, item }) {
const handlePayBounty = async onCompleted => { const handlePayBounty = async onCompleted => {
try { try {
strike() strike()
const { error } = await act({ onCompleted }) await act({ onCompleted })
if (error) throw error
} catch (error) { } catch (error) {
if (error instanceof ActCanceledError) { if (error instanceof InvoiceCanceledError || error instanceof ActCanceledError) {
return return
} }
const reason = error?.message || error?.toString?.() const reason = error?.message || error?.toString?.()
toaster.danger(reason)
toaster.danger('pay bounty failed: ' + reason)
} }
} }

View File

@ -13,7 +13,6 @@ export class InvoiceCanceledError extends Error {
super(actionError ?? `invoice canceled: ${hash}`) super(actionError ?? `invoice canceled: ${hash}`)
this.name = 'InvoiceCanceledError' this.name = 'InvoiceCanceledError'
this.hash = hash this.hash = hash
this.actionError = actionError
} }
} }

View File

@ -5,7 +5,7 @@ import { useMe } from './me'
import styles from './poll.module.css' import styles from './poll.module.css'
import { signIn } from 'next-auth/react' import { signIn } from 'next-auth/react'
import ActionTooltip from './action-tooltip' import ActionTooltip from './action-tooltip'
import { useQrPayment } from './payment' import { InvoiceCanceledError, useQrPayment } from './payment'
import { useToast } from './toast' import { useToast } from './toast'
import { usePaidMutation } from './use-paid-mutation' import { usePaidMutation } from './use-paid-mutation'
import { POLL_VOTE, RETRY_PAID_ACTION } from '@/fragments/paidAction' import { POLL_VOTE, RETRY_PAID_ACTION } from '@/fragments/paidAction'
@ -25,14 +25,18 @@ export default function Poll ({ item }) {
const variables = { id: v.id } const variables = { id: v.id }
const optimisticResponse = { pollVote: { __typename: 'PollVotePaidAction', result: { id: v.id } } } const optimisticResponse = { pollVote: { __typename: 'PollVotePaidAction', result: { id: v.id } } }
try { try {
const { error } = await pollVote({ await pollVote({
variables, variables,
optimisticResponse optimisticResponse
}) })
if (error) throw error
} catch (error) { } catch (error) {
if (error instanceof InvoiceCanceledError) {
return
}
const reason = error?.message || error?.toString?.() const reason = error?.message || error?.toString?.()
toaster.danger(reason)
toaster.danger('poll vote failed: ' + reason)
} }
} }
: signIn} : signIn}

View File

@ -41,7 +41,7 @@ export default function TerritoryForm ({ sub }) {
: await upsertSub({ variables: { oldName: sub?.name, ...variables } }) : await upsertSub({ variables: { oldName: sub?.name, ...variables } })
if (error) throw error if (error) throw error
if (payError) return if (payError) throw new Error('payment required')
// modify graphql cache to include new sub // modify graphql cache to include new sub
client.cache.modify({ client.cache.modify({

View File

@ -16,13 +16,15 @@ export default function TerritoryPaymentDue ({ sub }) {
const client = useApolloClient() const client = useApolloClient()
const [paySub] = usePaidMutation(SUB_PAY) const [paySub] = usePaidMutation(SUB_PAY)
const onSubmit = useCallback(async ({ ...variables }) => { const onSubmit = useCallback(
const { error } = await paySub({ async ({ ...variables }) => {
variables const { error, payError } = await paySub({
}) variables
})
if (error) throw error if (error) throw error
}, [client, paySub]) if (payError) throw new Error('payment required')
}, [client, paySub])
if (!sub || sub.userId !== Number(me?.id) || sub.status === 'ACTIVE') return null if (!sub || sub.userId !== Number(me?.id) || sub.status === 'ACTIVE') return null

View File

@ -139,7 +139,7 @@ export const ToastProvider = ({ children }) => {
> >
<ToastBody> <ToastBody>
<div className='d-flex align-items-center'> <div className='d-flex align-items-center'>
<div className='flex-grow-1 overflow-hidden'>{toast.body}</div> <div className='flex-grow-1'>{toast.body}</div>
<Button <Button
variant={null} variant={null}
className='p-0 ps-2' className='p-0 ps-2'

View File

@ -59,7 +59,7 @@ export default function useItemSubmit (mutation,
}) })
if (error) throw error if (error) throw error
if (payError) return if (payError) throw new Error('payment required')
// we don't know the mutation name, so we have to extract the result // we don't know the mutation name, so we have to extract the result
const response = Object.values(data)[0] const response = Object.values(data)[0]

View File

@ -63,14 +63,6 @@ export function usePaidMutation (mutation,
// if the mutation returns an invoice, pay it // if the mutation returns an invoice, pay it
if (invoice) { if (invoice) {
// adds payError, escalating to a normal error if the invoice is not canceled or
// has an actionError
const addPayError = (e, rest) => ({
...rest,
payError: e,
error: e instanceof InvoiceCanceledError && e.actionError ? e : undefined
})
// should we wait for the invoice to be paid? // should we wait for the invoice to be paid?
if (response?.paymentMethod === 'OPTIMISTIC' && !forceWaitForPayment) { if (response?.paymentMethod === 'OPTIMISTIC' && !forceWaitForPayment) {
// onCompleted is called before the invoice is paid for optimistic updates // onCompleted is called before the invoice is paid for optimistic updates
@ -83,7 +75,7 @@ export function usePaidMutation (mutation,
// onPayError is called after the invoice fails to pay // onPayError is called after the invoice fails to pay
// useful for updating invoiceActionState to FAILED // useful for updating invoiceActionState to FAILED
onPayError?.(e, client.cache, { data }) onPayError?.(e, client.cache, { data })
setInnerResult(r => addPayError(e, r)) setInnerResult(r => ({ payError: e, ...r }))
}) })
} else { } else {
// the action is pessimistic // the action is pessimistic
@ -103,7 +95,7 @@ export function usePaidMutation (mutation,
} catch (e) { } catch (e) {
console.error('usePaidMutation: failed to pay invoice', e) console.error('usePaidMutation: failed to pay invoice', e)
onPayError?.(e, client.cache, { data }) onPayError?.(e, client.cache, { data })
rest = addPayError(e, rest) rest = { ...rest, payError: e, error: e }
} }
} }
} else { } else {

View File

@ -1,5 +1,6 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useWalletLogger } from '../logger' import { useWalletLogger } from '../logger'
import LNC from '@lightninglabs/lnc-web'
import { Status, migrateLocalStorage } from '.' import { Status, migrateLocalStorage } from '.'
import { bolt11Tags } from '@/lib/bolt11' import { bolt11Tags } from '@/lib/bolt11'
import useModal from '../modal' import useModal from '../modal'
@ -15,7 +16,6 @@ const mutex = new Mutex()
async function getLNC ({ me }) { async function getLNC ({ me }) {
if (window.lnc) return window.lnc if (window.lnc) return window.lnc
const { default: LNC } = await import('@lightninglabs/lnc-web')
// backwards compatibility: migrate to new storage key // backwards compatibility: migrate to new storage key
if (me) migrateLocalStorage('lnc-web:default', `lnc-web:stacker:${me.id}`) if (me) migrateLocalStorage('lnc-web:default', `lnc-web:stacker:${me.id}`)
window.lnc = new LNC({ namespace: me?.id ? `stacker:${me.id}` : undefined }) window.lnc = new LNC({ namespace: me?.id ? `stacker:${me.id}` : undefined })

View File

@ -373,7 +373,7 @@ services:
build: build:
context: ./docker/litd context: ./docker/litd
profiles: profiles:
- wallets - payments
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
<<: *healthcheck <<: *healthcheck
@ -479,7 +479,7 @@ services:
context: ./docker/nwc context: ./docker/nwc
container_name: nwc container_name: nwc
profiles: profiles:
- wallets - payments
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
stacker_lnd: stacker_lnd:
@ -509,7 +509,7 @@ services:
image: lnbits/lnbits:0.12.5 image: lnbits/lnbits:0.12.5
container_name: lnbits container_name: lnbits
profiles: profiles:
- wallets - payments
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${LNBITS_WEB_PORT}:5000" - "${LNBITS_WEB_PORT}:5000"

2
sndev
View File

@ -10,7 +10,7 @@ docker__compose() {
fi fi
if [ -z "$COMPOSE_PROFILES" ]; then if [ -z "$COMPOSE_PROFILES" ]; then
COMPOSE_PROFILES="images,search,payments,wallets,email,capture" COMPOSE_PROFILES="images,search,payments,email,capture"
fi fi
ENV_LOCAL= ENV_LOCAL=