add ln addr + lnurl pay qr code to profile pages

This commit is contained in:
keyan 2022-05-06 14:32:20 -05:00
parent 4ba1227605
commit aa4ac2ecc9
8 changed files with 65 additions and 48 deletions

View File

@ -868,7 +868,7 @@ function newTimedOrderByWeightedSats (num) {
return ` return `
GROUP BY "Item".id GROUP BY "Item".id
ORDER BY (SUM(CASE WHEN "ItemAct".act = 'VOTE' AND "Item"."userId" <> "ItemAct"."userId" THEN users.trust ELSE 0 END)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 1.3) + ORDER BY (SUM(CASE WHEN "ItemAct".act = 'VOTE' AND "Item"."userId" <> "ItemAct"."userId" THEN users.trust ELSE 0 END)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 1.3) +
GREATEST(SUM(CASE WHEN "ItemAct".act = 'BOOST' THEN "ItemAct".sats ELSE 0 END)-1000+5, 0)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 5)) DESC NULLS LAST, "Item".id DESC` GREATEST(SUM(CASE WHEN "ItemAct".act = 'BOOST' THEN "ItemAct".sats ELSE 0 END)-1000+5, 0)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 3)) DESC NULLS LAST, "Item".id DESC`
} }
const TOP_ORDER_BY_SATS = 'GROUP BY "Item".id ORDER BY (SUM(CASE WHEN "ItemAct".act = \'VOTE\' AND "Item"."userId" <> "ItemAct"."userId" THEN users.trust ELSE 0 END)) DESC NULLS LAST, "Item".created_at DESC' const TOP_ORDER_BY_SATS = 'GROUP BY "Item".id ORDER BY (SUM(CASE WHEN "ItemAct".act = \'VOTE\' AND "Item"."userId" <> "ItemAct"."userId" THEN users.trust ELSE 0 END)) DESC NULLS LAST, "Item".created_at DESC'

View File

@ -32,11 +32,7 @@ export default {
return null return null
} }
models.user.update( return await models.user.update({ where: { id: me.id }, data: { lastSeenAt: new Date() } })
{ where: { id: me.id }, data: { lastSeenAt: new Date() } }
).catch(console.log)
return await models.user.findUnique({ where: { id: me.id } })
}, },
user: async (parent, { name }, { models }) => { user: async (parent, { name }, { models }) => {
return await models.user.findUnique({ where: { name } }) return await models.user.findUnique({ where: { name } })
@ -48,7 +44,9 @@ export default {
throw new AuthenticationError('you must be logged in') throw new AuthenticationError('you must be logged in')
} }
return me.name?.toUpperCase() === name?.toUpperCase() || !(await models.user.findUnique({ where: { name } })) const user = await models.user.findUnique({ where: { id: me.id } })
return user.name?.toUpperCase() === name?.toUpperCase() || !(await models.user.findUnique({ where: { name } }))
}, },
topUsers: async (parent, { cursor, within, userType }, { models, me }) => { topUsers: async (parent, { cursor, within, userType }, { models, me }) => {
const decodedCursor = decodeCursor(cursor) const decodedCursor = decodeCursor(cursor)
@ -136,7 +134,7 @@ export default {
} else { } else {
([item] = await serialize(models, ([item] = await serialize(models,
models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3) AS "Item"`, models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3) AS "Item"`,
`@${me.name}'s bio`, bio, Number(me.id)))) `@${user.name}'s bio`, bio, Number(me.id))))
} }
await createMentions(item, models) await createMentions(item, models)

View File

