donations to rewards
This commit is contained in:
parent
439c83f975
commit
e1bdb9c769
@ -74,10 +74,18 @@ export default {
|
|||||||
json_build_object('name', 'jobs', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'STREAM')),
|
json_build_object('name', 'jobs', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'STREAM')),
|
||||||
json_build_object('name', 'boost', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'BOOST')),
|
json_build_object('name', 'boost', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'BOOST')),
|
||||||
json_build_object('name', 'fees', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'FEE')),
|
json_build_object('name', 'fees', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'FEE')),
|
||||||
json_build_object('name', 'tips', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'TIP'))
|
json_build_object('name', 'tips', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'TIP')),
|
||||||
|
json_build_object('name', 'donation', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'DONATION'))
|
||||||
) AS data
|
) AS data
|
||||||
FROM times
|
FROM times
|
||||||
LEFT JOIN "ItemAct" ON ${intervalClause(when, 'ItemAct', true)} time = date_trunc('${timeUnit(when)}', created_at)
|
LEFT JOIN
|
||||||
|
((SELECT "ItemAct".created_at, "userId", act::text as act
|
||||||
|
FROM "ItemAct"
|
||||||
|
WHERE ${intervalClause(when, 'ItemAct', false)})
|
||||||
|
UNION ALL
|
||||||
|
(SELECT created_at, "userId", 'DONATION' as act
|
||||||
|
FROM "Donation"
|
||||||
|
WHERE ${intervalClause(when, 'Donation', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
|
||||||
GROUP BY time
|
GROUP BY time
|
||||||
ORDER BY time ASC`)
|
ORDER BY time ASC`)
|
||||||
},
|
},
|
||||||
@ -98,14 +106,21 @@ export default {
|
|||||||
return await models.$queryRaw(
|
return await models.$queryRaw(
|
||||||
`${withClause(when)}
|
`${withClause(when)}
|
||||||
SELECT time, json_build_array(
|
SELECT time, json_build_array(
|
||||||
json_build_object('name', 'jobs', 'value', coalesce(floor(sum(CASE WHEN act = 'STREAM' THEN "ItemAct".msats ELSE 0 END)/1000),0)),
|
json_build_object('name', 'jobs', 'value', coalesce(floor(sum(CASE WHEN act = 'STREAM' THEN msats ELSE 0 END)/1000),0)),
|
||||||
json_build_object('name', 'boost', 'value', coalesce(floor(sum(CASE WHEN act = 'BOOST' THEN "ItemAct".msats ELSE 0 END)/1000),0)),
|
json_build_object('name', 'boost', 'value', coalesce(floor(sum(CASE WHEN act = 'BOOST' THEN msats ELSE 0 END)/1000),0)),
|
||||||
json_build_object('name', 'fees', 'value', coalesce(floor(sum(CASE WHEN act NOT IN ('BOOST', 'TIP', 'STREAM') THEN "ItemAct".msats ELSE 0 END)/1000),0)),
|
json_build_object('name', 'fees', 'value', coalesce(floor(sum(CASE WHEN act NOT IN ('BOOST', 'TIP', 'STREAM', 'DONATION') THEN msats ELSE 0 END)/1000),0)),
|
||||||
json_build_object('name', 'tips', 'value', coalesce(floor(sum(CASE WHEN act = 'TIP' THEN "ItemAct".msats ELSE 0 END)/1000),0))
|
json_build_object('name', 'tips', 'value', coalesce(floor(sum(CASE WHEN act = 'TIP' THEN msats ELSE 0 END)/1000),0)),
|
||||||
|
json_build_object('name', 'donations', 'value', coalesce(floor(sum(CASE WHEN act = 'DONATION' THEN msats ELSE 0 END)/1000),0))
|
||||||
) AS data
|
) AS data
|
||||||
FROM times
|
FROM times
|
||||||
LEFT JOIN "ItemAct" ON ${intervalClause(when, 'ItemAct', true)} time = date_trunc('${timeUnit(when)}', created_at)
|
LEFT JOIN
|
||||||
JOIN "Item" ON "ItemAct"."itemId" = "Item".id
|
((SELECT "ItemAct".created_at, msats, act::text as act
|
||||||
|
FROM "ItemAct"
|
||||||
|
WHERE ${intervalClause(when, 'ItemAct', false)})
|
||||||
|
UNION ALL
|
||||||
|
(SELECT created_at, sats * 1000 as msats, 'DONATION' as act
|
||||||
|
FROM "Donation"
|
||||||
|
WHERE ${intervalClause(when, 'Donation', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
|
||||||
GROUP BY time
|
GROUP BY time
|
||||||
ORDER BY time ASC`)
|
ORDER BY time ASC`)
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,8 @@ import sub from './sub'
|
|||||||
import upload from './upload'
|
import upload from './upload'
|
||||||
import growth from './growth'
|
import growth from './growth'
|
||||||
import search from './search'
|
import search from './search'
|
||||||
|
import rewards from './rewards'
|
||||||
import { GraphQLJSONObject } from 'graphql-type-json'
|
import { GraphQLJSONObject } from 'graphql-type-json'
|
||||||
|
|
||||||
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
|
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
|
||||||
upload, growth, search, { JSONObject: GraphQLJSONObject }]
|
upload, growth, search, rewards, { JSONObject: GraphQLJSONObject }]
|
||||||
|
48
api/resolvers/rewards.js
Normal file
48
api/resolvers/rewards.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { AuthenticationError } from 'apollo-server-micro'
|
||||||
|
import serialize from './serial'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Query: {
|
||||||
|
expectedRewards: async (parent, args, { models }) => {
|
||||||
|
// get the last reward time, then get all contributions to rewards since then
|
||||||
|
const lastReward = await models.earn.findFirst({
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const [result] = await models.$queryRaw`
|
||||||
|
SELECT coalesce(sum(sats), 0) as total, json_build_array(
|
||||||
|
json_build_object('name', 'donations', 'value', coalesce(sum(sats) FILTER(WHERE type = 'DONATION'), 0)),
|
||||||
|
json_build_object('name', 'fees', 'value', coalesce(sum(sats) FILTER(WHERE type NOT IN ('BOOST', 'STREAM')), 0)),
|
||||||
|
json_build_object('name', 'boost', 'value', coalesce(sum(sats) FILTER(WHERE type = 'BOOST'), 0)),
|
||||||
|
json_build_object('name', 'jobs', 'value', coalesce(sum(sats) FILTER(WHERE type = 'STREAM'), 0))
|
||||||
|
) AS sources
|
||||||
|
FROM (
|
||||||
|
(SELECT msats / 1000 as sats, act::text as type
|
||||||
|
FROM "ItemAct"
|
||||||
|
WHERE created_at > ${lastReward.createdAt} AND "ItemAct".act <> 'TIP')
|
||||||
|
UNION ALL
|
||||||
|
(SELECT sats, 'DONATION' as type
|
||||||
|
FROM "Donation"
|
||||||
|
WHERE created_at > ${lastReward.createdAt})
|
||||||
|
) subquery`
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Mutation: {
|
||||||
|
donateToRewards: async (parent, { sats }, { me, models }) => {
|
||||||
|
if (!me) {
|
||||||
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
}
|
||||||
|
|
||||||
|
await serialize(models,
|
||||||
|
models.$queryRaw(
|
||||||
|
'SELECT donate($1, $2)',
|
||||||
|
sats, Number(me.id)))
|
||||||
|
|
||||||
|
return sats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -93,12 +93,20 @@ export default {
|
|||||||
let users
|
let users
|
||||||
if (sort === 'spent') {
|
if (sort === 'spent') {
|
||||||
users = await models.$queryRaw(`
|
users = await models.$queryRaw(`
|
||||||
SELECT users.*, floor(sum("ItemAct".msats)/1000) as spent
|
SELECT users.*, sum(sats_spent) as spent
|
||||||
|
FROM
|
||||||
|
((SELECT "userId", floor(sum("ItemAct".msats)/1000) as sats_spent
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
JOIN users on "ItemAct"."userId" = users.id
|
|
||||||
WHERE "ItemAct".created_at <= $1
|
WHERE "ItemAct".created_at <= $1
|
||||||
AND NOT users."hideFromTopUsers"
|
|
||||||
${within('ItemAct', when)}
|
${within('ItemAct', when)}
|
||||||
|
GROUP BY "userId")
|
||||||
|
UNION ALL
|
||||||
|
(SELECT "userId", sats as sats_spent
|
||||||
|
FROM "Donation"
|
||||||
|
WHERE created_at <= $1
|
||||||
|
${within('Donation', when)})) spending
|
||||||
|
JOIN users on spending."userId" = users.id
|
||||||
|
AND NOT users."hideFromTopUsers"
|
||||||
GROUP BY users.id, users.name
|
GROUP BY users.id, users.name
|
||||||
ORDER BY spent DESC NULLS LAST, users.created_at DESC
|
ORDER BY spent DESC NULLS LAST, users.created_at DESC
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
|
@ -122,6 +122,13 @@ export default {
|
|||||||
WHERE "ItemAct"."userId" = $1
|
WHERE "ItemAct"."userId" = $1
|
||||||
AND "ItemAct".created_at <= $2
|
AND "ItemAct".created_at <= $2
|
||||||
GROUP BY "Item".id)`)
|
GROUP BY "Item".id)`)
|
||||||
|
queries.push(
|
||||||
|
`(SELECT ('donation' || "Donation".id) as id, "Donation".id as "factId", NULL as bolt11,
|
||||||
|
created_at as "createdAt", sats * 1000 as msats,
|
||||||
|
0 as "msatsFee", NULL as status, 'donation' as type
|
||||||
|
FROM "Donation"
|
||||||
|
WHERE "userId" = $1
|
||||||
|
AND created_at <= $2)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queries.length === 0) {
|
if (queries.length === 0) {
|
||||||
@ -157,6 +164,9 @@ export default {
|
|||||||
case 'spent':
|
case 'spent':
|
||||||
f.msats *= -1
|
f.msats *= -1
|
||||||
break
|
break
|
||||||
|
case 'donation':
|
||||||
|
f.msats *= -1
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import invite from './invite'
|
|||||||
import sub from './sub'
|
import sub from './sub'
|
||||||
import upload from './upload'
|
import upload from './upload'
|
||||||
import growth from './growth'
|
import growth from './growth'
|
||||||
|
import rewards from './rewards'
|
||||||
|
|
||||||
const link = gql`
|
const link = gql`
|
||||||
type Query {
|
type Query {
|
||||||
@ -26,4 +27,4 @@ const link = gql`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export default [link, user, item, message, wallet, lnurl, notifications, invite,
|
export default [link, user, item, message, wallet, lnurl, notifications, invite,
|
||||||
sub, upload, growth]
|
sub, upload, growth, rewards]
|
||||||
|
16
api/typeDefs/rewards.js
Normal file
16
api/typeDefs/rewards.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { gql } from 'apollo-server-micro'
|
||||||
|
|
||||||
|
export default gql`
|
||||||
|
extend type Query {
|
||||||
|
expectedRewards: ExpectedRewards!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Mutation {
|
||||||
|
donateToRewards(sats: Int!): Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpectedRewards {
|
||||||
|
total: Int!
|
||||||
|
sources: [NameValue!]!
|
||||||
|
}
|
||||||
|
`
|
@ -151,6 +151,13 @@ export default function Footer ({ noLinks }) {
|
|||||||
<DarkModeIcon onClick={() => darkMode.toggle()} className='fill-grey theme' />
|
<DarkModeIcon onClick={() => darkMode.toggle()} className='fill-grey theme' />
|
||||||
<LnIcon onClick={toggleLightning} width={24} height={24} className='ml-2 fill-grey theme' />
|
<LnIcon onClick={toggleLightning} width={24} height={24} className='ml-2 fill-grey theme' />
|
||||||
</div>}
|
</div>}
|
||||||
|
<div className='mb-0' style={{ fontWeight: 500 }}>
|
||||||
|
<Link href='/rewards' passHref>
|
||||||
|
<a className='nav-link p-0 d-inline-flex'>
|
||||||
|
rewards
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<div className='mb-0' style={{ fontWeight: 500 }}>
|
<div className='mb-0' style={{ fontWeight: 500 }}>
|
||||||
<OverlayTrigger trigger='click' placement='top' overlay={AnalyticsPopover} rootClose>
|
<OverlayTrigger trigger='click' placement='top' overlay={AnalyticsPopover} rootClose>
|
||||||
<div className='nav-link p-0 d-inline-flex' style={{ cursor: 'pointer' }}>
|
<div className='nav-link p-0 d-inline-flex' style={{ cursor: 'pointer' }}>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import Layout from './layout'
|
import Layout from './layout'
|
||||||
import styles from './layout-center.module.css'
|
import styles from './layout-center.module.css'
|
||||||
|
|
||||||
export default function LayoutCenter ({ children, ...props }) {
|
export default function LayoutCenter ({ children, footerLinks, ...props }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<Layout noContain noFooterLinks {...props}>
|
<Layout noContain noFooterLinks={!footerLinks} {...props}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
142
pages/rewards.js
Normal file
142
pages/rewards.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { gql } from 'apollo-server-micro'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { Button, InputGroup, Modal } from 'react-bootstrap'
|
||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts'
|
||||||
|
import { getGetServerSideProps } from '../api/ssrApollo'
|
||||||
|
import { Form, Input, SubmitButton } from '../components/form'
|
||||||
|
import LayoutCenter from '../components/layout-center'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useMutation, useQuery } from '@apollo/client'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
const REWARDS = gql`
|
||||||
|
{
|
||||||
|
expectedRewards {
|
||||||
|
total
|
||||||
|
sources {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const getServerSideProps = getGetServerSideProps(REWARDS)
|
||||||
|
|
||||||
|
export default function Rewards ({ data: { expectedRewards: { total, sources } } }) {
|
||||||
|
const { data } = useQuery(REWARDS, { pollInterval: 1000 })
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
({ expectedRewards: { total, sources } } = data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutCenter footerLinks>
|
||||||
|
<h4 className='font-weight-bold text-muted text-center'>
|
||||||
|
<div>{total} sats to be rewarded today</div>
|
||||||
|
<Link href='http://localhost:3000/faq#how-do-i-earn-sats-on-stacker-news' passHref>
|
||||||
|
<a className='text-reset'><small><small><small>learn about rewards</small></small></small></a>
|
||||||
|
</Link>
|
||||||
|
</h4>
|
||||||
|
<div className='my-3 w-100'>
|
||||||
|
<GrowthPieChart data={sources} />
|
||||||
|
</div>
|
||||||
|
<DonateButton />
|
||||||
|
</LayoutCenter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
'var(--secondary)',
|
||||||
|
'var(--info)',
|
||||||
|
'var(--success)',
|
||||||
|
'var(--boost)',
|
||||||
|
'var(--grey)'
|
||||||
|
]
|
||||||
|
|
||||||
|
function GrowthPieChart ({ data }) {
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width='100%' height={250} minWidth={200}>
|
||||||
|
<PieChart margin={{ top: 5, right: 5, bottom: 5, left: 5 }}>
|
||||||
|
<Pie
|
||||||
|
dataKey='value'
|
||||||
|
isAnimationActive={false}
|
||||||
|
data={data}
|
||||||
|
cx='50%'
|
||||||
|
cy='50%'
|
||||||
|
outerRadius={80}
|
||||||
|
fill='var(--secondary)'
|
||||||
|
label
|
||||||
|
>
|
||||||
|
{
|
||||||
|
data.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index]} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DonateSchema = Yup.object({
|
||||||
|
amount: Yup.number().typeError('must be a number').required('required')
|
||||||
|
.positive('must be positive').integer('must be whole')
|
||||||
|
})
|
||||||
|
|
||||||
|
export function DonateButton () {
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
const inputRef = useRef(null)
|
||||||
|
const [donateToRewards] = useMutation(
|
||||||
|
gql`
|
||||||
|
mutation donateToRewards($sats: Int!) {
|
||||||
|
donateToRewards(sats: $sats)
|
||||||
|
}`)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}, [show])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setShow(true)}>DONATE TO REWARDS</Button>
|
||||||
|
<Modal
|
||||||
|
show={show}
|
||||||
|
onHide={() => {
|
||||||
|
setShow(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='modal-close' onClick={() => setShow(false)}>X</div>
|
||||||
|
<Modal.Body>
|
||||||
|
<Form
|
||||||
|
initial={{
|
||||||
|
amount: 1000
|
||||||
|
}}
|
||||||
|
schema={DonateSchema}
|
||||||
|
onSubmit={async ({ amount }) => {
|
||||||
|
await donateToRewards({
|
||||||
|
variables: {
|
||||||
|
sats: Number(amount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setShow(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label='amount'
|
||||||
|
name='amount'
|
||||||
|
innerRef={inputRef}
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||||
|
/>
|
||||||
|
<div className='d-flex'>
|
||||||
|
<SubmitButton variant='success' className='ml-auto mt-1 px-4' value='TIP'>donate</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -98,6 +98,16 @@ function Detail ({ fact }) {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (fact.type === 'donation') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={satusClass(fact.status)}>
|
||||||
|
You made a donation to daily rewards!
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!fact.item) {
|
if (!fact.item) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -145,6 +155,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor
|
|||||||
case 'invoice':
|
case 'invoice':
|
||||||
return `/${fact.type}s/${fact.factId}`
|
return `/${fact.type}s/${fact.factId}`
|
||||||
case 'earn':
|
case 'earn':
|
||||||
|
case 'donation':
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
return `/items/${fact.factId}`
|
return `/items/${fact.factId}`
|
||||||
|
@ -140,7 +140,8 @@ const COLORS = [
|
|||||||
'var(--info)',
|
'var(--info)',
|
||||||
'var(--success)',
|
'var(--success)',
|
||||||
'var(--boost)',
|
'var(--boost)',
|
||||||
'var(--theme-grey)'
|
'var(--theme-grey)',
|
||||||
|
'var(--danger)'
|
||||||
]
|
]
|
||||||
|
|
||||||
function GrowthAreaChart ({ data }) {
|
function GrowthAreaChart ({ data }) {
|
||||||
|
39
prisma/migrations/20221206213226_donate/migration.sql
Normal file
39
prisma/migrations/20221206213226_donate/migration.sql
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Donation" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"sats" INTEGER NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Donation" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION donate(sats INTEGER, user_id INTEGER)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
user_sats INTEGER;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT msats / 1000
|
||||||
|
INTO user_sats
|
||||||
|
FROM users WHERE id = user_id;
|
||||||
|
|
||||||
|
IF sats > user_sats THEN
|
||||||
|
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE users SET msats = msats - (sats * 1000) WHERE id = user_id;
|
||||||
|
|
||||||
|
INSERT INTO "Donate" (sats, "userId", created_at, updated_at)
|
||||||
|
VALUES (sats, user_id, now_utc(), now_utc());
|
||||||
|
|
||||||
|
RETURN sats;
|
||||||
|
END;
|
||||||
|
$$;
|
25
prisma/migrations/20221207212053_donate_func/migration.sql
Normal file
25
prisma/migrations/20221207212053_donate_func/migration.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION donate(sats INTEGER, user_id INTEGER)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
user_sats INTEGER;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT msats / 1000
|
||||||
|
INTO user_sats
|
||||||
|
FROM users WHERE id = user_id;
|
||||||
|
|
||||||
|
IF sats > user_sats THEN
|
||||||
|
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE users SET msats = msats - (sats * 1000) WHERE id = user_id;
|
||||||
|
|
||||||
|
INSERT INTO "Donate" (sats, "userId", created_at, updated_at)
|
||||||
|
VALUES (sats, user_id, now_utc(), now_utc());
|
||||||
|
|
||||||
|
RETURN sats;
|
||||||
|
END;
|
||||||
|
$$;
|
@ -68,12 +68,22 @@ model User {
|
|||||||
Earn Earn[]
|
Earn Earn[]
|
||||||
Upload Upload[] @relation(name: "Uploads")
|
Upload Upload[] @relation(name: "Uploads")
|
||||||
PollVote PollVote[]
|
PollVote PollVote[]
|
||||||
|
Donation Donation[]
|
||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([inviteId])
|
@@index([inviteId])
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Donation {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||||
|
sats Int
|
||||||
|
userId Int
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
}
|
||||||
|
|
||||||
model Upload {
|
model Upload {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
|
@ -10,13 +10,19 @@ function earn ({ models }) {
|
|||||||
console.log('running', name)
|
console.log('running', name)
|
||||||
|
|
||||||
// compute how much sn earned today
|
// compute how much sn earned today
|
||||||
const [{ sum }] = await models.$queryRaw`
|
let [{ sum }] = await models.$queryRaw`
|
||||||
SELECT sum("ItemAct".msats)
|
SELECT coalesce(sum("ItemAct".msats), 0) as sum
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
||||||
WHERE "ItemAct".act <> 'TIP'
|
WHERE "ItemAct".act <> 'TIP'
|
||||||
AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'`
|
AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'`
|
||||||
|
|
||||||
|
const [{ sum: donatedSum }] = await models.$queryRaw`
|
||||||
|
SELECT coalesce(sum(sats), 0) as sum
|
||||||
|
FROM "Donation"
|
||||||
|
WHERE created_at > now_utc() - INTERVAL '1 day'`
|
||||||
|
sum += donatedSum * 1000
|
||||||
|
|
||||||
/*
|
/*
|
||||||
How earnings work:
|
How earnings work:
|
||||||
1/3: top 21% posts over last 36 hours, scored on a relative basis
|
1/3: top 21% posts over last 36 hours, scored on a relative basis
|
||||||
|
Loading…
x
Reference in New Issue
Block a user