earning
This commit is contained in:
parent
8c513e93c8
commit
5ff856d061
|
@ -112,6 +112,13 @@ export default {
|
||||||
AND "maxBid" IS NOT NULL
|
AND "maxBid" IS NOT NULL
|
||||||
AND status <> 'STOPPED'
|
AND status <> 'STOPPED'
|
||||||
AND "statusUpdatedAt" <= $2)
|
AND "statusUpdatedAt" <= $2)
|
||||||
|
UNION ALL
|
||||||
|
(SELECT "Earn".id::text, "Earn".created_at AS "sortTime", FLOOR(msats / 1000) as "earnedSats",
|
||||||
|
'Earn' AS type
|
||||||
|
FROM "Earn"
|
||||||
|
WHERE "Earn"."userId" = $1 AND
|
||||||
|
FLOOR(msats / 1000) > 0
|
||||||
|
AND created_at <= $2)
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
OFFSET $3
|
OFFSET $3
|
||||||
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)
|
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)
|
||||||
|
|
|
@ -160,6 +160,7 @@ export default {
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
||||||
WHERE "ItemAct"."userId" <> ${user.id} AND "ItemAct".act <> 'BOOST'
|
WHERE "ItemAct"."userId" <> ${user.id} AND "ItemAct".act <> 'BOOST'
|
||||||
AND "Item"."userId" = ${user.id}`
|
AND "Item"."userId" = ${user.id}`
|
||||||
|
|
||||||
return sum || 0
|
return sum || 0
|
||||||
},
|
},
|
||||||
sats: async (user, args, { models, me }) => {
|
sats: async (user, args, { models, me }) => {
|
||||||
|
@ -217,23 +218,33 @@ export default {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const where = {
|
const job = await models.item.findFirst({
|
||||||
status: {
|
where: {
|
||||||
not: 'STOPPED'
|
status: {
|
||||||
},
|
not: 'STOPPED'
|
||||||
maxBid: {
|
},
|
||||||
not: null
|
maxBid: {
|
||||||
},
|
not: null
|
||||||
userId: user.id
|
},
|
||||||
|
userId: user.id,
|
||||||
|
statusUpdatedAt: {
|
||||||
|
gt: user.checkedNotesAt || new Date(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (job) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.checkedNotesAt) {
|
const earn = await models.earn.findFirst({
|
||||||
where.statusUpdatedAt = {
|
where: {
|
||||||
gt: user.checkedNotesAt
|
userId: user.id,
|
||||||
|
createdAt: {
|
||||||
|
gt: user.checkedNotesAt || new Date(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
const job = await models.item.findFirst({ where })
|
if (earn) {
|
||||||
if (job) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,12 @@ export default {
|
||||||
WHERE "ItemAct"."userId" <> $1 AND "ItemAct".act <> 'BOOST'
|
WHERE "ItemAct"."userId" <> $1 AND "ItemAct".act <> 'BOOST'
|
||||||
AND "Item"."userId" = $1 AND "ItemAct".created_at <= $2
|
AND "Item"."userId" = $1 AND "ItemAct".created_at <= $2
|
||||||
GROUP BY "Item".id)`)
|
GROUP BY "Item".id)`)
|
||||||
|
queries.push(
|
||||||
|
`(SELECT ('earn' || "Earn".id) as id, "Earn".id as "factId", NULL as bolt11,
|
||||||
|
created_at as "createdAt", msats,
|
||||||
|
0 as "msatsFee", NULL as status, 'earn' as type
|
||||||
|
FROM "Earn"
|
||||||
|
WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include.has('spent')) {
|
if (include.has('spent')) {
|
||||||
|
|
|
@ -32,7 +32,13 @@ export default gql`
|
||||||
sortTime: String!
|
sortTime: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
union Notification = Reply | Votification | Mention | Invitification | JobChanged
|
type Earn {
|
||||||
|
earnedSats: Int!
|
||||||
|
sortTime: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
union Notification = Reply | Votification | Mention
|
||||||
|
| Invitification | JobChanged | Earn
|
||||||
|
|
||||||
type Notifications {
|
type Notifications {
|
||||||
lastChecked: String
|
lastChecked: String
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useRouter } from 'next/router'
|
||||||
import MoreFooter from './more-footer'
|
import MoreFooter from './more-footer'
|
||||||
import Invite from './invite'
|
import Invite from './invite'
|
||||||
import { ignoreClick } from '../lib/clicks'
|
import { ignoreClick } from '../lib/clicks'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
function Notification ({ n }) {
|
function Notification ({ n }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -13,6 +14,10 @@ function Notification ({ n }) {
|
||||||
<div
|
<div
|
||||||
className='clickToContext'
|
className='clickToContext'
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
|
if (n.__typename === 'Earn') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (ignoreClick(e)) {
|
if (ignoreClick(e)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -48,33 +53,44 @@ function Notification ({ n }) {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (
|
: n.__typename === 'Earn'
|
||||||
<>
|
? (
|
||||||
{n.__typename === 'Votification' &&
|
<>
|
||||||
<small className='font-weight-bold text-success ml-2'>
|
<div className='font-weight-bold text-boost ml-2'>
|
||||||
your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats
|
you stacked {n.earnedSats} sats
|
||||||
</small>}
|
</div>
|
||||||
{n.__typename === 'Mention' &&
|
<div className='ml-4'>
|
||||||
<small className='font-weight-bold text-info ml-2'>
|
SN distributes the sats it earns back to its best users daily. These sats come from <Link href='/~jobs' passHref><a>jobs</a></Link>, boost, and posting fees.
|
||||||
you were mentioned in
|
</div>
|
||||||
</small>}
|
</>
|
||||||
{n.__typename === 'JobChanged' &&
|
)
|
||||||
<small className={`font-weight-bold text-${n.item.status === 'NOSATS' ? 'danger' : 'success'} ml-1`}>
|
: (
|
||||||
{n.item.status === 'NOSATS'
|
<>
|
||||||
? 'your job ran out of sats'
|
{n.__typename === 'Votification' &&
|
||||||
: 'your job is active again'}
|
<small className='font-weight-bold text-success ml-2'>
|
||||||
</small>}
|
your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats
|
||||||
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
|
</small>}
|
||||||
{n.item.maxBid
|
{n.__typename === 'Mention' &&
|
||||||
? <ItemJob item={n.item} />
|
<small className='font-weight-bold text-info ml-2'>
|
||||||
: n.item.title
|
you were mentioned in
|
||||||
? <Item item={n.item} />
|
</small>}
|
||||||
: (
|
{n.__typename === 'JobChanged' &&
|
||||||
<div className='pb-2'>
|
<small className={`font-weight-bold text-${n.item.status === 'NOSATS' ? 'danger' : 'success'} ml-1`}>
|
||||||
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
{n.item.status === 'NOSATS'
|
||||||
</div>)}
|
? 'your job ran out of sats'
|
||||||
</div>
|
: 'your job is active again'}
|
||||||
</>)}
|
</small>}
|
||||||
|
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
|
||||||
|
{n.item.maxBid
|
||||||
|
? <ItemJob item={n.item} />
|
||||||
|
: n.item.title
|
||||||
|
? <Item item={n.item} />
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
||||||
|
</div>)}
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,10 @@ export const NOTIFICATIONS = gql`
|
||||||
...ItemFields
|
...ItemFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
... on Earn {
|
||||||
|
sortTime
|
||||||
|
earnedSats
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} `
|
} `
|
||||||
|
|
|
@ -88,6 +88,15 @@ function Satus ({ status }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Detail ({ fact }) {
|
function Detail ({ fact }) {
|
||||||
|
if (fact.type === 'earn') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={satusClass(fact.status)}>
|
||||||
|
SN gives the sats it earns back to its best users daily. These sats come from <Link href='/~jobs' passHref><a>jobs</a></Link>, boost, and posting fees.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
if (!fact.item) {
|
if (!fact.item) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Earn" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"msats" INTEGER NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Earn.created_at_index" ON "Earn"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Earn.userId_index" ON "Earn"("userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Earn" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- charge the user for the auction item
|
||||||
|
CREATE OR REPLACE FUNCTION earn(user_id INTEGER, earn_msats INTEGER) RETURNS void AS $$
|
||||||
|
DECLARE
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
-- insert into earn
|
||||||
|
INSERT INTO "Earn" (msats, "userId") VALUES (earn_msats, user_id);
|
||||||
|
-- give the user the sats
|
||||||
|
UPDATE users SET msats = msats + earn_msats WHERE id = user_id;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
|
@ -41,11 +41,25 @@ model User {
|
||||||
upvotePopover Boolean @default(false)
|
upvotePopover Boolean @default(false)
|
||||||
tipPopover Boolean @default(false)
|
tipPopover Boolean @default(false)
|
||||||
|
|
||||||
|
Earn Earn[]
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([inviteId])
|
@@index([inviteId])
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Earn {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||||
|
|
||||||
|
msats Int
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId Int
|
||||||
|
|
||||||
|
@@index([createdAt])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
model LnAuth {
|
model LnAuth {
|
||||||
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")
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
const serialize = require('../api/resolvers/serial')
|
||||||
|
|
||||||
|
// TODO: use a weekly trust measure or make trust decay
|
||||||
|
function earn ({ models }) {
|
||||||
|
return async function ({ name }) {
|
||||||
|
console.log('running', name)
|
||||||
|
|
||||||
|
// compute how much sn earned today
|
||||||
|
const [{ sum }] = await models.$queryRaw`
|
||||||
|
SELECT sum("ItemAct".sats)
|
||||||
|
FROM "ItemAct"
|
||||||
|
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
||||||
|
WHERE ("ItemAct".act in ('BOOST', 'STREAM')
|
||||||
|
OR ("ItemAct".act = 'VOTE' AND "Item"."userId" = "ItemAct"."userId"))
|
||||||
|
AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'`
|
||||||
|
|
||||||
|
// calculate the total trust
|
||||||
|
const { sum: { trust } } = await models.user.aggregate({
|
||||||
|
sum: {
|
||||||
|
trust: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// get earners { id, earnings }
|
||||||
|
const earners = await models.$queryRaw(`
|
||||||
|
SELECT id, FLOOR(${sum} * (trust/${trust}) * 1000) as earnings
|
||||||
|
FROM users
|
||||||
|
WHERE trust > 0`)
|
||||||
|
|
||||||
|
// for each earner, serialize earnings
|
||||||
|
// we do this for each earner because we don't need to serialize
|
||||||
|
// all earner updates together
|
||||||
|
earners.forEach(async earner => {
|
||||||
|
if (earner.earnings > 0) {
|
||||||
|
await serialize(models,
|
||||||
|
models.$executeRaw`SELECT earn(${earner.id}, ${earner.earnings})`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('done', name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { earn }
|
|
@ -6,6 +6,7 @@ const { checkInvoice, checkWithdrawal } = require('./wallet')
|
||||||
const { repin } = require('./repin')
|
const { repin } = require('./repin')
|
||||||
const { trust } = require('./trust')
|
const { trust } = require('./trust')
|
||||||
const { auction } = require('./auction')
|
const { auction } = require('./auction')
|
||||||
|
const { earn } = require('./earn')
|
||||||
const { ApolloClient, HttpLink, InMemoryCache } = require('@apollo/client')
|
const { ApolloClient, HttpLink, InMemoryCache } = require('@apollo/client')
|
||||||
const { indexItem, indexAllItems } = require('./search')
|
const { indexItem, indexAllItems } = require('./search')
|
||||||
const fetch = require('cross-fetch')
|
const fetch = require('cross-fetch')
|
||||||
|
@ -43,6 +44,7 @@ async function work () {
|
||||||
await boss.work('indexItem', indexItem(args))
|
await boss.work('indexItem', indexItem(args))
|
||||||
await boss.work('indexAllItems', indexAllItems(args))
|
await boss.work('indexAllItems', indexAllItems(args))
|
||||||
await boss.work('auction', auction(args))
|
await boss.work('auction', auction(args))
|
||||||
|
await boss.work('earn', earn(args))
|
||||||
|
|
||||||
console.log('working jobs')
|
console.log('working jobs')
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue