diff --git a/api/resolvers/rewards.js b/api/resolvers/rewards.js index 3dcfd2c2..a65ebc43 100644 --- a/api/resolvers/rewards.js +++ b/api/resolvers/rewards.js @@ -157,7 +157,7 @@ export default { const [{ to, from }] = await models.$queryRaw` SELECT date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago' as from, (date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') + interval '1 day - 1 second' as to` - return await topUsers(parent, { when: 'custom', to: new Date(to).getTime().toString(), from: new Date(from).getTime().toString(), limit: 100 }, { models, ...context }) + return await topUsers(parent, { when: 'custom', to: new Date(to).getTime().toString(), from: new Date(from).getTime().toString(), limit: 500 }, { models, ...context }) }, total: async (parent, args, { models }) => { if (!parent.total) { diff --git a/api/resolvers/user.js b/api/resolvers/user.js index e836b22b..03f6a8d1 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -66,11 +66,12 @@ export async function topUsers (parent, { cursor, when, by, from, to, limit = LI case 'comments': column = 'ncomments'; break case 'referrals': column = 'referrals'; break case 'stacking': column = 'stacked'; break + case 'value': default: column = 'proportion'; break } const users = (await models.$queryRawUnsafe(` - SELECT * + SELECT * ${column === 'proportion' ? ', proportion' : ''} FROM (SELECT users.*, COALESCE(floor(sum(msats_spent)/1000), 0) as spent, diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 8b100170..e61cb4b7 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -59,6 +59,11 @@ export default gql` photoId: Int since: Int + """ + this is only returned when we sort stackers by value + """ + proportion: Float + optional: UserOptional! privates: UserPrivates diff --git a/components/user-list.js b/components/user-list.js index 40f77286..90be34f8 100644 --- a/components/user-list.js +++ b/components/user-list.js @@ -107,13 +107,13 @@ export function User ({ user, rank, statComps, className = 'mb-2', Embellish, ny
{statComps.map((Comp, i) => )}
} - {Embellish && } + {Embellish && } ) } -function UserHidden ({ rank, Embellish }) { +function UserHidden ({ rank, user, Embellish }) { return ( <> {rank @@ -133,7 +133,7 @@ function UserHidden ({ rank, Embellish }) {
stacker is in hiding
- {Embellish && } + {Embellish && } @@ -148,7 +148,7 @@ export function ListUsers ({ users, rank, statComps = DEFAULT_STAT_COMPONENTS, E {users.map((user, i) => ( user ? - : + : ))} ) diff --git a/docker-compose.yml b/docker-compose.yml index ca015448..40406609 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -680,7 +680,6 @@ services: - bitcoin - sn_lnd - lnd - - cln restart: unless-stopped command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME} volumes: diff --git a/fragments/users.js b/fragments/users.js index da207861..b96ec932 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -254,7 +254,7 @@ export const TOP_USERS = gql` photoId ncomments(when: $when, from: $from, to: $to) nposts(when: $when, from: $from, to: $to) - + proportion optional { stacked(when: $when, from: $from, to: $to) spent(when: $when, from: $from, to: $to) diff --git a/pages/rewards/index.js b/pages/rewards/index.js index 164710f4..a81d8843 100644 --- a/pages/rewards/index.js +++ b/pages/rewards/index.js @@ -16,7 +16,6 @@ import { useToast } from '@/components/toast' import { useLightning } from '@/components/lightning' import { ListUsers } from '@/components/user-list' import { Col, Row } from 'react-bootstrap' -import { proportions } from '@/lib/madness' import { useData } from '@/components/use-data' import { GrowthPieChartSkeleton } from '@/components/charts-skeletons' import { useMemo } from 'react' @@ -50,6 +49,7 @@ ${ITEM_FULL_FIELDS} photoId ncomments nposts + proportion optional { streak @@ -117,9 +117,10 @@ export default function Rewards ({ ssrData }) { if (!dat) return - function EstimatedReward ({ rank }) { - const referrerReward = Math.floor(total * proportions[rank - 1] * 0.2) - const reward = Math.floor(total * proportions[rank - 1]) - referrerReward + function EstimatedReward ({ rank, user }) { + if (!user) return null + const referrerReward = Math.max(Math.floor(total * user.proportion * 0.2), 0) + const reward = Math.max(Math.floor(total * user.proportion) - referrerReward, 0) return (
diff --git a/prisma/migrations/20241217163642_user_values_improve/migration.sql b/prisma/migrations/20241217163642_user_values_improve/migration.sql new file mode 100644 index 00000000..47f21be3 --- /dev/null +++ b/prisma/migrations/20241217163642_user_values_improve/migration.sql @@ -0,0 +1,93 @@ +CREATE OR REPLACE FUNCTION user_values( + min TIMESTAMP(3), max TIMESTAMP(3), ival INTERVAL, date_part TEXT, + percentile_cutoff INTEGER DEFAULT 50, + each_upvote_portion FLOAT DEFAULT 4.0, + each_item_portion FLOAT DEFAULT 4.0, + handicap_ids INTEGER[] DEFAULT '{616, 6030, 4502, 27}', + handicap_zap_mult FLOAT DEFAULT 0.3) +RETURNS TABLE ( + t TIMESTAMP(3), id INTEGER, proportion FLOAT +) +LANGUAGE plpgsql +AS $$ +DECLARE + min_utc TIMESTAMP(3) := timezone('utc', min AT TIME ZONE 'America/Chicago'); +BEGIN + RETURN QUERY + SELECT period.t, u."userId", u.total_proportion + FROM generate_series(min, max, ival) period(t), + LATERAL + (WITH item_ratios AS ( + SELECT *, + CASE WHEN "parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type, + CASE WHEN "weightedVotes" > 0 THEN "weightedVotes"/(sum("weightedVotes") OVER (PARTITION BY "parentId" IS NULL)) ELSE 0 END AS ratio + FROM ( + SELECT *, + NTILE(100) OVER (PARTITION BY "parentId" IS NULL ORDER BY ("weightedVotes"-"weightedDownVotes") desc) AS percentile, + ROW_NUMBER() OVER (PARTITION BY "parentId" IS NULL ORDER BY ("weightedVotes"-"weightedDownVotes") desc) AS rank + FROM + "Item" + WHERE date_trunc(date_part, created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = period.t + AND "weightedVotes" > 0 + AND "deletedAt" IS NULL + AND NOT bio + AND ("invoiceActionState" IS NULL OR "invoiceActionState" = 'PAID') + ) x + WHERE x.percentile <= percentile_cutoff + ), + -- get top upvoters of top posts and comments + upvoter_islands AS ( + SELECT "ItemAct"."userId", item_ratios.id, item_ratios.ratio, item_ratios."parentId", + "ItemAct".msats as tipped, "ItemAct".created_at as acted_at, + ROW_NUMBER() OVER (partition by item_ratios.id order by "ItemAct".created_at asc) + - ROW_NUMBER() OVER (partition by item_ratios.id, "ItemAct"."userId" order by "ItemAct".created_at asc) AS island + FROM item_ratios + JOIN "ItemAct" on "ItemAct"."itemId" = item_ratios.id + WHERE act = 'TIP' + AND date_trunc(date_part, "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = period.t + AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID') + ), + -- isolate contiguous upzaps from the same user on the same item so that when we take the log + -- of the upzaps it accounts for successive zaps and does not disproportionately reward them + -- quad root of the total tipped + upvoters AS ( + SELECT "userId", upvoter_islands.id, ratio, "parentId", GREATEST(power(sum(tipped) / 1000, 0.25), 0) as tipped, min(acted_at) as acted_at + FROM upvoter_islands + GROUP BY "userId", upvoter_islands.id, ratio, "parentId", island + ), + -- the relative contribution of each upvoter to the post/comment + -- early component: 1/ln(early_rank + e - 1) + -- tipped component: how much they tipped relative to the total tipped for the item + -- multiplied by the relative rank of the item to the total items + -- multiplied by the trust of the user + upvoter_ratios AS ( + SELECT "userId", sum((early_multiplier+tipped_ratio)*ratio*CASE WHEN users.id = ANY (handicap_ids) THEN handicap_zap_mult ELSE users.trust+0.1 END) as upvoter_ratio, + "parentId" IS NULL as "isPost", CASE WHEN "parentId" IS NULL THEN 'TIP_POST' ELSE 'TIP_COMMENT' END as type + FROM ( + SELECT *, + 1.0/LN(ROW_NUMBER() OVER (partition by upvoters.id order by acted_at asc) + EXP(1.0) - 1) AS early_multiplier, + tipped::float/(sum(tipped) OVER (partition by upvoters.id)) tipped_ratio + FROM upvoters + WHERE tipped > 2.1 + ) u + JOIN users on "userId" = users.id + GROUP BY "userId", "parentId" IS NULL + ), + proportions AS ( + SELECT "userId", NULL as id, type, ROW_NUMBER() OVER (PARTITION BY "isPost" ORDER BY upvoter_ratio DESC) as rank, + upvoter_ratio/(sum(upvoter_ratio) OVER (PARTITION BY "isPost"))/each_upvote_portion as proportion + FROM upvoter_ratios + WHERE upvoter_ratio > 0 + UNION ALL + SELECT "userId", item_ratios.id, type, rank, ratio/each_item_portion as proportion + FROM item_ratios + ) + SELECT "userId", sum(proportions.proportion) AS total_proportion + FROM proportions + GROUP BY "userId" + HAVING sum(proportions.proportion) > 0.000001) u; +END; +$$; + +REFRESH MATERIALIZED VIEW CONCURRENTLY user_values_today; +REFRESH MATERIALIZED VIEW CONCURRENTLY user_values_days; \ No newline at end of file diff --git a/worker/earn.js b/worker/earn.js index 63034259..019cf9bb 100644 --- a/worker/earn.js +++ b/worker/earn.js @@ -1,6 +1,5 @@ import { notifyEarner } from '@/lib/webPush' import createPrisma from '@/lib/create-prisma' -import { proportions } from '@/lib/madness' import { SN_NO_REWARDS_IDS } from '@/lib/constants' const TOTAL_UPPER_BOUND_MSATS = 1_000_000_000 @@ -40,18 +39,19 @@ export async function earn ({ name }) { /* How earnings (used to) work: - 1/3: top 21% posts over last 36 hours, scored on a relative basis - 1/3: top 21% comments over last 36 hours, scored on a relative basis + 1/3: top 50% posts over last 36 hours, scored on a relative basis + 1/3: top 50% comments over last 36 hours, scored on a relative basis 1/3: top upvoters of top posts/comments, scored on: - their trust - how much they tipped - how early they upvoted it - how the post/comment scored - Now: 80% of earnings go to top 100 stackers by value, and 10% each to their forever and one day referrers + Now: 80% of earnings go to top stackers by relative value, and 10% each to their forever and one day referrers */ // get earners { userId, id, type, rank, proportion, foreverReferrerId, oneDayReferrerId } + // has to earn at least 125000 msats to be eligible (so that they get at least 1 sat after referrals) const earners = await models.$queryRaw` WITH earners AS ( SELECT users.id AS "userId", users."referrerId" AS "foreverReferrerId", @@ -63,8 +63,8 @@ export async function earn ({ name }) { 'day') uv JOIN users ON users.id = uv.id WHERE NOT (users.id = ANY (${SN_NO_REWARDS_IDS})) + AND uv.proportion >= 0.0000125 ORDER BY proportion DESC - LIMIT 100 ) SELECT earners.*, COALESCE( @@ -86,10 +86,10 @@ export async function earn ({ name }) { let total = 0 const notifications = {} - for (const [i, earner] of earners.entries()) { + for (const [, earner] of earners.entries()) { const foreverReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings let oneDayReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings - const earnerEarnings = Math.floor(parseFloat(proportions[i] * sum)) - foreverReferrerEarnings - oneDayReferrerEarnings + const earnerEarnings = Math.floor(parseFloat(earner.proportion * sum)) - foreverReferrerEarnings - oneDayReferrerEarnings total += earnerEarnings + foreverReferrerEarnings + oneDayReferrerEarnings if (total > sum) { @@ -108,7 +108,7 @@ export async function earn ({ name }) { 'oneDayReferrer', earner.oneDayReferrerId, 'oneDayReferrerEarnings', oneDayReferrerEarnings) - if (earnerEarnings > 0) { + if (earnerEarnings > 1000) { stmts.push(...earnStmts({ msats: earnerEarnings, userId: earner.userId, @@ -140,7 +140,7 @@ export async function earn ({ name }) { } } - if (earner.foreverReferrerId && foreverReferrerEarnings > 0) { + if (earner.foreverReferrerId && foreverReferrerEarnings > 1000) { stmts.push(...earnStmts({ msats: foreverReferrerEarnings, userId: earner.foreverReferrerId, @@ -153,7 +153,7 @@ export async function earn ({ name }) { oneDayReferrerEarnings += foreverReferrerEarnings } - if (earner.oneDayReferrerId && oneDayReferrerEarnings > 0) { + if (earner.oneDayReferrerId && oneDayReferrerEarnings > 1000) { stmts.push(...earnStmts({ msats: oneDayReferrerEarnings, userId: earner.oneDayReferrerId,