anon func mods, e.g. inv limits

This commit is contained in:
keyan 2023-08-10 19:38:06 -05:00
parent 41f46cf41e
commit 6ba1c3e8ab
5 changed files with 176 additions and 10 deletions

View File

@ -8,7 +8,7 @@ import {
BOOST_MIN, ITEM_SPAM_INTERVAL,
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD,
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE, ANON_ITEM_SPAM_INTERVAL
} from '../../lib/constants'
import { msatsToSats, numWithUnits } from '../../lib/format'
import { parse } from 'tldts'
@ -627,10 +627,12 @@ export default {
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
const { sub, forward, boost, title, text, options, invoiceHash, invoiceHmac } = data
let author = me
let spamInterval = ITEM_SPAM_INTERVAL
const trx = []
if (!me && invoiceHash) {
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, ANON_POST_FEE)
author = invoice.user
spamInterval = ANON_ITEM_SPAM_INTERVAL
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
}
@ -670,7 +672,7 @@ export default {
return item
} else {
const [query] = await serialize(models,
models.$queryRawUnsafe(`${SELECT} FROM create_poll($1, $2, $3, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7, $8::INTEGER, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
models.$queryRawUnsafe(`${SELECT} FROM create_poll($1, $2, $3, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7, $8::INTEGER, '${spamInterval}') AS "Item"`,
sub || 'bitcoin', title, text, 1, Number(boost || 0), Number(author.id), options, Number(fwdUser?.id)), ...trx)
const item = trx.length > 0 ? query[0] : query
@ -1107,10 +1109,12 @@ export const updateItem = async (parent, { id, data: { sub, title, url, text, bo
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceHash, invoiceHmac }) => {
let author = me
let spamInterval = ITEM_SPAM_INTERVAL
const trx = []
if (!me && invoiceHash) {
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
author = invoice.user
spamInterval = ANON_ITEM_SPAM_INTERVAL
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
}
@ -1140,7 +1144,7 @@ const createItem = async (parent, { sub, title, url, text, boost, forward, bount
const [query] = await serialize(
models,
models.$queryRawUnsafe(
`${SELECT} FROM create_item($1, $2, $3, $4, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::INTEGER, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
`${SELECT} FROM create_item($1, $2, $3, $4, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::INTEGER, '${spamInterval}') AS "Item"`,
parentId ? null : sub || 'bitcoin',
title,
url,

View File

@ -8,7 +8,7 @@ import { SELECT } from './item'
import { lnurlPayDescriptionHash } from '../../lib/lnurl'
import { msatsToSats, msatsToSatsDecimal } from '../../lib/format'
import { amountSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '../../lib/validate'
import { ANON_USER_ID } from '../../lib/constants'
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT } from '../../lib/constants'
import { datePivot } from '../../lib/time'
export async function getInvoice (parent, { id }, { me, models }) {
@ -210,10 +210,20 @@ export default {
createInvoice: async (parent, { amount }, { me, models, lnd }) => {
await ssValidate(amountSchema, { amount })
const user = await models.user.findUnique({ where: { id: me ? me.id : ANON_USER_ID } })
const pivot = me ? { hours: 3 } : { minutes: 3 }
let expirePivot = { hours: 3 }
let invLimit = INV_PENDING_LIMIT
let balanceLimit = BALANCE_LIMIT_MSATS
let id = me?.id
if (!me) {
expirePivot = { minutes: 3 }
invLimit = ANON_INV_PENDING_LIMIT
balanceLimit = ANON_BALANCE_LIMIT_MSATS
id = ANON_USER_ID
}
const expiresAt = datePivot(new Date(), pivot)
const user = await models.user.findUnique({ where: { id } })
const expiresAt = datePivot(new Date(), expirePivot)
const description = `Funding @${user.name} on stacker.news`
try {
const invoice = await createInvoice({
@ -225,7 +235,8 @@ export default {
const [inv] = await serialize(models,
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description})`)
${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description},
${invLimit}::INTEGER, ${balanceLimit})`)
// the HMAC is only returned during invoice creation
// this makes sure that only the person who created this invoice

View File

@ -19,6 +19,11 @@ module.exports = {
MAX_TITLE_LENGTH: 80,
MAX_POLL_CHOICE_LENGTH: 30,
ITEM_SPAM_INTERVAL: '10m',
ANON_ITEM_SPAM_INTERVAL: '0',
INV_PENDING_LIMIT: 10,
BALANCE_LIMIT_MSATS: 1000000000, // 1m sats
ANON_INV_PENDING_LIMIT: 100,
ANON_BALANCE_LIMIT_MSATS: 100000000000, // 100m sats
MAX_POLL_NUM_CHOICES: 10,
MIN_POLL_NUM_CHOICES: 2,
ITEM_FILTER_THRESHOLD: 1.2,

View File

@ -6,6 +6,7 @@ import serialize from '../../../../api/resolvers/serial'
import { schnorr } from '@noble/curves/secp256k1'
import { createHash } from 'crypto'
import { datePivot } from '../../../../lib/time'
import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT } from '../../../../lib/constants'
export default async ({ query: { username, amount, nostr } }, res) => {
const user = await models.user.findUnique({ where: { name: username } })
@ -13,7 +14,7 @@ export default async ({ query: { username, amount, nostr } }, res) => {
return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` })
}
try {
// if nostr, decode, validate sig, check tags, set description hash
// if nostr, decode, validate sig, check tags, set description hash
let description, descriptionHash, noteStr
if (nostr) {
noteStr = decodeURIComponent(nostr)
@ -48,7 +49,8 @@ export default async ({ query: { username, amount, nostr } }, res) => {
await serialize(models,
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description})`)
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description},
${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`)
return res.status(200).json({
pr: invoice.request,

View File

@ -0,0 +1,144 @@
DROP FUNCTION IF EXISTS create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, msats_req BIGINT, user_id INTEGER, idesc TEXT);
-- make invoice limit and balance limit configurable
CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone,
msats_req BIGINT, user_id INTEGER, idesc TEXT, inv_limit INTEGER, balance_limit_msats BIGINT)
RETURNS "Invoice"
LANGUAGE plpgsql
AS $$
DECLARE
invoice "Invoice";
inv_limit_reached BOOLEAN;
balance_limit_reached BOOLEAN;
inv_pending_msats BIGINT;
BEGIN
PERFORM ASSERT_SERIALIZED();
-- prevent too many pending invoices
SELECT inv_limit > 0 AND count(*) >= inv_limit, sum("msatsRequested") INTO inv_limit_reached, inv_pending_msats
FROM "Invoice"
WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" IS NULL AND cancelled = false;
IF inv_limit_reached THEN
RAISE EXCEPTION 'SN_INV_PENDING_LIMIT';
END IF;
-- prevent pending invoices + msats from exceeding the limit
SELECT balance_limit_msats > 0 AND inv_pending_msats+msats_req+msats > balance_limit_msats INTO balance_limit_reached
FROM users
WHERE id = user_id;
IF balance_limit_reached THEN
RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE';
END IF;
-- we good, proceed frens
INSERT INTO "Invoice" (hash, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at, "desc")
VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc(), idesc) 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;
$$;
-- don't presume outlaw status for anon posters
CREATE OR REPLACE FUNCTION create_item(
sub TEXT, title TEXT, url TEXT, text TEXT, boost INTEGER, bounty 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;
freebie BOOLEAN;
item "Item";
med_votes FLOAT;
BEGIN
PERFORM ASSERT_SERIALIZED();
SELECT msats INTO user_msats 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, and boost = 0
freebie := (cost_msats <= 1000) AND (user_msats < 1000) AND (boost = 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
-- addendum: if they're an anon poster, always start at 0
IF med_votes >= 0 OR user_id = 27 THEN
med_votes := 0;
ELSE
med_votes := ABS(med_votes);
END IF;
INSERT INTO "Item"
("subName", title, url, text, bounty, "userId", "parentId", "fwdUserId",
freebie, "weightedDownVotes", created_at, updated_at)
VALUES
(sub, title, url, text, bounty, user_id, parent_id, fwd_user_id,
freebie, med_votes, now_utc(), now_utc()) RETURNING * INTO item;
IF NOT freebie THEN
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, 'FEE', now_utc(), now_utc());
END IF;
IF boost > 0 THEN
PERFORM item_act(item.id, user_id, 'BOOST', boost);
END IF;
RETURN item;
END;
$$;
-- keep item_spam unaware of anon user
CREATE OR REPLACE FUNCTION item_spam(parent_id INTEGER, user_id INTEGER, within INTERVAL)
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
DECLARE
repeats INTEGER;
self_replies INTEGER;
BEGIN
-- no fee escalation
IF within = interval '0' THEN
RETURN 0;
END IF;
SELECT count(*) INTO repeats
FROM "Item"
WHERE (parent_id IS NULL AND "parentId" IS NULL OR "parentId" = parent_id)
AND "userId" = user_id
AND created_at > now_utc() - within;
IF parent_id IS NULL THEN
RETURN repeats;
END IF;
WITH RECURSIVE base AS (
SELECT "Item".id, "Item"."parentId", "Item"."userId"
FROM "Item"
WHERE id = parent_id AND "userId" = user_id AND created_at > now_utc() - within
UNION ALL
SELECT "Item".id, "Item"."parentId", "Item"."userId"
FROM base p
JOIN "Item" ON "Item".id = p."parentId" AND "Item"."userId" = p."userId" AND "Item".created_at > now_utc() - within)
SELECT count(*) INTO self_replies FROM base;
RETURN repeats + self_replies;
END;
$$;