idempotent zaps
This commit is contained in:
parent
374a7985da
commit
73ad93f2bb
|
@ -786,6 +786,52 @@ export default {
|
|||
|
||||
notifyZapped({ models, id })
|
||||
|
||||
return {
|
||||
id,
|
||||
sats,
|
||||
act,
|
||||
path: item.path
|
||||
}
|
||||
},
|
||||
idempotentAct: async (parent, { id, sats, act = 'TIP', hash, hmac }, { me, models, lnd, headers }) => {
|
||||
if (!me) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
|
||||
await ssValidate(actSchema, { sats, act })
|
||||
await assertGofacYourself({ models, headers })
|
||||
|
||||
const [item] = await models.$queryRawUnsafe(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE id = $1`, Number(id))
|
||||
|
||||
if (Number(item.userId) === Number(me.id)) {
|
||||
throw new GraphQLError('cannot zap your self', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
|
||||
// Disallow tips if me is one of the forward user recipients
|
||||
if (act === 'TIP') {
|
||||
const existingForwards = await models.itemForward.findMany({ where: { itemId: Number(id) } })
|
||||
if (existingForwards.some(fwd => Number(fwd.userId) === Number(me.id))) {
|
||||
throw new GraphQLError('cannot zap a post for which you are forwarded zaps', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
}
|
||||
|
||||
await serializeInvoicable(
|
||||
models.$queryRaw`
|
||||
SELECT
|
||||
item_act(${Number(id)}::INTEGER, ${me.id}::INTEGER, ${act}::"ItemActType",
|
||||
(SELECT ${Number(sats)}::INTEGER - COALESCE(sum(msats) / 1000, 0)
|
||||
FROM "ItemAct"
|
||||
WHERE act IN ('TIP', 'FEE')
|
||||
AND "itemId" = ${Number(id)}::INTEGER
|
||||
AND "userId" = ${me.id}::INTEGER)::INTEGER)`,
|
||||
{ me, models, lnd, hash, hmac, enforceFee: sats }
|
||||
)
|
||||
|
||||
notifyZapped({ models, id })
|
||||
|
||||
return {
|
||||
id,
|
||||
sats,
|
||||
|
|
|
@ -37,6 +37,7 @@ export default gql`
|
|||
updateNoteId(id: ID!, noteId: String!): Item!
|
||||
upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: String): Item!
|
||||
act(id: ID!, sats: Int, act: String, hash: String, hmac: String): ItemActResult!
|
||||
idempotentAct(id: ID!, sats: Int, hash: String, hmac: String): ItemActResult!
|
||||
pollVote(id: ID!, hash: String, hmac: String): ID!
|
||||
}
|
||||
|
||||
|
|
|
@ -108,7 +108,6 @@ export default function Comment ({
|
|||
const ref = useRef(null)
|
||||
const router = useRouter()
|
||||
const root = useRoot()
|
||||
const [pendingSats, setPendingSats] = useState(0)
|
||||
const { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text })
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -148,7 +147,7 @@ export default function Comment ({
|
|||
<div className={`${itemStyles.item} ${styles.item}`}>
|
||||
{item.meDontLikeSats > item.meSats
|
||||
? <DownZap width={24} height={24} className={styles.dontLike} id={item.id} meDontLikeSats={item.meDontLikeSats} />
|
||||
: <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
|
||||
: <UpVote item={item} className={styles.upvote} />}
|
||||
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
||||
<div className='d-flex align-items-center'>
|
||||
{item.user?.meMute && !includeParent && collapse === 'yep'
|
||||
|
@ -162,7 +161,6 @@ export default function Comment ({
|
|||
</span>)
|
||||
: <ItemInfo
|
||||
item={item}
|
||||
pendingSats={pendingSats}
|
||||
commentsText='replies'
|
||||
commentTextSingular='reply'
|
||||
className={`${itemStyles.other} ${styles.other}`}
|
||||
|
|
|
@ -6,7 +6,6 @@ import AccordianItem from './accordian-item'
|
|||
import Flag from '../svgs/flag-fill.svg'
|
||||
import { useMemo } from 'react'
|
||||
import getColor from '../lib/rainbow'
|
||||
import { useLightning } from './lightning'
|
||||
|
||||
export function DownZap ({ id, meDontLikeSats, ...props }) {
|
||||
const style = useMemo(() => (meDontLikeSats
|
||||
|
@ -23,7 +22,6 @@ export function DownZap ({ id, meDontLikeSats, ...props }) {
|
|||
function DownZapper ({ id, As, children }) {
|
||||
const toaster = useToast()
|
||||
const showModal = useShowModal()
|
||||
const strike = useLightning()
|
||||
|
||||
return (
|
||||
<As
|
||||
|
@ -34,7 +32,7 @@ function DownZapper ({ id, As, children }) {
|
|||
onClose={() => {
|
||||
onClose()
|
||||
toaster.success('item downzapped')
|
||||
}} itemId={id} strike={strike} down
|
||||
}} itemId={id} down
|
||||
>
|
||||
<AccordianItem
|
||||
header='what is a downzap?' body={
|
||||
|
|
|
@ -6,6 +6,9 @@ import { useMe } from './me'
|
|||
import UpBolt from '../svgs/bolt.svg'
|
||||
import { amountSchema } from '../lib/validate'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import { payOrLoginError, useInvoiceModal } from './invoice'
|
||||
import { useToast } from './toast'
|
||||
import { useLightning } from './lightning'
|
||||
|
||||
const defaultTips = [100, 1000, 10000, 100000]
|
||||
|
||||
|
@ -37,10 +40,11 @@ const addCustomTip = (amount) => {
|
|||
window.localStorage.setItem('custom-tips', JSON.stringify(customTips))
|
||||
}
|
||||
|
||||
export default function ItemAct ({ onClose, itemId, down, strike, children }) {
|
||||
export default function ItemAct ({ onClose, itemId, down, children }) {
|
||||
const inputRef = useRef(null)
|
||||
const me = useMe()
|
||||
const [oValue, setOValue] = useState()
|
||||
const strike = useLightning()
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
|
@ -63,7 +67,7 @@ export default function ItemAct ({ onClose, itemId, down, strike, children }) {
|
|||
hmac
|
||||
}
|
||||
})
|
||||
strike && await strike()
|
||||
await strike()
|
||||
addCustomTip(Number(amount))
|
||||
onClose()
|
||||
}, [act, down, itemId, strike])
|
||||
|
@ -166,3 +170,110 @@ export function useAct ({ onUpdate } = {}) {
|
|||
}`, { update }
|
||||
)
|
||||
}
|
||||
|
||||
export function useZap () {
|
||||
const update = useCallback((cache, args) => {
|
||||
const { data: { idempotentAct: { id, sats, path } } } = args
|
||||
|
||||
// determine how much we increased existing sats by by checking the
|
||||
// difference between result sats and meSats
|
||||
// if it's negative, skip the cache as it's an out of order update
|
||||
// if it's positive, add it to sats and commentSats
|
||||
|
||||
const item = cache.readFragment({
|
||||
id: `Item:${id}`,
|
||||
fragment: gql`
|
||||
fragment ItemMeSats on Item {
|
||||
meSats
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
const satsDelta = sats - item.meSats
|
||||
|
||||
if (satsDelta > 0) {
|
||||
cache.modify({
|
||||
id: `Item:${id}`,
|
||||
fields: {
|
||||
sats (existingSats = 0) {
|
||||
return existingSats + satsDelta
|
||||
},
|
||||
meSats: () => {
|
||||
return sats
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// update all ancestors
|
||||
path.split('.').forEach(aId => {
|
||||
if (Number(aId) === Number(id)) return
|
||||
cache.modify({
|
||||
id: `Item:${aId}`,
|
||||
fields: {
|
||||
commentSats (existingCommentSats = 0) {
|
||||
return existingCommentSats + satsDelta
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [zap] = useMutation(
|
||||
gql`
|
||||
mutation idempotentAct($id: ID!, $sats: Int!, $hash: String, $hmac: String) {
|
||||
idempotentAct(id: $id, sats: $sats, hash: $hash, hmac: $hmac) {
|
||||
id
|
||||
sats
|
||||
path
|
||||
}
|
||||
}`, { update }
|
||||
)
|
||||
|
||||
const toaster = useToast()
|
||||
const strike = useLightning()
|
||||
const [act] = useAct()
|
||||
|
||||
const showInvoiceModal = useInvoiceModal(
|
||||
async ({ hash, hmac }, { variables }) => {
|
||||
await act({ variables: { ...variables, hash, hmac } })
|
||||
strike()
|
||||
}, [act, strike])
|
||||
|
||||
return useCallback(async ({ item, me }) => {
|
||||
console.log(item)
|
||||
const meSats = (item?.meSats || 0)
|
||||
|
||||
// what should our next tip be?
|
||||
let sats = me?.privates?.tipDefault || 1
|
||||
if (me?.privates?.turboTipping) {
|
||||
while (meSats >= sats) {
|
||||
sats *= 10
|
||||
}
|
||||
} else {
|
||||
sats = meSats + sats
|
||||
}
|
||||
|
||||
const variables = { id: item.id, sats, act: 'TIP' }
|
||||
try {
|
||||
await zap({
|
||||
variables,
|
||||
optimisticResponse: {
|
||||
idempotentAct: {
|
||||
path: item.path,
|
||||
...variables
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
if (payOrLoginError(error)) {
|
||||
// call non-idempotent version
|
||||
const amount = sats - meSats
|
||||
showInvoiceModal({ amount }, { variables: { ...variables, sats: amount } })
|
||||
return
|
||||
}
|
||||
console.error(error)
|
||||
toaster.danger(error?.message || error?.toString?.())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import MuteDropdownItem from './mute'
|
|||
import { DropdownItemUpVote } from './upvote'
|
||||
|
||||
export default function ItemInfo ({
|
||||
item, pendingSats, full, commentsText = 'comments',
|
||||
item, full, commentsText = 'comments',
|
||||
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
||||
onQuoteReply, nofollow, extraBadges
|
||||
}) {
|
||||
|
@ -40,8 +40,8 @@ export default function ItemInfo ({
|
|||
}, [item])
|
||||
|
||||
useEffect(() => {
|
||||
if (item) setMeTotalSats((item.meSats || 0) + (item.meAnonSats || 0) + (pendingSats || 0))
|
||||
}, [item?.meSats, item?.meAnonSats, pendingSats])
|
||||
if (item) setMeTotalSats((item.meSats || 0) + (item.meAnonSats || 0))
|
||||
}, [item?.meSats, item?.meAnonSats])
|
||||
|
||||
return (
|
||||
<div className={className || `${styles.other}`}>
|
||||
|
@ -57,7 +57,7 @@ export default function ItemInfo ({
|
|||
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
||||
: ''} from me)`} `}
|
||||
>
|
||||
{numWithUnits(item.sats + pendingSats)}
|
||||
{numWithUnits(item.sats)}
|
||||
</span>
|
||||
<span> \ </span>
|
||||
</>}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
import styles from './item.module.css'
|
||||
import UpVote from './upvote'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { AD_USER_ID, NOFOLLOW_LIMIT } from '../lib/constants'
|
||||
import Pin from '../svgs/pushpin-fill.svg'
|
||||
import reactStringReplace from 'react-string-replace'
|
||||
|
@ -27,7 +27,6 @@ export function SearchTitle ({ title }) {
|
|||
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments, onQuoteReply }) {
|
||||
const titleRef = useRef()
|
||||
const router = useRouter()
|
||||
const [pendingSats, setPendingSats] = useState(0)
|
||||
|
||||
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
|
||||
const nofollow = item.sats + item.boost < NOFOLLOW_LIMIT && !item.position ? 'nofollow' : ''
|
||||
|
@ -47,7 +46,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
|||
? <DownZap width={24} height={24} className={styles.dontLike} id={item.id} meDontLikeSats={item.meDontLikeSats} />
|
||||
: Number(item.user?.id) === AD_USER_ID
|
||||
? <AdIcon width={24} height={24} className={styles.ad} />
|
||||
: <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
|
||||
: <UpVote item={item} className={styles.upvote} />}
|
||||
<div className={styles.hunk}>
|
||||
<div className={`${styles.main} flex-wrap`}>
|
||||
<Link
|
||||
|
@ -93,7 +92,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
|||
</>}
|
||||
</div>
|
||||
<ItemInfo
|
||||
full={full} item={item} pendingSats={pendingSats}
|
||||
full={full} item={item}
|
||||
onQuoteReply={onQuoteReply}
|
||||
nofollow={nofollow}
|
||||
extraBadges={Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
||||
|
|
|
@ -54,7 +54,6 @@ export default function Items ({ ssrData, variables = {}, query, destructureData
|
|||
}
|
||||
|
||||
export function ListItem ({ item, ...props }) {
|
||||
console.log(item)
|
||||
return (
|
||||
item.parentId
|
||||
? <CommentFlat item={item} noReply includeParent clickToContext {...props} />
|
||||
|
|
|
@ -2,7 +2,7 @@ import UpBolt from '../svgs/bolt.svg'
|
|||
import styles from './upvote.module.css'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import ItemAct, { useAct } from './item-act'
|
||||
import ItemAct, { useAct, useZap } from './item-act'
|
||||
import { useMe } from './me'
|
||||
import getColor from '../lib/rainbow'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
|
@ -10,11 +10,8 @@ import LongPressable from 'react-longpressable'
|
|||
import Overlay from 'react-bootstrap/Overlay'
|
||||
import Popover from 'react-bootstrap/Popover'
|
||||
import { useShowModal } from './modal'
|
||||
import { LightningConsumer, useLightning } from './lightning'
|
||||
import { useLightning } from './lightning'
|
||||
import { numWithUnits } from '../lib/format'
|
||||
import { payOrLoginError, useInvoiceModal } from './invoice'
|
||||
import useDebounceCallback from './use-debounce-callback'
|
||||
import { useToast } from './toast'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
|
||||
const UpvotePopover = ({ target, show, handleClose }) => {
|
||||
|
@ -58,13 +55,12 @@ const TipPopover = ({ target, show, handleClose }) => (
|
|||
|
||||
export function DropdownItemUpVote ({ item }) {
|
||||
const showModal = useShowModal()
|
||||
const strike = useLightning()
|
||||
|
||||
return (
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
showModal(onClose =>
|
||||
<ItemAct onClose={onClose} itemId={item.id} strike={strike} />)
|
||||
<ItemAct onClose={onClose} itemId={item.id} />)
|
||||
}}
|
||||
>
|
||||
<span className='text-success'>zap</span>
|
||||
|
@ -72,14 +68,12 @@ export function DropdownItemUpVote ({ item }) {
|
|||
)
|
||||
}
|
||||
|
||||
export default function UpVote ({ item, className, pendingSats, setPendingSats }) {
|
||||
export default function UpVote ({ item, className }) {
|
||||
const showModal = useShowModal()
|
||||
const [voteShow, _setVoteShow] = useState(false)
|
||||
const [tipShow, _setTipShow] = useState(false)
|
||||
const ref = useRef()
|
||||
const me = useMe()
|
||||
const strike = useLightning()
|
||||
const toaster = useToast()
|
||||
const [setWalkthrough] = useMutation(
|
||||
gql`
|
||||
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
|
||||
|
@ -116,61 +110,32 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
}, [me, tipShow, setWalkthrough])
|
||||
|
||||
const [act] = useAct()
|
||||
|
||||
const showInvoiceModal = useInvoiceModal(
|
||||
async ({ hash, hmac }, { variables }) => {
|
||||
await act({ variables: { ...variables, hash, hmac } })
|
||||
strike()
|
||||
}, [act, strike])
|
||||
|
||||
const zap = useDebounceCallback(async (sats) => {
|
||||
if (!sats) return
|
||||
const variables = { id: item.id, sats, act: 'TIP' }
|
||||
|
||||
act({
|
||||
variables,
|
||||
optimisticResponse: {
|
||||
act: {
|
||||
id: item.id,
|
||||
sats,
|
||||
path: item.path,
|
||||
act: 'TIP'
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (payOrLoginError(error)) {
|
||||
showInvoiceModal({ amount: sats }, { variables })
|
||||
return
|
||||
}
|
||||
console.error(error)
|
||||
toaster.danger(error?.message || error?.toString?.())
|
||||
})
|
||||
setPendingSats(0)
|
||||
}, 500, [act, toaster, item?.id, item?.path, showInvoiceModal, setPendingSats])
|
||||
const zap = useZap()
|
||||
const strike = useLightning()
|
||||
|
||||
const disabled = useMemo(() => item?.mine || item?.meForward || item?.deletedAt,
|
||||
[item?.mine, item?.meForward, item?.deletedAt])
|
||||
|
||||
const [meSats, sats, overlayText, color] = useMemo(() => {
|
||||
const meSats = (item?.meSats || item?.meAnonSats || 0) + pendingSats
|
||||
const [meSats, overlayText, color] = useMemo(() => {
|
||||
const meSats = (item?.meSats || item?.meAnonSats || 0)
|
||||
|
||||
// what should our next tip be?
|
||||
let sats = me?.privates?.tipDefault || 1
|
||||
let raiseSats = sats
|
||||
if (me?.privates?.turboTipping) {
|
||||
let raiseTip = sats
|
||||
while (meSats >= raiseTip) {
|
||||
raiseTip *= 10
|
||||
while (meSats >= raiseSats) {
|
||||
raiseSats *= 10
|
||||
}
|
||||
|
||||
sats = raiseTip - meSats
|
||||
sats = raiseSats - meSats
|
||||
} else {
|
||||
raiseSats = meSats + sats
|
||||
}
|
||||
|
||||
return [meSats, sats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it', getColor(meSats)]
|
||||
}, [item?.meSats, item?.meAnonSats, pendingSats, me?.privates?.tipDefault, me?.privates?.turboDefault])
|
||||
return [meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it', getColor(meSats)]
|
||||
}, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault])
|
||||
|
||||
return (
|
||||
<LightningConsumer>
|
||||
{(strike) =>
|
||||
<div ref={ref} className='upvoteParent'>
|
||||
<LongPressable
|
||||
onLongPress={
|
||||
|
@ -184,7 +149,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
|
||||
setTipShow(false)
|
||||
showModal(onClose =>
|
||||
<ItemAct onClose={onClose} itemId={item.id} strike={strike} />)
|
||||
<ItemAct onClose={onClose} itemId={item.id} />)
|
||||
}
|
||||
}
|
||||
onShortPress={
|
||||
|
@ -205,13 +170,9 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
|
||||
strike()
|
||||
|
||||
setPendingSats(pendingSats => {
|
||||
const zapAmount = pendingSats + sats
|
||||
zap(zapAmount)
|
||||
return zapAmount
|
||||
})
|
||||
zap({ item, me })
|
||||
}
|
||||
: () => showModal(onClose => <ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
|
||||
: () => showModal(onClose => <ItemAct onClose={onClose} itemId={item.id} act={act} />)
|
||||
}
|
||||
>
|
||||
<ActionTooltip notForm disable={disabled} overlayText={overlayText}>
|
||||
|
@ -239,7 +200,6 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
</LongPressable>
|
||||
<TipPopover target={ref.current} show={tipShow} handleClose={() => setTipShow(false)} />
|
||||
<UpvotePopover target={ref.current} show={voteShow} handleClose={() => setVoteShow(false)} />
|
||||
</div>}
|
||||
</LightningConsumer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
-- Update item_act to noop on negative or zero sats
|
||||
CREATE OR REPLACE FUNCTION item_act(item_id INTEGER, user_id INTEGER, act "ItemActType", act_sats INTEGER)
|
||||
RETURNS INTEGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats BIGINT;
|
||||
act_msats BIGINT;
|
||||
fee_msats BIGINT;
|
||||
item_act_id INTEGER;
|
||||
fwd_entry record; -- for loop iterator variable to iterate across forward recipients
|
||||
fwd_msats BIGINT; -- for loop variable calculating how many msats to give each forward recipient
|
||||
total_fwd_msats BIGINT := 0; -- accumulator to see how many msats have been forwarded for the act
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
IF act_sats <= 0 THEN
|
||||
RETURN 0;
|
||||
END IF;
|
||||
|
||||
act_msats := act_sats * 1000;
|
||||
SELECT msats INTO user_msats FROM users WHERE id = user_id;
|
||||
IF act_msats > user_msats THEN
|
||||
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||
END IF;
|
||||
|
||||
-- deduct msats from actor
|
||||
UPDATE users SET msats = msats - act_msats WHERE id = user_id;
|
||||
|
||||
IF act = 'TIP' THEN
|
||||
-- call to influence weightedVotes ... we need to do this before we record the acts because
|
||||
-- the priors acts are taken into account
|
||||
PERFORM weighted_votes_after_tip(item_id, user_id, act_sats);
|
||||
-- call to denormalize sats and commentSats
|
||||
PERFORM sats_after_tip(item_id, user_id, act_msats);
|
||||
|
||||
-- take 10% and insert as FEE
|
||||
fee_msats := CEIL(act_msats * 0.1);
|
||||
act_msats := act_msats - fee_msats;
|
||||
|
||||
-- save the fee act into item_act_id so we can record referral acts
|
||||
INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (fee_msats, item_id, user_id, 'FEE', now_utc(), now_utc())
|
||||
RETURNING id INTO item_act_id;
|
||||
|
||||
-- leave the rest as a tip
|
||||
INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (act_msats, item_id, user_id, 'TIP', now_utc(), now_utc());
|
||||
|
||||
-- denormalize bounty paid (if applicable)
|
||||
PERFORM bounty_paid_after_act(item_id, user_id);
|
||||
|
||||
-- add sats to actees' balance and stacked count
|
||||
FOR fwd_entry IN SELECT "userId", "pct" FROM "ItemForward" WHERE "itemId" = item_id
|
||||
LOOP
|
||||
-- fwd_msats represents the sats for this forward recipient from this particular tip action
|
||||
fwd_msats := act_msats * fwd_entry.pct / 100;
|
||||
-- keep track of how many msats have been forwarded, so we can give any remaining to OP
|
||||
total_fwd_msats := fwd_msats + total_fwd_msats;
|
||||
|
||||
UPDATE users
|
||||
SET msats = msats + fwd_msats, "stackedMsats" = "stackedMsats" + fwd_msats
|
||||
WHERE id = fwd_entry."userId";
|
||||
END LOOP;
|
||||
|
||||
-- Give OP any remaining msats after forwards have been applied
|
||||
IF act_msats - total_fwd_msats > 0 THEN
|
||||
UPDATE users
|
||||
SET msats = msats + act_msats - total_fwd_msats, "stackedMsats" = "stackedMsats" + act_msats - total_fwd_msats
|
||||
WHERE id = (SELECT "userId" FROM "Item" WHERE id = item_id);
|
||||
END IF;
|
||||
ELSE -- BOOST, POLL, DONT_LIKE_THIS, STREAM
|
||||
-- call to influence if DONT_LIKE_THIS weightedDownVotes
|
||||
IF act = 'DONT_LIKE_THIS' THEN
|
||||
PERFORM weighted_downvotes_after_act(item_id, user_id, act_sats);
|
||||
END IF;
|
||||
|
||||
INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (act_msats, item_id, user_id, act, now_utc(), now_utc())
|
||||
RETURNING id INTO item_act_id;
|
||||
END IF;
|
||||
|
||||
-- store referral effects
|
||||
PERFORM referral_act(item_act_id);
|
||||
|
||||
RETURN 0;
|
||||
END;
|
||||
$$;
|
Loading…
Reference in New Issue