@ -178,9 +178,11 @@ export default {
throw new UserInputError('amount must be positive', { argumentName: 'amount' }) throw new UserInputError('amount must be positive', { argumentName: 'amount' })
} }
const user = await models.user.findUnique({ where: { id: me.id } })
// set expires at to 3 hours into future // set expires at to 3 hours into future
const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3)) const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3))
const description = `${amount} sats for @${me.name} on stacker.news` const description = `${amount} sats for @${user.name} on stacker.news`
try { try {
const invoice = await createInvoice({ const invoice = await createInvoice({
description, description,
@ -271,10 +273,12 @@ async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd
const msatsFee = Number(maxFee) * 1000 const msatsFee = Number(maxFee) * 1000
const user = await models.user.findUnique({ where: { id: me.id } })
// create withdrawl transactionally (id, bolt11, amount, fee) // create withdrawl transactionally (id, bolt11, amount, fee)
const [withdrawl] = await serialize(models, const [withdrawl] = await serialize(models,
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice}, models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
${Number(decoded.mtokens)}, ${msatsFee}, ${me.name})`) ${Number(decoded.mtokens)}, ${msatsFee}, ${user.name})`)
payViaPaymentRequest({ payViaPaymentRequest({
lnd, lnd,

View File

@ -0,0 +1,21 @@
import { useState } from 'react'
import { Modal } from 'react-bootstrap'
export default function ModalButton ({ children, clicker }) {
const [show, setShow] = useState()
return (
<>
<Modal
show={show}
onHide={() => setShow(false)}
>
<div className='modal-close' onClick={() => setShow(false)}>X</div>
<Modal.Body>
{children}
</Modal.Body>
</Modal>
<div className='pointer' onClick={() => setShow(true)}>{clicker}</div>
</>
)
}

View File

@ -10,9 +10,11 @@ import { gql, useApolloClient, useMutation } from '@apollo/client'
import styles from './user-header.module.css' import styles from './user-header.module.css'
import { useMe } from './me' import { useMe } from './me'
import { NAME_MUTATION, NAME_QUERY } from '../fragments/users' import { NAME_MUTATION, NAME_QUERY } from '../fragments/users'
import Image from 'next/image' // import Image from 'next/image'
import QRCode from 'qrcode.react' import QRCode from 'qrcode.react'
import LightningIcon from '../svgs/bolt.svg' import LightningIcon from '../svgs/bolt.svg'
import ModalButton from './modal-button'
import { encodeLNUrl } from '../lib/lnurl'
export default function UserHeader ({ user }) { export default function UserHeader ({ user }) {
const [editting, setEditting] = useState(false) const [editting, setEditting] = useState(false)
@ -22,7 +24,7 @@ export default function UserHeader ({ user }) {
const [setName] = useMutation(NAME_MUTATION) const [setName] = useMutation(NAME_MUTATION)
const isMe = me?.name === user.name const isMe = me?.name === user.name
const Satistics = () => <div className={`mb-4 ${styles.username} text-success`}>{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked</div> const Satistics = () => <div className={`mb-2 ${styles.username} text-success`}>{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked</div>
const UserSchema = Yup.object({ const UserSchema = Yup.object({
name: Yup.string() name: Yup.string()
@ -40,14 +42,16 @@ export default function UserHeader ({ user }) {
}) })
}) })
const lnurlp = encodeLNUrl(new URL(`https://stacker.news/.well-known/lnurlp/${user.name}`))
return ( return (
<> <>
<div className='d-flex align-items-center mt-2 flex-wrap'> <div className='d-flex align-items-center mt-2 flex-wrap'>
<Image {/* <Image
src='/dorian400.jpg' width='200' height='166' layout='fixed' src='/dorian400.jpg' width='135' height='135' layout='fixed'
className={styles.userimg} className={styles.userimg}
/> /> */}
<div className='ml-3'> <div>
{editting {editting
? ( ? (
<Form <Form
@ -85,7 +89,7 @@ export default function UserHeader ({ user }) {
setEditting(false) setEditting(false)
}} }}
> >
<div className='d-flex align-items-center mb-1'> <div className='d-flex align-items-center mb-2'>
<Input <Input
prepend=<InputGroup.Text>@</InputGroup.Text> prepend=<InputGroup.Text>@</InputGroup.Text>
name='name' name='name'
@ -98,22 +102,30 @@ export default function UserHeader ({ user }) {
</Form> </Form>
) )
: ( : (
<div className='d-flex align-items-center mb-1'> <div className='d-flex align-items-center mb-2'>
<div className={styles.username}>@{user.name}</div> <div className={styles.username}>@{user.name}</div>
{isMe && {isMe &&
<Button className='py-0' variant='link' onClick={() => setEditting(true)}>edit nym</Button>} <Button className='py-0' variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
</div> </div>
)} )}
<Satistics user={user} /> <Satistics user={user} />
<Button className='font-weight-bold'> <ModalButton
<LightningIcon clicker={
width={20} <Button className='font-weight-bold'>
height={20} <LightningIcon
className='mr-1' width={20}
/>{user.name}@stacker.news height={20}
</Button> className='mr-1'
/>{user.name}@stacker.news
</Button>
}
>
<a className='d-flex m-auto p-3' style={{ background: 'white', width: 'fit-content' }} href={`lightning:${lnurlp}`}>
<QRCode className='d-flex m-auto' value={lnurlp} renderAs='svg' size={300} />
</a>
<div className='text-center font-weight-bold text-muted mt-3'>click or scan</div>
</ModalButton>
</div> </div>
<QRCode className='ml-auto' value='fdsajfkldsajlkfjdlksajfkldjsalkjfdklsa' renderAs='svg' size={166} />
</div> </div>
<Nav <Nav
className={styles.nav} className={styles.nav}

View File

@ -1,19 +1,9 @@
import { randomBytes, createHash } from 'crypto' import { createHash } from 'crypto'
import { bech32 } from 'bech32' import { bech32 } from 'bech32'
export function lnurlAuth (params) { export function encodeLNUrl (url) {
// generate secret (32 random bytes)
const secret = Buffer.from(randomBytes(32), 'hex')
// create url
const url = new URL(process.env.LNAUTH_URL)
url.searchParams = new URLSearchParams({
...params,
k1: secret
})
// bech32 encode url
const words = bech32.toWords(Buffer.from(url.toString(), 'utf8')) const words = bech32.toWords(Buffer.from(url.toString(), 'utf8'))
const encodedUrl = bech32.encode('lnurl', words, 1023) return bech32.encode('lnurl', words, 1023)
return { secret, encodedUrl }
} }
export function lnurlPayMetadataString (username) { export function lnurlPayMetadataString (username) {

View File

@ -22,13 +22,6 @@ const options = {
token.id = user.id token.id = user.id
} }
// XXX We need to update the user name incase they update it ... kind of hacky
// better if we use user id everywhere an ignore the username ...
if (token?.id) {
const { name } = await prisma.user.findUnique({ where: { id: token.id } })
token.name = name
}
// sign them up for the newsletter // sign them up for the newsletter
if (isNewUser && profile.email) { if (isNewUser && profile.email) {
fetch(process.env.LIST_MONK_URL + '/api/subscribers', { fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
@ -52,7 +45,6 @@ const options = {
async session (session, token) { async session (session, token) {
// we need to add additional session params here // we need to add additional session params here
session.user.id = token.id session.user.id = token.id
session.user.name = token.name
return session return session
} }
}, },

View File

@ -38,7 +38,7 @@ const apolloServer = new ApolloServer({
models, models,
lnd, lnd,
me: session me: session
? await models.user.findUnique({ where: { id: session.user?.id } }) ? session.user
: null, : null,
search search
} }