diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js
index ea28de4e..40e8c904 100644
--- a/api/resolvers/notifications.js
+++ b/api/resolvers/notifications.js
@@ -112,6 +112,13 @@ export default {
AND "maxBid" IS NOT NULL
AND status <> 'STOPPED'
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
OFFSET $3
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)
diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index 05d96229..9cc8fb61 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -160,6 +160,7 @@ export default {
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE "ItemAct"."userId" <> ${user.id} AND "ItemAct".act <> 'BOOST'
AND "Item"."userId" = ${user.id}`
+
return sum || 0
},
sats: async (user, args, { models, me }) => {
@@ -217,23 +218,33 @@ export default {
return true
}
- const where = {
- status: {
- not: 'STOPPED'
- },
- maxBid: {
- not: null
- },
- userId: user.id
+ const job = await models.item.findFirst({
+ where: {
+ status: {
+ not: 'STOPPED'
+ },
+ maxBid: {
+ not: null
+ },
+ userId: user.id,
+ statusUpdatedAt: {
+ gt: user.checkedNotesAt || new Date(0)
+ }
+ }
+ })
+ if (job) {
+ return true
}
- if (user.checkedNotesAt) {
- where.statusUpdatedAt = {
- gt: user.checkedNotesAt
+ const earn = await models.earn.findFirst({
+ where: {
+ userId: user.id,
+ createdAt: {
+ gt: user.checkedNotesAt || new Date(0)
+ }
}
- }
- const job = await models.item.findFirst({ where })
- if (job) {
+ })
+ if (earn) {
return true
}
diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js
index 3f3ca834..bd2b700d 100644
--- a/api/resolvers/wallet.js
+++ b/api/resolvers/wallet.js
@@ -97,6 +97,12 @@ export default {
WHERE "ItemAct"."userId" <> $1 AND "ItemAct".act <> 'BOOST'
AND "Item"."userId" = $1 AND "ItemAct".created_at <= $2
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')) {
diff --git a/api/typeDefs/notifications.js b/api/typeDefs/notifications.js
index 3899418f..e7dba19c 100644
--- a/api/typeDefs/notifications.js
+++ b/api/typeDefs/notifications.js
@@ -32,7 +32,13 @@ export default gql`
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 {
lastChecked: String
diff --git a/components/notifications.js b/components/notifications.js
index da364624..7d85431b 100644
--- a/components/notifications.js
+++ b/components/notifications.js
@@ -6,6 +6,7 @@ import { useRouter } from 'next/router'
import MoreFooter from './more-footer'
import Invite from './invite'
import { ignoreClick } from '../lib/clicks'
+import Link from 'next/link'
function Notification ({ n }) {
const router = useRouter()
@@ -13,6 +14,10 @@ function Notification ({ n }) {
{
+ if (n.__typename === 'Earn') {
+ return
+ }
+
if (ignoreClick(e)) {
return
}
@@ -48,33 +53,44 @@ function Notification ({ n }) {
>
)
- : (
- <>
- {n.__typename === 'Votification' &&
-
- your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats
- }
- {n.__typename === 'Mention' &&
-
- you were mentioned in
- }
- {n.__typename === 'JobChanged' &&
-
- {n.item.status === 'NOSATS'
- ? 'your job ran out of sats'
- : 'your job is active again'}
- }
-
- {n.item.maxBid
- ?
- : n.item.title
- ?
- : (
-
-
-
)}
-
- >)}
+ : n.__typename === 'Earn'
+ ? (
+ <>
+
+ you stacked {n.earnedSats} sats
+
+
+ SN distributes the sats it earns back to its best users daily. These sats come from
jobs, boost, and posting fees.
+
+ >
+ )
+ : (
+ <>
+ {n.__typename === 'Votification' &&
+
+ your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats
+ }
+ {n.__typename === 'Mention' &&
+
+ you were mentioned in
+ }
+ {n.__typename === 'JobChanged' &&
+
+ {n.item.status === 'NOSATS'
+ ? 'your job ran out of sats'
+ : 'your job is active again'}
+ }
+
+ {n.item.maxBid
+ ?
+ : n.item.title
+ ?
+ : (
+
+
+
)}
+
+ >)}
)
}
diff --git a/fragments/notifications.js b/fragments/notifications.js
index f2fbb755..3ef3871a 100644
--- a/fragments/notifications.js
+++ b/fragments/notifications.js
@@ -47,6 +47,10 @@ export const NOTIFICATIONS = gql`
...ItemFields
}
}
+ ... on Earn {
+ sortTime
+ earnedSats
+ }
}
}
} `
diff --git a/pages/satistics.js b/pages/satistics.js
index 1b79213f..fbe1c692 100644
--- a/pages/satistics.js
+++ b/pages/satistics.js
@@ -88,6 +88,15 @@ function Satus ({ status }) {
}
function Detail ({ fact }) {
+ if (fact.type === 'earn') {
+ return (
+ <>
+
+ SN gives the sats it earns back to its best users daily. These sats come from
jobs, boost, and posting fees.
+
+ >
+ )
+ }
if (!fact.item) {
return (
<>
diff --git a/prisma/migrations/20220316212238_earn/migration.sql b/prisma/migrations/20220316212238_earn/migration.sql
new file mode 100644
index 00000000..609ecfa2
--- /dev/null
+++ b/prisma/migrations/20220316212238_earn/migration.sql
@@ -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;
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 98d044ed..9417d6e4 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -41,11 +41,25 @@ model User {
upvotePopover Boolean @default(false)
tipPopover Boolean @default(false)
+ Earn Earn[]
@@index([createdAt])
@@index([inviteId])
@@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 {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
diff --git a/worker/earn.js b/worker/earn.js
new file mode 100644
index 00000000..3ac5d01a
--- /dev/null
+++ b/worker/earn.js
@@ -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 }
diff --git a/worker/index.js b/worker/index.js
index d163266d..3399acd5 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -6,6 +6,7 @@ const { checkInvoice, checkWithdrawal } = require('./wallet')
const { repin } = require('./repin')
const { trust } = require('./trust')
const { auction } = require('./auction')
+const { earn } = require('./earn')
const { ApolloClient, HttpLink, InMemoryCache } = require('@apollo/client')
const { indexItem, indexAllItems } = require('./search')
const fetch = require('cross-fetch')
@@ -43,6 +44,7 @@ async function work () {
await boss.work('indexItem', indexItem(args))
await boss.work('indexAllItems', indexAllItems(args))
await boss.work('auction', auction(args))
+ await boss.work('earn', earn(args))
console.log('working jobs')
}