From a5fa40aa1bb8917121cb94c733a4ad515ee89343 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Mon, 9 Dec 2024 16:04:41 +0100 Subject: [PATCH 01/39] Fix comment in useSendWallets (#1691) --- wallets/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallets/index.js b/wallets/index.js index dbf6f45b3..bdf00083b 100644 --- a/wallets/index.js +++ b/wallets/index.js @@ -220,7 +220,7 @@ export function useWallet (name) { export function useSendWallets () { const { wallets } = useWallets() - // return the first enabled wallet that is available and can send + // return all enabled wallets that are available and can send return wallets .filter(w => !w.def.isAvailable || w.def.isAvailable()) .filter(w => w.config?.enabled && canSend(w)) From d05a27a6c380abdfc12a173f071d155e4972acfc Mon Sep 17 00:00:00 2001 From: Keyan <34140557+huumn@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:10:49 -0600 Subject: [PATCH 02/39] Update awards.csv --- awards.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awards.csv b/awards.csv index 9a4f38995..fca3c3276 100644 --- a/awards.csv +++ b/awards.csv @@ -152,7 +152,7 @@ Gudnessuche,issue,#1662,#1661,good-first-issue,,,,2k,everythingsatoshi@getalby.c aegroto,pr,#1589,#1586,easy,,,,100k,aegroto@blink.sv,2024-12-07 aegroto,issue,#1589,#1586,easy,,,,10k,aegroto@blink.sv,2024-12-07 aegroto,pr,#1619,#914,easy,,,,100k,aegroto@blink.sv,2024-12-07 -felipebueno,pr,#1620,,medium,,,1,225k,felipebueno@getalby.com,??? +felipebueno,pr,#1620,,medium,,,1,225k,felipebueno@getalby.com,2024-12-09 Soxasora,pr,#1647,#1645,easy,,,,100k,soxasora@blink.sv,2024-12-07 Soxasora,pr,#1667,#1568,easy,,,,100k,soxasora@blink.sv,2024-12-07 aegroto,pr,#1633,#1471,easy,,,1,90k,aegroto@blink.sv,2024-12-07 From 61fb1c445fe0a392369050f0ada600489650bcc2 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Mon, 9 Dec 2024 23:09:26 +0100 Subject: [PATCH 03/39] Fix header carousel desync (#1696) --- components/nav/carousel.js | 46 ++++++++++++++++++++++++++++++++++++++ components/nav/index.js | 5 +++-- components/price.js | 44 ++++++++---------------------------- 3 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 components/nav/carousel.js diff --git a/components/nav/carousel.js b/components/nav/carousel.js new file mode 100644 index 000000000..293e9c28c --- /dev/null +++ b/components/nav/carousel.js @@ -0,0 +1,46 @@ +import { createContext, useCallback, useContext, useEffect, useState } from 'react' + +const STORAGE_KEY = 'asSats' +const DEFAULT_SELECTION = 'fiat' + +const carousel = [ + 'fiat', + 'yep', + '1btc', + 'blockHeight', + 'chainFee', + 'halving' +] + +export const CarouselContext = createContext({ + selection: undefined, + handleClick: () => {} +}) + +export function CarouselProvider ({ children }) { + const [selection, setSelection] = useState(undefined) + const [pos, setPos] = useState(0) + + useEffect(() => { + const selection = window.localStorage.getItem(STORAGE_KEY) ?? DEFAULT_SELECTION + setSelection(selection) + setPos(carousel.findIndex((item) => item === selection)) + }, []) + + const handleClick = useCallback(() => { + const nextPos = (pos + 1) % carousel.length + window.localStorage.setItem(STORAGE_KEY, carousel[nextPos]) + setSelection(carousel[nextPos]) + setPos(nextPos) + }, [pos]) + + return ( + + {children} + + ) +} + +export function useCarousel () { + return useContext(CarouselContext) +} diff --git a/components/nav/index.js b/components/nav/index.js index 9413bbd65..f8906b27b 100644 --- a/components/nav/index.js +++ b/components/nav/index.js @@ -2,6 +2,7 @@ import { useRouter } from 'next/router' import DesktopHeader from './desktop/header' import MobileHeader from './mobile/header' import StickyBar from './sticky-bar' +import { CarouselProvider } from './carousel' export default function Navigation ({ sub }) { const router = useRouter() @@ -16,10 +17,10 @@ export default function Navigation ({ sub }) { } return ( - <> + - + ) } diff --git a/components/price.js b/components/price.js index b104b5df8..415c0877a 100644 --- a/components/price.js +++ b/components/price.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useMemo, useState } from 'react' +import React, { useContext, useMemo } from 'react' import { useQuery } from '@apollo/client' import { fixedDecimal } from '@/lib/format' import { useMe } from './me' @@ -8,6 +8,7 @@ import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants' import { useBlockHeight } from './block-height' import { useChainFee } from './chain-fee' import { CompactLongCountdown } from './countdown' +import { useCarousel } from './nav/carousel' export const PriceContext = React.createContext({ price: null, @@ -43,43 +44,16 @@ export function PriceProvider ({ price, children }) { ) } -const STORAGE_KEY = 'asSats' -const DEFAULT_SELECTION = 'fiat' - -const carousel = [ - 'fiat', - 'yep', - '1btc', - 'blockHeight', - 'chainFee', - 'halving' -] - export default function Price ({ className }) { - const [asSats, setAsSats] = useState(undefined) - const [pos, setPos] = useState(0) - - useEffect(() => { - const selection = window.localStorage.getItem(STORAGE_KEY) ?? DEFAULT_SELECTION - setAsSats(selection) - setPos(carousel.findIndex((item) => item === selection)) - }, []) + const [selection, handleClick] = useCarousel() const { price, fiatSymbol } = usePrice() const { height: blockHeight, halving } = useBlockHeight() const { fee: chainFee } = useChainFee() - const handleClick = () => { - const nextPos = (pos + 1) % carousel.length - - window.localStorage.setItem(STORAGE_KEY, carousel[nextPos]) - setAsSats(carousel[nextPos]) - setPos(nextPos) - } - const compClassName = (className || '') + ' text-reset pointer' - if (asSats === 'yep') { + if (selection === 'yep') { if (!price || price < 0) return null return (
@@ -88,7 +62,7 @@ export default function Price ({ className }) { ) } - if (asSats === '1btc') { + if (selection === '1btc') { return (
1sat=1sat @@ -96,7 +70,7 @@ export default function Price ({ className }) { ) } - if (asSats === 'blockHeight') { + if (selection === 'blockHeight') { if (blockHeight <= 0) return null return (
@@ -105,7 +79,7 @@ export default function Price ({ className }) { ) } - if (asSats === 'halving') { + if (selection === 'halving') { if (!halving) return null return (
@@ -114,7 +88,7 @@ export default function Price ({ className }) { ) } - if (asSats === 'chainFee') { + if (selection === 'chainFee') { if (chainFee <= 0) return null return (
@@ -123,7 +97,7 @@ export default function Price ({ className }) { ) } - if (asSats === 'fiat') { + if (selection === 'fiat') { if (!price || price < 0) return null return (
From 52098a3e5011cb405a71b32c84e098f8c603d484 Mon Sep 17 00:00:00 2001 From: k00b Date: Mon, 9 Dec 2024 19:03:30 -0600 Subject: [PATCH 04/39] fix broken static header from carousel --- components/nav/static.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/components/nav/static.js b/components/nav/static.js index 9f453df08..68400f1e9 100644 --- a/components/nav/static.js +++ b/components/nav/static.js @@ -1,19 +1,22 @@ import { Container, Nav, Navbar } from 'react-bootstrap' import styles from '../header.module.css' import { BackOrBrand, NavPrice, SearchItem } from './common' +import { CarouselProvider } from './carousel' export default function StaticHeader () { return ( - - - - - + + + + + + + ) } From bf20cf8f56dc9637523b8bc1016f335871736980 Mon Sep 17 00:00:00 2001 From: k00b Date: Mon, 9 Dec 2024 19:06:46 -0600 Subject: [PATCH 05/39] fix different carousels named the exact same thing --- components/nav/index.js | 6 +++--- components/nav/{carousel.js => price-carousel.js} | 12 ++++++------ components/nav/static.js | 6 +++--- components/price.js | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) rename components/nav/{carousel.js => price-carousel.js} (74%) diff --git a/components/nav/index.js b/components/nav/index.js index f8906b27b..beacbd6fc 100644 --- a/components/nav/index.js +++ b/components/nav/index.js @@ -2,7 +2,7 @@ import { useRouter } from 'next/router' import DesktopHeader from './desktop/header' import MobileHeader from './mobile/header' import StickyBar from './sticky-bar' -import { CarouselProvider } from './carousel' +import { PriceCarouselProvider } from './price-carousel' export default function Navigation ({ sub }) { const router = useRouter() @@ -17,10 +17,10 @@ export default function Navigation ({ sub }) { } return ( - + - + ) } diff --git a/components/nav/carousel.js b/components/nav/price-carousel.js similarity index 74% rename from components/nav/carousel.js rename to components/nav/price-carousel.js index 293e9c28c..0b09c6721 100644 --- a/components/nav/carousel.js +++ b/components/nav/price-carousel.js @@ -12,12 +12,12 @@ const carousel = [ 'halving' ] -export const CarouselContext = createContext({ +export const PriceCarouselContext = createContext({ selection: undefined, handleClick: () => {} }) -export function CarouselProvider ({ children }) { +export function PriceCarouselProvider ({ children }) { const [selection, setSelection] = useState(undefined) const [pos, setPos] = useState(0) @@ -35,12 +35,12 @@ export function CarouselProvider ({ children }) { }, [pos]) return ( - + {children} - + ) } -export function useCarousel () { - return useContext(CarouselContext) +export function usePriceCarousel () { + return useContext(PriceCarouselContext) } diff --git a/components/nav/static.js b/components/nav/static.js index 68400f1e9..707fccc62 100644 --- a/components/nav/static.js +++ b/components/nav/static.js @@ -1,11 +1,11 @@ import { Container, Nav, Navbar } from 'react-bootstrap' import styles from '../header.module.css' import { BackOrBrand, NavPrice, SearchItem } from './common' -import { CarouselProvider } from './carousel' +import { PriceCarouselProvider } from './price-carousel' export default function StaticHeader () { return ( - +
@@ -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 ca0154480..40406609c 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 da2078618..b96ec9327 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 164710f4c..a81d88437 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 000000000..47f21be36 --- /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 630342597..019cf9bb1 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, From faeefdc498cec342deb65dfc864f161fce26b9f9 Mon Sep 17 00:00:00 2001 From: k00b Date: Wed, 18 Dec 2024 12:09:07 -0600 Subject: [PATCH 35/39] increase send payment timeout --- lib/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/constants.js b/lib/constants.js index 5cbbf6531..2afb2dceb 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -192,5 +192,5 @@ export const EXTRA_LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_EXTRA_LON export const ZAP_UNDO_DELAY_MS = 5_000 -export const WALLET_SEND_PAYMENT_TIMEOUT_MS = 15_000 +export const WALLET_SEND_PAYMENT_TIMEOUT_MS = 150_000 export const WALLET_CREATE_INVOICE_TIMEOUT_MS = 15_000 From 16b7160d3611c8f181b2babb9b3e68550f496d77 Mon Sep 17 00:00:00 2001 From: k00b Date: Wed, 18 Dec 2024 13:47:03 -0600 Subject: [PATCH 36/39] increase create invoice timeout --- lib/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/constants.js b/lib/constants.js index 2afb2dceb..b828d26d8 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -193,4 +193,4 @@ export const EXTRA_LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_EXTRA_LON export const ZAP_UNDO_DELAY_MS = 5_000 export const WALLET_SEND_PAYMENT_TIMEOUT_MS = 150_000 -export const WALLET_CREATE_INVOICE_TIMEOUT_MS = 15_000 +export const WALLET_CREATE_INVOICE_TIMEOUT_MS = 45_000 From b8061a630ccf7bc3b39f985180edae1cbe5812ce Mon Sep 17 00:00:00 2001 From: k00b Date: Wed, 18 Dec 2024 18:27:38 -0600 Subject: [PATCH 37/39] don't double factor trust --- .../migration.sql | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 prisma/migrations/20241219002217_improve_values_again/migration.sql diff --git a/prisma/migrations/20241219002217_improve_values_again/migration.sql b/prisma/migrations/20241219002217_improve_values_again/migration.sql new file mode 100644 index 000000000..2e8bfa37c --- /dev/null +++ b/prisma/migrations/20241219002217_improve_values_again/migration.sql @@ -0,0 +1,94 @@ +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 + HAVING CASE WHEN "parentId" IS NULL THEN sum(tipped) / 1000 > 40 ELSE sum(tipped) / 1000 > 20 END + ), + -- 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((2*early_multiplier+1)*tipped_ratio*ratio) 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 > 0 + ) 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 From d3a705d3ad3f526481f1e05f02c33df4fbd9b574 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Thu, 19 Dec 2024 01:28:18 +0100 Subject: [PATCH 38/39] Fix admin edits (#1737) --- api/resolvers/item.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/resolvers/item.js b/api/resolvers/item.js index cd65ff849..2f0676a94 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -1381,10 +1381,11 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, .. const user = await models.user.findUnique({ where: { id: meId } }) - // edits are only allowed for own items within 10 minutes but forever if it's their bio or a job + // edits are only allowed for own items within 10 minutes + // but forever if an admin is editing an "admin item", it's their bio or a job const myBio = user.bioId === old.id const timer = Date.now() < datePivot(new Date(old.invoicePaidAt ?? old.createdAt), { seconds: ITEM_EDIT_SECONDS }) - const canEdit = (timer && ownerEdit) || myBio || isJob(item) + const canEdit = (timer && ownerEdit) || adminEdit || myBio || isJob(item) if (!canEdit) { throw new GqlInputError('item can no longer be edited') } From 4db2edb1d9bfdc8d7951bd141cf1e5fd8883e300 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Thu, 19 Dec 2024 07:11:03 +0100 Subject: [PATCH 39/39] Close relay connections after each NWC call (#1739) --- lib/nostr.js | 7 +------ wallets/nwc/client.js | 6 ++---- wallets/nwc/index.js | 26 +++++++++++++++++++------- wallets/nwc/server.js | 9 +++++---- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/lib/nostr.js b/lib/nostr.js index 7e17340e0..077e658b1 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -29,7 +29,7 @@ export const RELAYS_BLACKLIST = [] * @property {function(Object, {privKey: string, signer: NDKSigner}): Promise} sign * @property {function(Object, {relays: Array, privKey: string, signer: NDKSigner}): Promise} publish */ -export class Nostr { +export default class Nostr { /** * @type {NDK} */ @@ -153,11 +153,6 @@ export class Nostr { } } -/** - * @type {Nostr} - */ -export default new Nostr() - export function hexToBech32 (hex, prefix = 'npub') { return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex'))) } diff --git a/wallets/nwc/client.js b/wallets/nwc/client.js index 74691edc2..f6fc3829a 100644 --- a/wallets/nwc/client.js +++ b/wallets/nwc/client.js @@ -1,4 +1,4 @@ -import { getNwc, supportedMethods, nwcTryRun } from '@/wallets/nwc' +import { supportedMethods, nwcTryRun } from '@/wallets/nwc' export * from '@/wallets/nwc' export async function testSendPayment ({ nwcUrl }, { signal }) { @@ -9,8 +9,6 @@ export async function testSendPayment ({ nwcUrl }, { signal }) { } export async function sendPayment (bolt11, { nwcUrl }, { signal }) { - const nwc = await getNwc(nwcUrl, { signal }) - // TODO: support AbortSignal - const result = await nwcTryRun(() => nwc.payInvoice(bolt11)) + const result = await nwcTryRun(nwc => nwc.payInvoice(bolt11), { nwcUrl }, { signal }) return result.preimage } diff --git a/wallets/nwc/index.js b/wallets/nwc/index.js index 7ec942410..3d0e212cf 100644 --- a/wallets/nwc/index.js +++ b/wallets/nwc/index.js @@ -36,8 +36,8 @@ export const card = { subtitle: 'use Nostr Wallet Connect for payments' } -export async function getNwc (nwcUrl, { signal }) { - const ndk = Nostr.ndk +async function getNwc (nwcUrl, { signal }) { + const ndk = new Nostr().ndk const { walletPubkey, secret, relayUrls } = parseNwcUrl(nwcUrl) const nwc = new NDKNwc({ ndk, @@ -65,20 +65,32 @@ export async function getNwc (nwcUrl, { signal }) { * @param {function} fun - the nwc function to run * @returns - the result of the nwc function */ -export async function nwcTryRun (fun) { +export async function nwcTryRun (fun, { nwcUrl }, { signal }) { + let nwc try { - const { error, result } = await fun() + nwc = await getNwc(nwcUrl, { signal }) + const { error, result } = await fun(nwc) if (error) throw new Error(error.message || error.code) return result } catch (e) { if (e.error) throw new Error(e.error.message || e.error.code) throw e + } finally { + if (nwc) close(nwc) + } +} + +/** + * Close all relay connections of the NDKNwc instance + * @param {NDKNwc} nwc + */ +async function close (nwc) { + for (const relay of nwc.relaySet.relays) { + nwc.ndk.pool.removeRelay(relay.url) } } export async function supportedMethods (nwcUrl, { signal }) { - const nwc = await getNwc(nwcUrl, { signal }) - // TODO: support AbortSignal - const result = await nwcTryRun(() => nwc.getInfo()) + const result = await nwcTryRun(nwc => nwc.getInfo(), { nwcUrl }, { signal }) return result.methods } diff --git a/wallets/nwc/server.js b/wallets/nwc/server.js index b9cc8fd56..6fb4c82c7 100644 --- a/wallets/nwc/server.js +++ b/wallets/nwc/server.js @@ -1,4 +1,4 @@ -import { getNwc, supportedMethods, nwcTryRun } from '@/wallets/nwc' +import { supportedMethods, nwcTryRun } from '@/wallets/nwc' export * from '@/wallets/nwc' export async function testCreateInvoice ({ nwcUrlRecv }, { signal }) { @@ -21,8 +21,9 @@ export async function testCreateInvoice ({ nwcUrlRecv }, { signal }) { } export async function createInvoice ({ msats, description, expiry }, { nwcUrlRecv }, { signal }) { - const nwc = await getNwc(nwcUrlRecv, { signal }) - // TODO: support AbortSignal - const result = await nwcTryRun(() => nwc.sendReq('make_invoice', { amount: msats, description, expiry })) + const result = await nwcTryRun( + nwc => nwc.sendReq('make_invoice', { amount: msats, description, expiry }), + { nwcUrl: nwcUrlRecv }, { signal } + ) return result.invoice }