Skip to content

Commit

Permalink
Add anon zaps
Browse files Browse the repository at this point in the history
  • Loading branch information
ekzyis committed Jul 11, 2023
1 parent bc9081e commit 87ec14a
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 65 deletions.
37 changes: 36 additions & 1 deletion api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
import domino from 'domino'
import {
BOOST_MIN, ITEM_SPAM_INTERVAL,
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, ANON_USER_NAME
} from '../../lib/constants'
import { msatsToSats } from '../../lib/format'
import { parse } from 'tldts'
import uu from 'url-unshort'
import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
import { sendUserNotification } from '../webPush'
import { createInvoice } from 'ln-service'

async function comments (me, models, id, sort) {
let orderBy
Expand Down Expand Up @@ -960,6 +961,40 @@ export default {
sats
}
},
anonAct: async (parent, { id, sats }, { me, models, lnd }) => {
// this should never be called when logged in
if (me) {
throw new AuthenticationError('you must not be logged in')
}

await ssValidate(amountSchema, { amount: sats })

const user = await models.user.findUnique({ where: { name: ANON_USER_NAME } })

// set expires at to 3 hours into future
const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3))
const description = `Zapping item @${id} on stacker.news`
try {
const invoice = await createInvoice({
description: user.hideInvoiceDesc ? undefined : description,
lnd,
tokens: sats,
expires_at: expiresAt
})

const [[inv]] = await serialize(models,
models.$queryRaw`SELECT * FROM create_invoice(
${invoice.id}, ${invoice.request},
${expiresAt}, ${sats * 1000}, ${user.id}, ${description})`,
models.invoice.update({ where: { hash: invoice.id }, data: { itemId: Number(id) } })
)

return inv
} catch (error) {
console.log(error)
throw error
}
},
dontLikeThis: async (parent, { id }, { me, models }) => {
// need to make sure we are logged in
if (!me) {
Expand Down
8 changes: 4 additions & 4 deletions api/resolvers/serial.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
const { UserInputError } = require('apollo-server-micro')
const retry = require('async-retry')

async function serialize (models, call) {
async function serialize (models, ...calls) {
return await retry(async bail => {
try {
const [, result] = await models.$transaction([
const [, ...result] = await models.$transaction([
models.$executeRaw(SERIALIZE),
call
...calls
])
return result
return calls.length > 1 ? result : result[0]
} catch (error) {
console.log(error)
if (error.message.includes('SN_INSUFFICIENT_FUNDS')) {
Expand Down
13 changes: 7 additions & 6 deletions api/resolvers/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ import { SELECT } from './item'
import { lnurlPayDescriptionHash } from '../../lib/lnurl'
import { msatsToSats, msatsToSatsDecimal } from '../../lib/format'
import { amountSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '../../lib/validate'
import { ANON_USER_NAME } from '../../lib/constants'

export async function getInvoice (parent, { id }, { me, models }) {
if (!me) {
throw new AuthenticationError('you must be logged in')
}

const inv = await models.invoice.findUnique({
where: {
id: Number(id)
Expand All @@ -22,8 +19,12 @@ export async function getInvoice (parent, { id }, { me, models }) {
}
})

if (inv.user.id !== me.id) {
throw new AuthenticationError('not ur invoice')
if (me) {
if (inv.user.id !== me.id) {
throw new AuthenticationError('not ur invoice')
}
} else if (inv.user.name !== ANON_USER_NAME) {
throw new AuthenticationError('you must be logged in')
}

return inv
Expand Down
1 change: 1 addition & 0 deletions api/typeDefs/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default gql`
updateComment(id: ID!, text: String!): Item!
dontLikeThis(id: ID!): Boolean!
act(id: ID!, sats: Int): ItemActResult!
anonAct(id: ID!, sats: Int): Invoice!
pollVote(id: ID!): ID!
}
Expand Down
3 changes: 2 additions & 1 deletion components/invoice.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import Qr from './qr'

export function Invoice ({ invoice }) {
export function Invoice ({ invoice, onSuccess }) {
let variant = 'default'
let status = 'waiting for you'
if (invoice.confirmedAt) {
variant = 'confirmed'
status = `${invoice.satsReceived} sats deposited`
onSuccess?.(invoice.satsReceived)
} else if (invoice.cancelled) {
variant = 'failed'
status = 'cancelled'
Expand Down
8 changes: 5 additions & 3 deletions components/item-act.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
sats: Number(amount)
}
})
await strike()
addCustomTip(Number(amount))
onClose()
if (me) {
await strike()
addCustomTip(Number(amount))
onClose()
}
}}
>
<Input
Expand Down
7 changes: 6 additions & 1 deletion components/item-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@ export default function ItemInfo ({ item, pendingSats, full, commentsText, class
const [canEdit, setCanEdit] =
useState(item.mine && (Date.now() < editThreshold))
const [hasNewComments, setHasNewComments] = useState(false)
const [meTotalSats, setMeTotalSats] = useState(0)
useEffect(() => {
if (!full) {
setHasNewComments(newComments(item))
}
}, [item])

useEffect(() => {
if (item) setMeTotalSats(item.meSats + item.meAnonSats + pendingSats)
}, [item, pendingSats])

return (
<div className={className || `${styles.other}`}>
{!item.position &&
<>
<span title={`from ${item.upvotes} stackers ${item.mine ? `\\ ${item.meSats} sats to post` : `(${item.meSats + pendingSats} sats from me)`} `}>{abbrNum(item.sats + pendingSats)} sats</span>
<span title={`from ${item.upvotes} stackers ${item.mine ? `\\ ${item.meSats} sats to post` : `(${meTotalSats} sats from me)`} `}>{abbrNum(item.sats + pendingSats)} sats</span>
<span> \ </span>
</>}
{item.boost > 0 &&
Expand Down
139 changes: 93 additions & 46 deletions components/upvote.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import UpBolt from '../svgs/bolt.svg'
import styles from './upvote.module.css'
import { gql, useMutation } from '@apollo/client'
import { gql, useApolloClient, useMutation } from '@apollo/client'
import FundError from './fund-error'
import ActionTooltip from './action-tooltip'
import ItemAct from './item-act'
Expand All @@ -10,8 +10,8 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import LongPressable from 'react-longpressable'
import { Overlay, Popover } from 'react-bootstrap'
import { useShowModal } from './modal'
import { useRouter } from 'next/router'
import { LightningConsumer } from './lightning'
import { LightningConsumer, useLightning } from './lightning'
import { LoadInvoice } from '../pages/invoices/[id]'

const getColor = (meSats) => {
if (!meSats || meSats <= 10) {
Expand Down Expand Up @@ -64,8 +64,8 @@ const TipPopover = ({ target, show, handleClose }) => (
)

export default function UpVote ({ item, className, pendingSats, setPendingSats }) {
const { cache } = useApolloClient()
const showModal = useShowModal()
const router = useRouter()
const [voteShow, _setVoteShow] = useState(false)
const [tipShow, _setTipShow] = useState(false)
const ref = useRef()
Expand All @@ -77,6 +77,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
setWalkthrough(upvotePopover: $upvotePopover, tipPopover: $tipPopover)
}`
)
const strike = useLightning()

const setVoteShow = (yes) => {
if (!me) return
Expand Down Expand Up @@ -107,6 +108,51 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
}
}

const updateCache = (mutation, sats) => {
setPendingSats(0)

const fields = {
sats (existingSats = 0) {
return existingSats + sats
},
meSats: mutation === 'act'
? (existingSats = 0) => {
if (sats <= me.sats) {
if (existingSats === 0) {
setVoteShow(true)
} else {
setTipShow(true)
}
}

return existingSats + sats
}
: undefined,
meAnonSats: mutation === 'anonAct'
? (existingSats = 0) => {
return existingSats + sats
}
: undefined
}
cache.modify({
id: `Item:${item.id}`,
fields
})

// update all ancestors
item.path.split('.').forEach(id => {
if (Number(id) === Number(item.id)) return
cache.modify({
id: `Item:${id}`,
fields: {
commentSats (existingCommentSats = 0) {
return existingCommentSats + sats
}
}
})
})
}

const [act] = useMutation(
gql`
mutation act($id: ID!, $sats: Int!) {
Expand All @@ -115,42 +161,25 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
}
}`, {
update (cache, { data: { act: { sats } } }) {
cache.modify({
id: `Item:${item.id}`,
fields: {
sats (existingSats = 0) {
return existingSats + sats
},
meSats (existingSats = 0) {
if (sats <= me.sats) {
if (existingSats === 0) {
setVoteShow(true)
} else {
setTipShow(true)
}
}

return existingSats + sats
}
}
})

// update all ancestors
item.path.split('.').forEach(id => {
if (Number(id) === Number(item.id)) return
cache.modify({
id: `Item:${id}`,
fields: {
commentSats (existingCommentSats = 0) {
return existingCommentSats + sats
}
}
})
})
updateCache('act', sats)
}
}
)

const [anonAct, { data: anonActData }] = useMutation(
gql`
mutation anonAct($id: ID!, $sats: Int!) {
anonAct(id: $id, sats: $sats) {
id
bolt11
satsReceived
cancelled
confirmedAt
expiresAt
}
}`
)

// if we want to use optimistic response, we need to buffer the votes
// because if someone votes in quick succession, responses come back out of order
// so we wait a bit to see if there are more votes coming in
Expand Down Expand Up @@ -194,8 +223,9 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
return item?.mine || (me && Number(me.id) === item?.fwdUserId) || item?.deletedAt
}, [me?.id, item?.fwdUserId, item?.mine, item?.deletedAt])

const [meSats, sats, overlayText, color] = useMemo(() => {
const [meSats, meTotalSats, sats, overlayText, color] = useMemo(() => {
const meSats = (item?.meSats || 0) + pendingSats
const meTotalSats = meSats + (item?.meAnonSats || 0)

// what should our next tip be?
let sats = me?.tipDefault || 1
Expand All @@ -208,8 +238,28 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
sats = raiseTip - meSats
}

return [meSats, sats, `${sats} sat${sats > 1 ? 's' : ''}`, getColor(meSats)]
}, [item?.meSats, pendingSats, me?.tipDefault, me?.turboDefault])
return [meSats, meTotalSats, sats, `${sats} sat${sats > 1 ? 's' : ''}`, getColor(meTotalSats)]
}, [item?.meSats, item?.meAnonSats, pendingSats, me?.tipDefault, me?.turboDefault])

const invoice = anonActData?.anonAct
useEffect(() => {
if (invoice) {
showModal(onClose => (
<LoadInvoice
id={invoice.id}
onSuccess={(satsReceived) => {
const key = `TIP-item:${item.id}`
const meAnonSats = Number(localStorage.getItem(key) || '0')
localStorage.setItem(key, meAnonSats + satsReceived)
setTimeout(() => {
onClose()
updateCache('anonAct', satsReceived)
strike()
}, 2000)
}}
/>))
}
}, [invoice])

return (
<LightningConsumer>
Expand All @@ -227,7 +277,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }

setTipShow(false)
showModal(onClose =>
<ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
<ItemAct onClose={onClose} itemId={item.id} act={me ? act : anonAct} strike={strike} />)
}
}
onShortPress={
Expand All @@ -248,10 +298,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }

setPendingSats(pendingSats + sats)
}
: async () => await router.push({
pathname: '/signup',
query: { callbackUrl: window.location.origin + router.asPath }
})
: () => showModal(onClose => <ItemAct onClose={onClose} itemId={item.id} act={anonAct} strike={strike} />)
}
>
<ActionTooltip notForm disable={disabled} overlayText={overlayText}>
Expand All @@ -265,9 +312,9 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
`${styles.upvote}
${className || ''}
${disabled ? styles.noSelfTips : ''}
${meSats ? styles.voted : ''}`
${meTotalSats ? styles.voted : ''}`
}
style={meSats
style={meTotalSats
? {
fill: color,
filter: `drop-shadow(0 0 6px ${color}90)`
Expand Down
1 change: 1 addition & 0 deletions fragments/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const COMMENT_FIELDS = gql`
wvotes
boost
meSats
meAnonSats @client
meDontLike
meBookmark
meSubscription
Expand Down
Loading

0 comments on commit 87ec14a

Please sign in to comment.