From 32651bea92804d2ea5c2b822fd8ba8036e9fc3d2 Mon Sep 17 00:00:00 2001 From: keyan Date: Sun, 6 Nov 2022 14:39:23 -0600 Subject: [PATCH 1/4] more indices --- .../20221106203216_item_indices/migration.sql | 14 ++++++++++++++ prisma/schema.prisma | 4 ++++ 2 files changed, 18 insertions(+) create mode 100644 prisma/migrations/20221106203216_item_indices/migration.sql diff --git a/prisma/migrations/20221106203216_item_indices/migration.sql b/prisma/migrations/20221106203216_item_indices/migration.sql new file mode 100644 index 00000000..7cd5b493 --- /dev/null +++ b/prisma/migrations/20221106203216_item_indices/migration.sql @@ -0,0 +1,14 @@ +-- CreateIndex +CREATE INDEX "Item.weightedVotes_index" ON "Item"("weightedVotes"); + +-- CreateIndex +CREATE INDEX "Item.weightedDownVotes_index" ON "Item"("weightedDownVotes"); + +-- CreateIndex +CREATE INDEX "Item.bio_index" ON "Item"("bio"); + +-- CreateIndex +CREATE INDEX "Item.freebie_index" ON "Item"("freebie"); + +-- CreateIndex +CREATE INDEX "Item.sumVotes_index" ON "Item"(("weightedVotes" - "weightedDownVotes")); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8b4eeba8..1ad3d5f8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -224,6 +224,10 @@ model Item { PollOption PollOption[] PollVote PollVote[] + @@index([weightedVotes]) + @@index([weightedDownVotes]) + @@index([bio]) + @@index([freebie]) @@index([createdAt]) @@index([userId]) @@index([parentId]) From f1a4e9c682286c8a43572c6672c57da0f57f6a35 Mon Sep 17 00:00:00 2001 From: keyan Date: Mon, 7 Nov 2022 17:31:29 -0600 Subject: [PATCH 2/4] poll for notifications less, don't retry gql --- api/resolvers/user.js | 220 +++++++++++++++++++++--------------------- api/ssrApollo.js | 4 +- api/typeDefs/user.js | 2 +- components/header.js | 10 +- fragments/users.js | 30 ------ lib/apollo.js | 14 +-- 6 files changed, 129 insertions(+), 151 deletions(-) diff --git a/api/resolvers/user.js b/api/resolvers/user.js index b4163775..f0e5dcec 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -149,6 +149,118 @@ export default { users } }, + hasNewNotes: async (parent, args, { me, models }) => { + if (!me) { + return false + } + const user = await models.user.findUnique({ where: { id: me.id } }) + const lastChecked = user.checkedNotesAt || new Date(0) + + // check if any votes have been cast for them since checkedNotesAt + if (user.noteItemSats) { + const votes = await models.$queryRaw(` + SELECT "ItemAct".id, "ItemAct".created_at + FROM "Item" + JOIN "ItemAct" on "ItemAct"."itemId" = "Item".id + WHERE "ItemAct"."userId" <> $1 + AND "ItemAct".created_at > $2 + AND "Item"."userId" = $1 + AND "ItemAct".act IN ('VOTE', 'TIP') + LIMIT 1`, me.id, lastChecked) + if (votes.length > 0) { + return true + } + } + + // check if they have any replies since checkedNotesAt + const newReplies = await models.$queryRaw(` + SELECT "Item".id, "Item".created_at + FROM "Item" + JOIN "Item" p ON ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'} + WHERE p."userId" = $1 + AND "Item".created_at > $2 AND "Item"."userId" <> $1 + ${await filterClause(me, models)} + LIMIT 1`, me.id, lastChecked) + if (newReplies.length > 0) { + return true + } + + // check if they have any mentions since checkedNotesAt + if (user.noteMentions) { + const newMentions = await models.$queryRaw(` + SELECT "Item".id, "Item".created_at + FROM "Mention" + JOIN "Item" ON "Mention"."itemId" = "Item".id + WHERE "Mention"."userId" = $1 + AND "Mention".created_at > $2 + AND "Item"."userId" <> $1 + LIMIT 1`, me.id, lastChecked) + if (newMentions.length > 0) { + return true + } + } + + const job = await models.item.findFirst({ + where: { + maxBid: { + not: null + }, + userId: me.id, + statusUpdatedAt: { + gt: lastChecked + } + } + }) + if (job) { + return true + } + + if (user.noteEarning) { + const earn = await models.earn.findFirst({ + where: { + userId: me.id, + createdAt: { + gt: lastChecked + }, + msats: { + gte: 1000 + } + } + }) + if (earn) { + return true + } + } + + if (user.noteDeposits) { + const invoice = await models.invoice.findFirst({ + where: { + userId: me.id, + confirmedAt: { + gt: lastChecked + } + } + }) + if (invoice) { + return true + } + } + + // check if new invites have been redeemed + if (user.noteInvites) { + const newInvitees = await models.$queryRaw(` + SELECT "Invite".id + FROM users JOIN "Invite" on users."inviteId" = "Invite".id + WHERE "Invite"."userId" = $1 + AND users.created_at > $2 + LIMIT 1`, me.id, lastChecked) + if (newInvitees.length > 0) { + return true + } + } + + return false + }, searchUsers: async (parent, { q, limit, similarity }, { models }) => { return await models.$queryRaw` SELECT * FROM users where id > 615 AND SIMILARITY(name, ${q}) > ${Number(similarity) || 0.1} ORDER BY SIMILARITY(name, ${q}) DESC LIMIT ${Number(limit) || 5}` @@ -362,114 +474,6 @@ export default { }).invites({ take: 1 }) return invites.length > 0 - }, - hasNewNotes: async (user, args, { me, models }) => { - const lastChecked = user.checkedNotesAt || new Date(0) - - // check if any votes have been cast for them since checkedNotesAt - if (user.noteItemSats) { - const votes = await models.$queryRaw(` - SELECT "ItemAct".id, "ItemAct".created_at - FROM "Item" - JOIN "ItemAct" on "ItemAct"."itemId" = "Item".id - WHERE "ItemAct"."userId" <> $1 - AND "ItemAct".created_at > $2 - AND "Item"."userId" = $1 - AND "ItemAct".act IN ('VOTE', 'TIP') - LIMIT 1`, me.id, lastChecked) - if (votes.length > 0) { - return true - } - } - - // check if they have any replies since checkedNotesAt - const newReplies = await models.$queryRaw(` - SELECT "Item".id, "Item".created_at - FROM "Item" - JOIN "Item" p ON ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'} - WHERE p."userId" = $1 - AND "Item".created_at > $2 AND "Item"."userId" <> $1 - ${await filterClause(me, models)} - LIMIT 1`, me.id, lastChecked) - if (newReplies.length > 0) { - return true - } - - // check if they have any mentions since checkedNotesAt - if (user.noteMentions) { - const newMentions = await models.$queryRaw(` - SELECT "Item".id, "Item".created_at - FROM "Mention" - JOIN "Item" ON "Mention"."itemId" = "Item".id - WHERE "Mention"."userId" = $1 - AND "Mention".created_at > $2 - AND "Item"."userId" <> $1 - LIMIT 1`, me.id, lastChecked) - if (newMentions.length > 0) { - return true - } - } - - const job = await models.item.findFirst({ - where: { - maxBid: { - not: null - }, - userId: me.id, - statusUpdatedAt: { - gt: lastChecked - } - } - }) - if (job) { - return true - } - - if (user.noteEarning) { - const earn = await models.earn.findFirst({ - where: { - userId: me.id, - createdAt: { - gt: lastChecked - }, - msats: { - gte: 1000 - } - } - }) - if (earn) { - return true - } - } - - if (user.noteDeposits) { - const invoice = await models.invoice.findFirst({ - where: { - userId: me.id, - confirmedAt: { - gt: lastChecked - } - } - }) - if (invoice) { - return true - } - } - - // check if new invites have been redeemed - if (user.noteInvites) { - const newInvitees = await models.$queryRaw(` - SELECT "Invite".id - FROM users JOIN "Invite" on users."inviteId" = "Invite".id - WHERE "Invite"."userId" = $1 - AND users.created_at > $2 - LIMIT 1`, me.id, lastChecked) - if (newInvitees.length > 0) { - return true - } - } - - return false } } } diff --git a/api/ssrApollo.js b/api/ssrApollo.js index c13792d5..3931c76d 100644 --- a/api/ssrApollo.js +++ b/api/ssrApollo.js @@ -8,7 +8,7 @@ import models from './models' import { print } from 'graphql' import lnd from './lnd' import search from './search' -import { ME_SSR } from '../fragments/users' +import { ME } from '../fragments/users' import { getPrice } from '../components/price' export default async function getSSRApolloClient (req, me = null) { @@ -40,7 +40,7 @@ export function getGetServerSideProps (query, variables = null, notFoundFunc, re const client = await getSSRApolloClient(req) const { data: { me } } = await client.query({ - query: ME_SSR + query: ME }) const price = await getPrice(me?.fiatCurrency) diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index f2c260c7..c6fffb6a 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -9,6 +9,7 @@ export default gql` nameAvailable(name: String!): Boolean! topUsers(cursor: String, when: String, sort: String): Users searchUsers(q: String!, limit: Int, similarity: Float): [User!]! + hasNewNotes: Boolean! } type Users { @@ -46,7 +47,6 @@ export default gql` spent(when: String): Int! freePosts: Int! freeComments: Int! - hasNewNotes: Boolean! hasInvites: Boolean! tipDefault: Int! fiatCurrency: String! diff --git a/components/header.js b/components/header.js index 1580dcfb..6e34a2b2 100644 --- a/components/header.js +++ b/components/header.js @@ -35,7 +35,11 @@ export default function Header ({ sub }) { subLatestPost(name: $name) } `, { variables: { name: 'jobs' }, pollInterval: 600000, fetchPolicy: 'network-only' }) - + const { data: hasNewNotes } = useQuery(gql` + { + hasNewNotes + } + `, { pollInterval: 30000, fetchPolicy: 'cache-and-network' }) const [lastCheckedJobs, setLastCheckedJobs] = useState(new Date().getTime()) useEffect(() => { if (me) { @@ -53,12 +57,12 @@ export default function Header ({ sub }) { return (
- + - {me?.hasNewNotes && + {hasNewNotes?.hasNewNotes && {' '} } diff --git a/fragments/users.js b/fragments/users.js index db4e1e16..5b98bc98 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -3,36 +3,6 @@ import { COMMENT_FIELDS } from './comments' import { ITEM_FIELDS, ITEM_WITH_COMMENTS } from './items' export const ME = gql` - { - me { - id - name - sats - stacked - freePosts - freeComments - hasNewNotes - tipDefault - fiatCurrency - bioId - hasInvites - upvotePopover - tipPopover - noteItemSats - noteEarning - noteAllDescendants - noteMentions - noteDeposits - noteInvites - noteJobIndicator - hideInvoiceDesc - wildWestMode - greeterMode - lastCheckedJobs - } - }` - -export const ME_SSR = gql` { me { id diff --git a/lib/apollo.js b/lib/apollo.js index 5829c368..291e3d2f 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -1,11 +1,11 @@ -import { ApolloClient, InMemoryCache, from, HttpLink } from '@apollo/client' +import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client' import { decodeCursor, LIMIT } from './cursor' -import { RetryLink } from '@apollo/client/link/retry' +// import { RetryLink } from '@apollo/client/link/retry' -const additiveLink = from([ - new RetryLink(), - new HttpLink({ uri: '/api/graphql' }) -]) +// const additiveLink = from([ +// new RetryLink(), +// new HttpLink({ uri: '/api/graphql' }) +// ]) function isFirstPage (cursor, existingThings) { if (cursor) { @@ -20,7 +20,7 @@ function isFirstPage (cursor, existingThings) { export default function getApolloClient () { global.apolloClient ||= new ApolloClient({ - link: additiveLink, + link: new HttpLink({ uri: '/api/graphql' }), cache: new InMemoryCache({ typePolicies: { Query: { From e79f39274b9e4187b4fe1407c9c3f7ab31c87b93 Mon Sep 17 00:00:00 2001 From: keyan Date: Tue, 8 Nov 2022 13:24:15 -0600 Subject: [PATCH 3/4] puppeteer config --- .puppeteerrc.cjs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .puppeteerrc.cjs diff --git a/.puppeteerrc.cjs b/.puppeteerrc.cjs new file mode 100644 index 00000000..a7021076 --- /dev/null +++ b/.puppeteerrc.cjs @@ -0,0 +1,9 @@ +const {join} = require('path'); + +/** + * @type {import("puppeteer").Configuration} + */ +module.exports = { + // Changes the cache location for Puppeteer. + cacheDirectory: join(__dirname, '.cache', 'puppeteer'), +}; \ No newline at end of file From bcdd5410a3f24534e6b29856c3cfd94b33b2f6a9 Mon Sep 17 00:00:00 2001 From: keyan Date: Tue, 15 Nov 2022 14:51:55 -0600 Subject: [PATCH 4/4] sats to msats --- api/resolvers/growth.js | 36 +- api/resolvers/item.js | 30 +- api/resolvers/notifications.js | 2 +- api/resolvers/user.js | 25 +- api/resolvers/wallet.js | 23 +- api/typeDefs/wallet.js | 10 +- components/invoice.js | 2 +- components/text.js | 42 +- fragments/wallet.js | 6 +- lib/format.js | 7 + pages/_app.js | 10 +- pages/invoices/[id].js | 5 +- pages/satistics.js | 2 +- .../20221110190205_msats_bigint/migration.sql | 43 ++ .../20221110224543_msats_funcs/migration.sql | 376 ++++++++++++++++++ prisma/schema.prisma | 30 +- worker/earn.js | 9 +- 17 files changed, 546 insertions(+), 112 deletions(-) create mode 100644 prisma/migrations/20221110190205_msats_bigint/migration.sql create mode 100644 prisma/migrations/20221110224543_msats_funcs/migration.sql diff --git a/api/resolvers/growth.js b/api/resolvers/growth.js index 3b03b385..7793a759 100644 --- a/api/resolvers/growth.js +++ b/api/resolvers/growth.js @@ -33,10 +33,10 @@ export default { return await models.$queryRaw( `SELECT date_trunc('month', "ItemAct".created_at) AS time, - sum(CASE WHEN act = 'STREAM' THEN "ItemAct".sats ELSE 0 END) as jobs, - sum(CASE WHEN act IN ('VOTE', 'POLL') AND "Item"."userId" = "ItemAct"."userId" THEN "ItemAct".sats ELSE 0 END) as fees, - sum(CASE WHEN act = 'BOOST' THEN "ItemAct".sats ELSE 0 END) as boost, - sum(CASE WHEN act = 'TIP' THEN "ItemAct".sats ELSE 0 END) as tips + floor(sum(CASE WHEN act = 'STREAM' THEN "ItemAct".msats ELSE 0 END)/1000) as jobs, + floor(sum(CASE WHEN act IN ('VOTE', 'POLL') AND "Item"."userId" = "ItemAct"."userId" THEN "ItemAct".msats ELSE 0 END)/1000) as fees, + floor(sum(CASE WHEN act = 'BOOST' THEN "ItemAct".msats ELSE 0 END)/1000) as boost, + floor(sum(CASE WHEN act = 'TIP' THEN "ItemAct".msats ELSE 0 END)/1000) as tips FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id WHERE date_trunc('month', now_utc()) <> date_trunc('month', "ItemAct".created_at) @@ -60,17 +60,17 @@ export default { }, stackedGrowth: async (parent, args, { models }) => { return await models.$queryRaw( - `SELECT time, sum(airdrop) as rewards, sum(post) as posts, sum(comment) as comments + `SELECT time, floor(sum(airdrop)/1000) as rewards, floor(sum(post)/1000) as posts, floor(sum(comment)/1000) as comments FROM ((SELECT date_trunc('month', "ItemAct".created_at) AS time, 0 as airdrop, - CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".sats END as comment, - CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".sats ELSE 0 END as post + CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment, + CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId" WHERE date_trunc('month', now_utc()) <> date_trunc('month', "ItemAct".created_at) AND "ItemAct".act IN ('VOTE', 'TIP')) UNION ALL - (SELECT date_trunc('month', created_at) AS time, msats / 1000 as airdrop, 0 as post, 0 as comment + (SELECT date_trunc('month', created_at) AS time, msats as airdrop, 0 as post, 0 as comment FROM "Earn" WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at))) u GROUP BY time @@ -121,10 +121,10 @@ export default { spentWeekly: async (parent, args, { models }) => { const [stats] = await models.$queryRaw( `SELECT json_build_array( - json_build_object('name', 'jobs', 'value', sum(CASE WHEN act = 'STREAM' THEN "ItemAct".sats ELSE 0 END)), - json_build_object('name', 'fees', 'value', sum(CASE WHEN act in ('VOTE', 'POLL') AND "Item"."userId" = "ItemAct"."userId" THEN "ItemAct".sats ELSE 0 END)), - json_build_object('name', 'boost', 'value', sum(CASE WHEN act = 'BOOST' THEN "ItemAct".sats ELSE 0 END)), - json_build_object('name', 'tips', 'value', sum(CASE WHEN act = 'TIP' THEN "ItemAct".sats ELSE 0 END))) as array + json_build_object('name', 'jobs', 'value', floor(sum(CASE WHEN act = 'STREAM' THEN "ItemAct".msats ELSE 0 END)/1000)), + json_build_object('name', 'fees', 'value', floor(sum(CASE WHEN act in ('VOTE', 'POLL') AND "Item"."userId" = "ItemAct"."userId" THEN "ItemAct".msats ELSE 0 END)/1000)), + json_build_object('name', 'boost', 'value',floor(sum(CASE WHEN act = 'BOOST' THEN "ItemAct".msats ELSE 0 END)/1000)), + json_build_object('name', 'tips', 'value', floor(sum(CASE WHEN act = 'TIP' THEN "ItemAct".msats ELSE 0 END)/1000))) as array FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id WHERE "ItemAct".created_at >= now_utc() - interval '1 week'`) @@ -134,20 +134,20 @@ export default { stackedWeekly: async (parent, args, { models }) => { const [stats] = await models.$queryRaw( `SELECT json_build_array( - json_build_object('name', 'rewards', 'value', sum(airdrop)), - json_build_object('name', 'posts', 'value', sum(post)), - json_build_object('name', 'comments', 'value', sum(comment)) + json_build_object('name', 'rewards', 'value', floor(sum(airdrop)/1000)), + json_build_object('name', 'posts', 'value', floor(sum(post)/1000)), + json_build_object('name', 'comments', 'value', floor(sum(comment)/1000)) ) as array FROM ((SELECT 0 as airdrop, - CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".sats END as comment, - CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".sats ELSE 0 END as post + CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment, + CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId" WHERE "ItemAct".created_at >= now_utc() - interval '1 week' AND "ItemAct".act IN ('VOTE', 'TIP')) UNION ALL - (SELECT msats / 1000 as airdrop, 0 as post, 0 as comment + (SELECT msats as airdrop, 0 as post, 0 as comment FROM "Earn" WHERE created_at >= now_utc() - interval '1 week')) u`) diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 302f1441..550d3eda 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -8,6 +8,7 @@ import { BOOST_MIN, ITEM_SPAM_INTERVAL, MAX_POLL_NUM_CHOICES, MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST } from '../../lib/constants' +import { msatsToSats } from '../../lib/format' async function comments (me, models, id, sort) { let orderBy @@ -74,7 +75,7 @@ async function topOrderClause (sort, me, models) { case 'comments': return 'ORDER BY ncomments DESC' case 'sats': - return 'ORDER BY sats DESC' + return 'ORDER BY msats DESC' default: return await topOrderByWeightedSats(me, models) } @@ -690,6 +691,12 @@ export default { } }, Item: { + sats: async (item, args, { models }) => { + return msatsToSats(item.msats) + }, + commentSats: async (item, args, { models }) => { + return msatsToSats(item.commentMsats) + }, isJob: async (item, args, { models }) => { return item.subName === 'jobs' }, @@ -771,10 +778,7 @@ export default { return comments(me, models, item.id, 'hot') }, upvotes: async (item, args, { models }) => { - const { sum: { sats } } = await models.itemAct.aggregate({ - sum: { - sats: true - }, + const count = await models.itemAct.count({ where: { itemId: Number(item.id), userId: { @@ -784,12 +788,12 @@ export default { } }) - return sats || 0 + return count }, boost: async (item, args, { models }) => { - const { sum: { sats } } = await models.itemAct.aggregate({ + const { sum: { msats } } = await models.itemAct.aggregate({ sum: { - sats: true + msats: true }, where: { itemId: Number(item.id), @@ -797,7 +801,7 @@ export default { } }) - return sats || 0 + return (msats && msatsToSats(msats)) || 0 }, wvotes: async (item) => { return item.weightedVotes - item.weightedDownVotes @@ -805,9 +809,9 @@ export default { meSats: async (item, args, { me, models }) => { if (!me) return 0 - const { sum: { sats } } = await models.itemAct.aggregate({ + const { sum: { msats } } = await models.itemAct.aggregate({ sum: { - sats: true + msats: true }, where: { itemId: Number(item.id), @@ -823,7 +827,7 @@ export default { } }) - return sats || 0 + return (msats && msatsToSats(msats)) || 0 }, meDontLike: async (item, args, { me, models }) => { if (!me) return false @@ -1010,7 +1014,7 @@ export const SELECT = "Item".text, "Item".url, "Item"."userId", "Item"."fwdUserId", "Item"."parentId", "Item"."pinId", "Item"."maxBid", "Item".company, "Item".location, "Item".remote, "Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost", - "Item".sats, "Item".ncomments, "Item"."commentSats", "Item"."lastCommentAt", "Item"."weightedVotes", + "Item".msats, "Item".ncomments, "Item"."commentMsats", "Item"."lastCommentAt", "Item"."weightedVotes", "Item"."weightedDownVotes", "Item".freebie, ltree2text("Item"."path") AS "path"` async function newTimedOrderByWeightedSats (me, models, num) { diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index 7e630494..79272e86 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -106,7 +106,7 @@ export default { if (meFull.noteItemSats) { queries.push( `(SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime", - sum("ItemAct".sats) as "earnedSats", 'Votification' AS type + floor(sum("ItemAct".msats)/1000) as "earnedSats", 'Votification' AS type FROM "Item" JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id WHERE "ItemAct"."userId" <> $1 diff --git a/api/resolvers/user.js b/api/resolvers/user.js index f0e5dcec..b9691616 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -1,5 +1,6 @@ import { AuthenticationError, UserInputError } from 'apollo-server-errors' import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' +import { msatsToSats } from '../../lib/format' import { createMentions, getItem, SELECT, updateItem, filterClause } from './item' import serialize from './serial' @@ -92,7 +93,7 @@ export default { let users if (sort === 'spent') { users = await models.$queryRaw(` - SELECT users.*, sum("ItemAct".sats) as spent + SELECT users.*, floor(sum("ItemAct".msats)/1000) as spent FROM "ItemAct" JOIN users on "ItemAct"."userId" = users.id WHERE "ItemAct".created_at <= $1 @@ -125,16 +126,16 @@ export default { LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset) } else { users = await models.$queryRaw(` - SELECT u.id, u.name, u."photoId", sum(amount) as stacked + SELECT u.id, u.name, u."photoId", floor(sum(amount)/1000) as stacked FROM - ((SELECT users.*, "ItemAct".sats as amount + ((SELECT users.*, "ItemAct".msats as amount FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id JOIN users on "Item"."userId" = users.id WHERE act <> 'BOOST' AND "ItemAct"."userId" <> users.id AND "ItemAct".created_at <= $1 ${within('ItemAct', when)}) UNION ALL - (SELECT users.*, "Earn".msats/1000 as amount + (SELECT users.*, "Earn".msats as amount FROM "Earn" JOIN users on users.id = "Earn"."userId" WHERE "Earn".msats > 0 ${within('Earn', when)})) u @@ -422,22 +423,22 @@ export default { if (!when) { // forever - return Math.floor((user.stackedMsats || 0) / 1000) + return (user.stackedMsats && msatsToSats(user.stackedMsats)) || 0 } else { const [{ stacked }] = await models.$queryRaw(` SELECT sum(amount) as stacked FROM - ((SELECT sum("ItemAct".sats) as amount + ((SELECT sum("ItemAct".msats) as amount FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id WHERE act <> 'BOOST' AND "ItemAct"."userId" <> $2 AND "Item"."userId" = $2 AND "ItemAct".created_at >= $1) UNION ALL - (SELECT sum("Earn".msats/1000) as amount + (SELECT sum("Earn".msats) as amount FROM "Earn" WHERE "Earn".msats > 0 AND "Earn"."userId" = $2 AND "Earn".created_at >= $1)) u`, withinDate(when), Number(user.id)) - return stacked || 0 + return (stacked && msatsToSats(stacked)) || 0 } }, spent: async (user, { when }, { models }) => { @@ -445,9 +446,9 @@ export default { return user.spent } - const { sum: { sats } } = await models.itemAct.aggregate({ + const { sum: { msats } } = await models.itemAct.aggregate({ sum: { - sats: true + msats: true }, where: { userId: user.id, @@ -457,13 +458,13 @@ export default { } }) - return sats || 0 + return (msats && msatsToSats(msats)) || 0 }, sats: async (user, args, { models, me }) => { if (me?.id !== user.id) { return 0 } - return Math.floor(user.msats / 1000.0) + return msatsToSats(user.msats) }, bio: async (user, args, { models }) => { return getItem(user, { id: user.bioId }, { models }) diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index c9724cee..4e8a98f6 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -5,6 +5,7 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' import lnpr from 'bolt11' import { SELECT } from './item' import { lnurlPayDescriptionHash } from '../../lib/lnurl' +import { msatsToSats } from '../../lib/format' export async function getInvoice (parent, { id }, { me, models }) { if (!me) { @@ -93,7 +94,7 @@ export default { if (include.has('stacked')) { queries.push( `(SELECT ('stacked' || "Item".id) as id, "Item".id as "factId", NULL as bolt11, - MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".sats) * 1000 as msats, + MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".msats) as msats, 0 as "msatsFee", NULL as status, 'stacked' as type FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id @@ -114,7 +115,7 @@ export default { if (include.has('spent')) { queries.push( `(SELECT ('spent' || "Item".id) as id, "Item".id as "factId", NULL as bolt11, - MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".sats) * 1000 as msats, + MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".msats) as msats, 0 as "msatsFee", NULL as status, 'spent' as type FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id @@ -254,10 +255,14 @@ export default { }, Withdrawl: { - satsPaying: w => Math.floor(w.msatsPaying / 1000), - satsPaid: w => Math.floor(w.msatsPaid / 1000), - satsFeePaying: w => Math.floor(w.msatsFeePaying / 1000), - satsFeePaid: w => Math.floor(w.msatsFeePaid / 1000) + satsPaying: w => msatsToSats(w.msatsPaying), + satsPaid: w => msatsToSats(w.msatsPaid), + satsFeePaying: w => msatsToSats(w.msatsFeePaying), + satsFeePaid: w => msatsToSats(w.msatsFeePaid) + }, + + Invoice: { + satsReceived: i => msatsToSats(i.msatsReceived) }, Fact: { @@ -271,7 +276,9 @@ export default { WHERE id = $1`, Number(fact.factId)) return item - } + }, + sats: fact => msatsToSats(fact.msats), + satsFee: fact => msatsToSats(fact.msatsFee) } } @@ -285,7 +292,7 @@ async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd throw new UserInputError('could not decode invoice') } - if (!decoded.mtokens || Number(decoded.mtokens) <= 0) { + if (!decoded.mtokens || BigInt(decoded.mtokens) <= 0) { throw new UserInputError('your invoice must specify an amount') } diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 8d955a4d..5b4d3303 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -21,7 +21,7 @@ export default gql` expiresAt: String! cancelled: Boolean! confirmedAt: String - msatsReceived: Int + satsReceived: Int } type Withdrawl { @@ -29,13 +29,9 @@ export default gql` createdAt: String! hash: String! bolt11: String! - msatsPaying: Int! satsPaying: Int! - msatsPaid: Int satsPaid: Int - msatsFeePaying: Int! satsFeePaying: Int! - msatsFeePaid: Int satsFeePaid: Int status: String } @@ -45,8 +41,8 @@ export default gql` factId: ID! bolt11: String createdAt: String! - msats: Int! - msatsFee: Int + sats: Int! + satsFee: Int status: String type: String! description: String diff --git a/components/invoice.js b/components/invoice.js index 707a4a21..677f83da 100644 --- a/components/invoice.js +++ b/components/invoice.js @@ -5,7 +5,7 @@ export function Invoice ({ invoice }) { let status = 'waiting for you' if (invoice.confirmedAt) { variant = 'confirmed' - status = `${invoice.msatsReceived / 1000} sats deposited` + status = `${invoice.satsReceived} sats deposited` } else if (invoice.cancelled) { variant = 'failed' status = 'cancelled' diff --git a/components/text.js b/components/text.js index 282fc6bc..77fb035c 100644 --- a/components/text.js +++ b/components/text.js @@ -12,7 +12,7 @@ import React, { useEffect, useState } from 'react' import GithubSlugger from 'github-slugger' import LinkIcon from '../svgs/link.svg' import Thumb from '../svgs/thumb-up-fill.svg' -import {toString} from 'mdast-util-to-string' +import { toString } from 'mdast-util-to-string' import copy from 'clipboard-copy' function myRemarkPlugin () { @@ -32,8 +32,6 @@ function myRemarkPlugin () { } } - - function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props }) { const [copied, setCopied] = useState(false) const [id] = useState(noFragments ? undefined : slugger.slug(toString(node).replace(/[^\w\-\s]+/gi, ''))) @@ -44,20 +42,20 @@ function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props ) } @@ -66,7 +64,7 @@ export default function Text ({ topLevel, noFragments, nofollow, children }) { // all the reactStringReplace calls are to facilitate search highlighting const slugger = new GithubSlugger() - const HeadingWrapper = (props) => Heading({ topLevel, slugger, noFragments, ...props}) + const HeadingWrapper = (props) => Heading({ topLevel, slugger, noFragments, ...props }) return (
@@ -108,10 +106,10 @@ export default function Text ({ topLevel, noFragments, nofollow, children }) { // map: fix any highlighted links children = children?.map(e => typeof e === 'string' - ? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => { - return {match} - }) - : e) + ? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => { + return {match} + }) + : e) return ( /* eslint-disable-next-line */ diff --git a/fragments/wallet.js b/fragments/wallet.js index bdf02e70..93a5b57e 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -7,7 +7,7 @@ export const INVOICE = gql` invoice(id: $id) { id bolt11 - msatsReceived + satsReceived cancelled confirmedAt expiresAt @@ -40,8 +40,8 @@ export const WALLET_HISTORY = gql` factId type createdAt - msats - msatsFee + sats + satsFee status type description diff --git a/lib/format.js b/lib/format.js index 7cb2f1d4..d312eb9e 100644 --- a/lib/format.js +++ b/lib/format.js @@ -9,3 +9,10 @@ export const abbrNum = n => { export const fixedDecimal = (n, f) => { return Number.parseFloat(n).toFixed(f) } + +export const msatsToSats = msats => { + if (msats === null || msats === undefined) { + return null + } + return Number(BigInt(msats) / 1000n) +} diff --git a/pages/_app.js b/pages/_app.js index fdb578e3..e9a3c6d7 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -49,10 +49,12 @@ function MyApp ({ Component, pageProps: { session, ...props } }) { // this nodata var will get passed to the server on back/foward and // 1. prevent data from reloading and 2. perserve scroll // (2) is not possible while intercepting nav with beforePopState - router.replace({ - pathname: router.pathname, - query: { ...router.query, nodata: true } - }, router.asPath, { ...router.options, scroll: false }) + if (router.isReady) { + router.replace({ + pathname: router.pathname, + query: { ...router.query, nodata: true } + }, router.asPath, { ...router.options, scroll: false }) + } }, [router.asPath]) /* diff --git a/pages/invoices/[id].js b/pages/invoices/[id].js index 909e8d4f..5a6812e8 100644 --- a/pages/invoices/[id].js +++ b/pages/invoices/[id].js @@ -19,7 +19,10 @@ function LoadInvoice () { pollInterval: 1000, variables: { id: router.query.id } }) - if (error) return
error
+ if (error) { + console.log(error) + return
error
+ } if (!data || loading) { return } diff --git a/pages/satistics.js b/pages/satistics.js index 5b550773..bea223cd 100644 --- a/pages/satistics.js +++ b/pages/satistics.js @@ -213,7 +213,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor - {Math.floor(f.msats / 1000)} + {Math.floor(f.sats)} ) diff --git a/prisma/migrations/20221110190205_msats_bigint/migration.sql b/prisma/migrations/20221110190205_msats_bigint/migration.sql new file mode 100644 index 00000000..8eb94638 --- /dev/null +++ b/prisma/migrations/20221110190205_msats_bigint/migration.sql @@ -0,0 +1,43 @@ +-- AlterTable +ALTER TABLE "Earn" ALTER COLUMN "msats" SET DATA TYPE BIGINT; + +-- AlterTable +ALTER TABLE "Invoice" ALTER COLUMN "msatsRequested" SET DATA TYPE BIGINT, +ALTER COLUMN "msatsReceived" SET DATA TYPE BIGINT; + +-- AlterTable +ALTER TABLE "Item" +ALTER COLUMN "commentSats" SET DATA TYPE BIGINT, +ALTER COLUMN "sats" SET DATA TYPE BIGINT; + +-- AlterTable +ALTER TABLE "Item" RENAME COLUMN "commentSats" TO "commentMsats"; +ALTER TABLE "Item" RENAME COLUMN "sats" TO "msats"; + +-- update to msats +UPDATE "Item" SET +"commentMsats" = "commentMsats" * 1000, +"msats" = "msats" * 1000; + +-- AlterTable +ALTER TABLE "ItemAct" +ALTER COLUMN "sats" SET DATA TYPE BIGINT; + +-- AlterTable +ALTER TABLE "ItemAct" RENAME COLUMN "sats" TO "msats"; + +-- update to msats +UPDATE "ItemAct" SET +"msats" = "msats" * 1000; + +-- AlterTable +ALTER TABLE "Withdrawl" ALTER COLUMN "msatsPaying" SET DATA TYPE BIGINT, +ALTER COLUMN "msatsPaid" SET DATA TYPE BIGINT, +ALTER COLUMN "msatsFeePaying" SET DATA TYPE BIGINT, +ALTER COLUMN "msatsFeePaid" SET DATA TYPE BIGINT; + +-- AlterTable +ALTER TABLE "users" ALTER COLUMN "msats" SET DEFAULT 0, +ALTER COLUMN "msats" SET DATA TYPE BIGINT, +ALTER COLUMN "stackedMsats" SET DEFAULT 0, +ALTER COLUMN "stackedMsats" SET DATA TYPE BIGINT; \ No newline at end of file diff --git a/prisma/migrations/20221110224543_msats_funcs/migration.sql b/prisma/migrations/20221110224543_msats_funcs/migration.sql new file mode 100644 index 00000000..1be0d18f --- /dev/null +++ b/prisma/migrations/20221110224543_msats_funcs/migration.sql @@ -0,0 +1,376 @@ +-- item_act should take sats but treat them as msats +CREATE OR REPLACE FUNCTION item_act(item_id INTEGER, user_id INTEGER, act "ItemActType", act_sats INTEGER) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + user_msats BIGINT; + act_msats BIGINT; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + act_msats := act_sats * 1000; + SELECT msats INTO user_msats FROM users WHERE id = user_id; + IF act_msats > user_msats THEN + RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS'; + END IF; + + + -- deduct msats from actor + UPDATE users SET msats = msats - act_msats WHERE id = user_id; + + IF act = 'VOTE' OR act = 'TIP' THEN + -- add sats to actee's balance and stacked count + UPDATE users + SET msats = msats + act_msats, "stackedMsats" = "stackedMsats" + act_msats + WHERE id = (SELECT COALESCE("fwdUserId", "userId") FROM "Item" WHERE id = item_id); + + -- if they have already voted, this is a tip + IF EXISTS (SELECT 1 FROM "ItemAct" WHERE "itemId" = item_id AND "userId" = user_id AND "ItemAct".act = 'VOTE') THEN + INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) + VALUES (act_msats, item_id, user_id, 'TIP', now_utc(), now_utc()); + ELSE + -- else this is a vote with a possible extra tip + INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) + VALUES (1000, item_id, user_id, 'VOTE', now_utc(), now_utc()); + act_msats := act_msats - 1000; + + -- if we have sats left after vote, leave them as a tip + IF act_msats > 0 THEN + INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) + VALUES (act_msats, item_id, user_id, 'TIP', now_utc(), now_utc()); + END IF; + + RETURN 1; + END IF; + ELSE -- BOOST, POLL, DONT_LIKE_THIS + INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) + VALUES (act_msats, item_id, user_id, act, now_utc(), now_utc()); + END IF; + + RETURN 0; +END; +$$; + +-- when creating free item, set freebie flag so can be optionally viewed +CREATE OR REPLACE FUNCTION create_item( + title TEXT, url TEXT, text TEXT, boost INTEGER, + parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER, + spam_within INTERVAL) +RETURNS "Item" +LANGUAGE plpgsql +AS $$ +DECLARE + user_msats BIGINT; + cost_msats BIGINT; + free_posts INTEGER; + free_comments INTEGER; + freebie BOOLEAN; + item "Item"; + med_votes FLOAT; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + SELECT msats, "freePosts", "freeComments" + INTO user_msats, free_posts, free_comments + FROM users WHERE id = user_id; + + cost_msats := 1000 * POWER(10, item_spam(parent_id, user_id, spam_within)); + -- it's only a freebie if it's a 1 sat cost, they have < 1 sat, boost = 0, and they have freebies left + freebie := (cost_msats <= 1000) AND (user_msats < 1000) AND (boost = 0) AND ((parent_id IS NULL AND free_posts > 0) OR (parent_id IS NOT NULL AND free_comments > 0)); + + IF NOT freebie AND cost_msats > user_msats THEN + RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS'; + END IF; + + -- get this user's median item score + SELECT COALESCE(percentile_cont(0.5) WITHIN GROUP(ORDER BY "weightedVotes" - "weightedDownVotes"), 0) INTO med_votes FROM "Item" WHERE "userId" = user_id; + + -- if their median votes are positive, start at 0 + -- if the median votes are negative, start their post with that many down votes + -- basically: if their median post is bad, presume this post is too + IF med_votes >= 0 THEN + med_votes := 0; + ELSE + med_votes := ABS(med_votes); + END IF; + + INSERT INTO "Item" (title, url, text, "userId", "parentId", "fwdUserId", freebie, "weightedDownVotes", created_at, updated_at) + VALUES (title, url, text, user_id, parent_id, fwd_user_id, freebie, med_votes, now_utc(), now_utc()) RETURNING * INTO item; + + IF freebie THEN + IF parent_id IS NULL THEN + UPDATE users SET "freePosts" = "freePosts" - 1 WHERE id = user_id; + ELSE + UPDATE users SET "freeComments" = "freeComments" - 1 WHERE id = user_id; + END IF; + ELSE + UPDATE users SET msats = msats - cost_msats WHERE id = user_id; + + INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) + VALUES (cost_msats, item.id, user_id, 'VOTE', now_utc(), now_utc()); + END IF; + + IF boost > 0 THEN + PERFORM item_act(item.id, user_id, 'BOOST', boost); + END IF; + + RETURN item; +END; +$$; + +CREATE OR REPLACE FUNCTION run_auction(item_id INTEGER) RETURNS void AS $$ + DECLARE + bid_msats BIGINT; + user_msats BIGINT; + user_id INTEGER; + item_status "Status"; + status_updated_at timestamp(3); + BEGIN + PERFORM ASSERT_SERIALIZED(); + + -- extract data we need + SELECT "maxBid" * 1000, "userId", status, "statusUpdatedAt" INTO bid_msats, user_id, item_status, status_updated_at FROM "Item" WHERE id = item_id; + SELECT msats INTO user_msats FROM users WHERE id = user_id; + + -- 0 bid items expire after 30 days unless updated + IF bid_msats = 0 THEN + IF item_status <> 'STOPPED' THEN + IF status_updated_at < now_utc() - INTERVAL '30 days' THEN + UPDATE "Item" SET status = 'STOPPED', "statusUpdatedAt" = now_utc() WHERE id = item_id; + ELSEIF item_status = 'NOSATS' THEN + UPDATE "Item" SET status = 'ACTIVE' WHERE id = item_id; + END IF; + END IF; + RETURN; + END IF; + + -- check if user wallet has enough sats + IF bid_msats > user_msats THEN + -- if not, set status = NOSATS and statusUpdatedAt to now_utc if not already set + IF item_status <> 'NOSATS' THEN + UPDATE "Item" SET status = 'NOSATS', "statusUpdatedAt" = now_utc() WHERE id = item_id; + END IF; + ELSE + -- if so, deduct from user + UPDATE users SET msats = msats - bid_msats WHERE id = user_id; + + -- create an item act + INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) + VALUES (bid_msats, item_id, user_id, 'STREAM', now_utc(), now_utc()); + + -- update item status = ACTIVE and statusUpdatedAt = now_utc if NOSATS + IF item_status = 'NOSATS' THEN + UPDATE "Item" SET status = 'ACTIVE', "statusUpdatedAt" = now_utc() WHERE id = item_id; + END IF; + END IF; + END; +$$ LANGUAGE plpgsql; + + +-- on item act denormalize sats and comment sats +CREATE OR REPLACE FUNCTION sats_after_act() RETURNS TRIGGER AS $$ +DECLARE + item "Item"; +BEGIN + SELECT * FROM "Item" WHERE id = NEW."itemId" INTO item; + IF item."userId" = NEW."userId" THEN + RETURN NEW; + END IF; + + UPDATE "Item" + SET "msats" = "msats" + NEW.msats + WHERE id = item.id; + + UPDATE "Item" + SET "commentMsats" = "commentMsats" + NEW.msats + WHERE id <> item.id and path @> item.path; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS sats_after_act_trigger ON "ItemAct"; +CREATE TRIGGER sats_after_act_trigger + AFTER INSERT ON "ItemAct" + FOR EACH ROW + WHEN (NEW.act = 'VOTE' or NEW.act = 'TIP') + EXECUTE PROCEDURE sats_after_act(); + +CREATE OR REPLACE FUNCTION boost_after_act() RETURNS TRIGGER AS $$ +BEGIN + -- update item + UPDATE "Item" SET boost = boost + FLOOR(NEW.msats / 1000) WHERE id = NEW."itemId"; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS boost_after_act ON "ItemAct"; +CREATE TRIGGER boost_after_act + AFTER INSERT ON "ItemAct" + FOR EACH ROW + WHEN (NEW.act = 'BOOST') + EXECUTE PROCEDURE boost_after_act(); + +DROP FUNCTION IF EXISTS create_invoice(TEXT, TEXT, timestamp(3) without time zone, INTEGER, INTEGER); +CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, msats_req BIGINT, user_id INTEGER) +RETURNS "Invoice" +LANGUAGE plpgsql +AS $$ +DECLARE + invoice "Invoice"; + limit_reached BOOLEAN; + too_much BOOLEAN; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + SELECT count(*) >= 10, coalesce(sum("msatsRequested"),0)+coalesce(max(users.msats), 0)+msats_req > 1000000000 INTO limit_reached, too_much + FROM "Invoice" + JOIN users on "userId" = users.id + WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" is null AND cancelled = false; + + -- prevent more than 10 pending invoices + IF limit_reached THEN + RAISE EXCEPTION 'SN_INV_PENDING_LIMIT'; + END IF; + + -- prevent pending invoices + msats from exceeding 1,000,000 sats + IF too_much THEN + RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE'; + END IF; + + INSERT INTO "Invoice" (hash, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at) + VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc()) RETURNING * INTO invoice; + + INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter) + VALUES ('checkInvoice', jsonb_build_object('hash', hash), 21, true, now() + interval '10 seconds'); + + RETURN invoice; +END; +$$; + +DROP FUNCTION IF EXISTS confirm_invoice(TEXT, INTEGER); +CREATE OR REPLACE FUNCTION confirm_invoice(lnd_id TEXT, lnd_received BIGINT) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + user_id INTEGER; + confirmed_at TIMESTAMP; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + SELECT "userId", "confirmedAt" INTO user_id, confirmed_at FROM "Invoice" WHERE hash = lnd_id; + IF confirmed_at IS NULL THEN + UPDATE "Invoice" SET "msatsReceived" = lnd_received, "confirmedAt" = now_utc(), updated_at = now_utc() + WHERE hash = lnd_id; + UPDATE users SET msats = msats + lnd_received WHERE id = user_id; + END IF; + RETURN 0; +END; +$$; + +DROP FUNCTION IF EXISTS create_withdrawl(TEXT, TEXT, INTEGER, INTEGER, TEXT); +CREATE OR REPLACE FUNCTION create_withdrawl(lnd_id TEXT, invoice TEXT, msats_amount BIGINT, msats_max_fee BIGINT, username TEXT) +RETURNS "Withdrawl" +LANGUAGE plpgsql +AS $$ +DECLARE + user_id INTEGER; + user_msats BIGINT; + withdrawl "Withdrawl"; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + SELECT msats, id INTO user_msats, user_id FROM users WHERE name = username; + IF (msats_amount + msats_max_fee) > user_msats THEN + RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS'; + END IF; + + IF EXISTS (SELECT 1 FROM "Withdrawl" WHERE hash = lnd_id AND status IS NULL) THEN + RAISE EXCEPTION 'SN_PENDING_WITHDRAWL_EXISTS'; + END IF; + + IF EXISTS (SELECT 1 FROM "Withdrawl" WHERE hash = lnd_id AND status = 'CONFIRMED') THEN + RAISE EXCEPTION 'SN_CONFIRMED_WITHDRAWL_EXISTS'; + END IF; + + INSERT INTO "Withdrawl" (hash, bolt11, "msatsPaying", "msatsFeePaying", "userId", created_at, updated_at) + VALUES (lnd_id, invoice, msats_amount, msats_max_fee, user_id, now_utc(), now_utc()) RETURNING * INTO withdrawl; + + UPDATE users SET msats = msats - msats_amount - msats_max_fee WHERE id = user_id; + + INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter) + VALUES ('checkWithdrawal', jsonb_build_object('id', withdrawl.id, 'hash', lnd_id), 21, true, now() + interval '10 seconds'); + + RETURN withdrawl; +END; +$$; + +DROP FUNCTION IF EXISTS confirm_withdrawl(INTEGER, INTEGER, INTEGER); +CREATE OR REPLACE FUNCTION confirm_withdrawl(wid INTEGER, msats_paid BIGINT, msats_fee_paid BIGINT) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + msats_fee_paying BIGINT; + user_id INTEGER; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + IF EXISTS (SELECT 1 FROM "Withdrawl" WHERE id = wid AND status IS NULL) THEN + SELECT "msatsFeePaying", "userId" INTO msats_fee_paying, user_id + FROM "Withdrawl" WHERE id = wid AND status IS NULL; + + UPDATE "Withdrawl" + SET status = 'CONFIRMED', "msatsPaid" = msats_paid, + "msatsFeePaid" = msats_fee_paid, updated_at = now_utc() + WHERE id = wid AND status IS NULL; + + UPDATE users SET msats = msats + (msats_fee_paying - msats_fee_paid) WHERE id = user_id; + END IF; + + RETURN 0; +END; +$$; + +CREATE OR REPLACE FUNCTION reverse_withdrawl(wid INTEGER, wstatus "WithdrawlStatus") +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + msats_fee_paying BIGINT; + msats_paying BIGINT; + user_id INTEGER; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + IF EXISTS (SELECT 1 FROM "Withdrawl" WHERE id = wid AND status IS NULL) THEN + SELECT "msatsPaying", "msatsFeePaying", "userId" INTO msats_paying, msats_fee_paying, user_id + FROM "Withdrawl" WHERE id = wid AND status IS NULL; + + UPDATE "Withdrawl" SET status = wstatus, updated_at = now_utc() WHERE id = wid AND status IS NULL; + + UPDATE users SET msats = msats + msats_paying + msats_fee_paying WHERE id = user_id; + END IF; + RETURN 0; +END; +$$; + +DROP FUNCTION IF EXISTS earn(INTEGER, INTEGER, TIMESTAMP(3), "EarnType", INTEGER, INTEGER); +CREATE OR REPLACE FUNCTION earn(user_id INTEGER, earn_msats BIGINT, created_at TIMESTAMP(3), + type "EarnType", type_id INTEGER, rank INTEGER) +RETURNS void AS $$ +DECLARE +BEGIN + PERFORM ASSERT_SERIALIZED(); + -- insert into earn + INSERT INTO "Earn" (msats, "userId", created_at, type, "typeId", rank) + VALUES (earn_msats, user_id, created_at, type, type_id, rank); + + -- give the user the sats + UPDATE users + SET msats = msats + earn_msats, "stackedMsats" = "stackedMsats" + 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 1ad3d5f8..503a0268 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,8 +30,8 @@ model User { inviteId String? bio Item? @relation(fields: [bioId], references: [id]) bioId Int? - msats Int @default(0) - stackedMsats Int @default(0) + msats BigInt @default(0) + stackedMsats BigInt @default(0) freeComments Int @default(5) freePosts Int @default(2) checkedNotesAt DateTime? @@ -105,8 +105,8 @@ model Earn { 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]) + msats BigInt + user User @relation(fields: [userId], references: [id]) userId Int type EarnType? @@ -192,13 +192,13 @@ model Item { bio Boolean @default(false) // denormalized self stats - weightedVotes Float @default(0) - weightedDownVotes Float @default(0) - sats Int @default(0) + weightedVotes Float @default(0) + weightedDownVotes Float @default(0) + msats BigInt @default(0) // denormalized comment stats ncomments Int @default(0) - commentSats Int @default(0) + commentMsats BigInt @default(0) lastCommentAt DateTime? // if sub is null, this is the main sub @@ -317,7 +317,7 @@ model ItemAct { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) @map(name: "created_at") updatedAt DateTime @updatedAt @map(name: "updated_at") - sats Int + msats BigInt act ItemActType item Item @relation(fields: [itemId], references: [id]) itemId Int @@ -356,8 +356,8 @@ model Invoice { bolt11 String expiresAt DateTime confirmedAt DateTime? - msatsRequested Int - msatsReceived Int? + msatsRequested BigInt + msatsReceived BigInt? cancelled Boolean @default(false) @@index([createdAt]) @@ -382,10 +382,10 @@ model Withdrawl { hash String bolt11 String - msatsPaying Int - msatsPaid Int? - msatsFeePaying Int - msatsFeePaid Int? + msatsPaying BigInt + msatsPaid BigInt? + msatsFeePaying BigInt + msatsFeePaid BigInt? status WithdrawlStatus? diff --git a/worker/earn.js b/worker/earn.js index 95e9895a..8f666fdb 100644 --- a/worker/earn.js +++ b/worker/earn.js @@ -10,17 +10,14 @@ function earn ({ models }) { console.log('running', name) // compute how much sn earned today - let [{ sum }] = await models.$queryRaw` - SELECT sum("ItemAct".sats) + const [{ sum }] = await models.$queryRaw` + SELECT sum("ItemAct".msats) FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id WHERE ("ItemAct".act in ('BOOST', 'STREAM') OR ("ItemAct".act IN ('VOTE','POLL') AND "Item"."userId" = "ItemAct"."userId")) AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'` - // convert to msats - sum = sum * 1000 - /* How earnings work: 1/3: top 21% posts over last 36 hours, scored on a relative basis @@ -56,7 +53,7 @@ function earn ({ models }) { ), upvoters AS ( SELECT "ItemAct"."userId", item_ratios.id, item_ratios.ratio, item_ratios."parentId", - sum("ItemAct".sats) as tipped, min("ItemAct".created_at) as acted_at + sum("ItemAct".msats) as tipped, min("ItemAct".created_at) as acted_at FROM item_ratios JOIN "ItemAct" on "ItemAct"."itemId" = item_ratios.id WHERE act IN ('VOTE','TIP')