From caafc43781aa48266b623bb0d10ccf4cf379e6b6 Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Tue, 25 Feb 2025 11:57:12 -0800 Subject: [PATCH 01/34] Add AAO plugin with basic attest (#11453) --- apps/anti-abuse-oracle/.eslintrc.js | 4 ++ apps/anti-abuse-oracle/package.json | 46 ++++++++++++++ apps/anti-abuse-oracle/src/config.ts | 73 ++++++++++++++++++++++ apps/anti-abuse-oracle/src/index.ts | 93 ++++++++++++++++++++++++++++ apps/anti-abuse-oracle/src/logger.ts | 16 +++++ apps/anti-abuse-oracle/tsconfig.json | 11 ++++ 6 files changed, 243 insertions(+) create mode 100644 apps/anti-abuse-oracle/.eslintrc.js create mode 100644 apps/anti-abuse-oracle/package.json create mode 100644 apps/anti-abuse-oracle/src/config.ts create mode 100644 apps/anti-abuse-oracle/src/index.ts create mode 100644 apps/anti-abuse-oracle/src/logger.ts create mode 100644 apps/anti-abuse-oracle/tsconfig.json diff --git a/apps/anti-abuse-oracle/.eslintrc.js b/apps/anti-abuse-oracle/.eslintrc.js new file mode 100644 index 0000000..f355699 --- /dev/null +++ b/apps/anti-abuse-oracle/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['custom-server'] +} diff --git a/apps/anti-abuse-oracle/package.json b/apps/anti-abuse-oracle/package.json new file mode 100644 index 0000000..cee89f3 --- /dev/null +++ b/apps/anti-abuse-oracle/package.json @@ -0,0 +1,46 @@ +{ + "name": "@pedalboard/anti-abuse-oracle", + "version": "0.0.30", + "private": true, + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "dev": "ts-node-dev --respawn ./src/index.ts", + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "lint": "tsc --noEmit && eslint \"src/**/*.ts*\"", + "start": "node ./dist/index.js", + "test": "jest --detectOpenHandles" + }, + "jest": { + "preset": "jest-presets/jest/node" + }, + "dependencies": { + "@pedalboard/basekit": "*", + "@pedalboard/logger": "*", + "@pedalboard/storage": "*", + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "express": "^4.17.1", + "morgan": "^1.10.0", + "@audius/sdk": "5.0.0" + }, + "devDependencies": { + "@types/body-parser": "^1.19.0", + "@types/cors": "^2.8.10", + "@types/express": "^4.17.12", + "@types/jest": "^26.0.22", + "@types/morgan": "^1.9.2", + "@types/node": "^15.12.2", + "@types/supertest": "^2.0.11", + "esbuild": "^0.14.38", + "esbuild-register": "^3.3.2", + "eslint": "8.56.0", + "eslint-config-custom-server": "*", + "jest": "^26.6.3", + "jest-presets": "*", + "nodemon": "^2.0.15", + "supertest": "^6.1.3", + "tsconfig": "*", + "typescript": "^4.5.3" + } +} diff --git a/apps/anti-abuse-oracle/src/config.ts b/apps/anti-abuse-oracle/src/config.ts new file mode 100644 index 0000000..5afc7c0 --- /dev/null +++ b/apps/anti-abuse-oracle/src/config.ts @@ -0,0 +1,73 @@ +import { PublicKey } from '@solana/web3.js' +import dotenv from 'dotenv' +import { cleanEnv, str, num } from 'envalid' + +import { logger } from './logger' + +export const LISTENS_RATE_LIMIT_IP_PREFIX = 'listens-rate-limit-ip' +export const LISTENS_RATE_LIMIT_TRACK_PREFIX = 'listens-rate-limit-track' + +export const ClockProgram = new PublicKey( + 'SysvarC1ock11111111111111111111111111111111' +) +export const InstructionsProgram = new PublicKey( + 'Sysvar1nstructions1111111111111111111111111' +) + +// reads .env file based on environment +const readDotEnv = () => { + const environment = process.env.audius_discprov_env || 'dev' + const dotenvConfig = (filename: string) => + dotenv.config({ path: `${filename}.env`, override: true }) + logger.info(`running on ${environment} network`) + dotenvConfig(environment) +} + +type Config = { + environment: string + discoveryDbConnectionString: string + redisUrl: string + serverHost: string + serverPort: number + privateSignerAddress: string +} + +let cachedConfig: Config | null = null + +const readConfig = (): Config => { + if (cachedConfig !== null) return cachedConfig + readDotEnv() + + // validate env + const env = cleanEnv(process.env, { + audius_discprov_env: str({ + default: 'dev' + }), + audius_discprov_url: str({ + default: 'http://audius-protocol-discovery-provider-1' + }), + audius_db_url: str({ + default: + 'postgresql+psycopg2://postgres:postgres@db:5432/discovery_provider_1' + }), + audius_redis_url: str({ + default: 'redis://audius-protocol-discovery-provider-redis-1:6379/00' + }), + anti_abuse_oracle_server_host: str({ default: '0.0.0.0' }), + anti_abuse_oracle_server_port: num({ default: 6003 }), + + private_signer_address: str({ default: '' }) + }) + + cachedConfig = { + environment: env.audius_discprov_env, + discoveryDbConnectionString: env.audius_db_url, + redisUrl: env.audius_redis_url, + serverHost: env.anti_abuse_oracle_server_host, + serverPort: env.anti_abuse_oracle_server_port, + privateSignerAddress: env.private_signer_address + } + return readConfig() +} + +export const config = readConfig() diff --git a/apps/anti-abuse-oracle/src/index.ts b/apps/anti-abuse-oracle/src/index.ts new file mode 100644 index 0000000..4d3b67f --- /dev/null +++ b/apps/anti-abuse-oracle/src/index.ts @@ -0,0 +1,93 @@ +import express from 'express' +import { config } from './config' +import { logger } from './logger' +import { knex } from 'knex' +import { SolanaUtils } from '@audius/sdk' +import bn from 'bn.js' + +// Initialize Knex +const db = knex({ + client: 'pg', // Change to 'mysql' or other if needed + connection: config.discoveryDbConnectionString +}) + +const main = async () => { + const { serverHost, serverPort } = config + + const app = express() + app.use(express.json()) + app.post('/attestation/:handle', async (req, res) => { + const { + body: { challengeId, challengeSpecifier, amount }, + params: { handle } + } = req + if ( + !challengeId || + !challengeSpecifier || + amount === null || + amount === undefined + ) { + res.status(500).json({ error: 'Missing body parameters' }) + } + + const userWalletWithPlays = await db('users') + .join('plays', 'plays.user_id', '=', 'users.user_id') + .where('users.handle_lc', handle.toLowerCase()) + .select('users.wallet') + .first() + + if (!userWalletWithPlays) { + res.json({ result: false }) + } + + try { + const bnAmount = SolanaUtils.uiAudioToBNWaudio(amount) + const identifier = SolanaUtils.constructTransferId( + challengeId, + challengeSpecifier + ) + const toSignStr = SolanaUtils.constructAttestation( + userWalletWithPlays.wallet, + bnAmount, + identifier + ) + const { signature, recoveryId } = SolanaUtils.signBytes( + Buffer.from(toSignStr), + config.privateSignerAddress + ) + const result = new bn(Uint8Array.of(...signature, recoveryId)).toString( + 'hex' + ) + + res.json({ result }) + } catch (error) { + logger.error(`Something went wrong: ${error}`) + } + }) + + app.get('/attestation/:handle', async (req, res) => { + const { handle } = req.params + + try { + // Query database for attestation matching the handle + const hasPlays = await db('users') + .join('plays', 'plays.user_id', '=', 'users.user_id') + .where('users.handle_lc', handle.toLowerCase()) + .first() // Returns undefined if no match + if (!hasPlays) { + res.json({ isOk: false }) + } + + res.json({ isOk: true }) + } catch (error) { + logger.error({ error }, 'Database query failed') + res.status(500).json({ error: 'Internal server error' }) + } + }) + + app.listen(serverPort, serverHost, () => { + logger.info({ serverHost, serverPort }, 'server initialized') + }) +} + +main().catch((e) => logger.error({ error: e }, 'Fatal error in main!')) diff --git a/apps/anti-abuse-oracle/src/logger.ts b/apps/anti-abuse-oracle/src/logger.ts new file mode 100644 index 0000000..1231f00 --- /dev/null +++ b/apps/anti-abuse-oracle/src/logger.ts @@ -0,0 +1,16 @@ +import pino, { stdTimeFunctions } from 'pino' + +const formatters = { + level(label: string) { + // Set level to string format + return { level: label.toUpperCase() } + } +} + +// set config for logger here +export const logger = pino({ + name: `anti-abuse-oracle`, + base: undefined, + timestamp: stdTimeFunctions.isoTime, + formatters +}) diff --git a/apps/anti-abuse-oracle/tsconfig.json b/apps/anti-abuse-oracle/tsconfig.json new file mode 100644 index 0000000..d5f3855 --- /dev/null +++ b/apps/anti-abuse-oracle/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "module": "CommonJS", + "outDir": "./dist", + "rootDir": "./src" + }, + "exclude": ["node_modules"], + "extends": "../../packages/tsconfig/base.json", + "include": ["src"] +} From 127318d3a39dbaa0f4f684d14d16bfbacc8a6e4c Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Tue, 25 Feb 2025 12:52:44 -0800 Subject: [PATCH 02/34] Fix aao plugin build (#11461) --- apps/anti-abuse-oracle/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/anti-abuse-oracle/package.json b/apps/anti-abuse-oracle/package.json index cee89f3..a1cabe1 100644 --- a/apps/anti-abuse-oracle/package.json +++ b/apps/anti-abuse-oracle/package.json @@ -41,6 +41,7 @@ "nodemon": "^2.0.15", "supertest": "^6.1.3", "tsconfig": "*", - "typescript": "^4.5.3" + "typescript": "^4.5.3", + "envalid": "8.0.0" } } From d4b1f1c265cb11e883adb6bd94c633ee775254a0 Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Wed, 26 Feb 2025 14:16:21 -0800 Subject: [PATCH 03/34] Setup AAO plugin routing (#11472) --- apps/anti-abuse-oracle/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/anti-abuse-oracle/src/index.ts b/apps/anti-abuse-oracle/src/index.ts index 4d3b67f..4ec38cf 100644 --- a/apps/anti-abuse-oracle/src/index.ts +++ b/apps/anti-abuse-oracle/src/index.ts @@ -4,6 +4,7 @@ import { logger } from './logger' import { knex } from 'knex' import { SolanaUtils } from '@audius/sdk' import bn from 'bn.js' +import cors from 'cors' // Initialize Knex const db = knex({ @@ -16,6 +17,7 @@ const main = async () => { const app = express() app.use(express.json()) + app.use(cors()) app.post('/attestation/:handle', async (req, res) => { const { body: { challengeId, challengeSpecifier, amount }, From 8cbafd91b2dad6133f48b6bfc440c76262b62607 Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Tue, 4 Mar 2025 15:50:42 -0500 Subject: [PATCH 04/34] AAO UI (#11499) Co-authored-by: isaac --- apps/anti-abuse-oracle/package.json | 10 +- apps/anti-abuse-oracle/src/actionLog.ts | 477 ++++++++++++++++++++++ apps/anti-abuse-oracle/src/identity.ts | 50 +++ apps/anti-abuse-oracle/src/index.ts | 95 ----- apps/anti-abuse-oracle/src/server.tsx | 516 ++++++++++++++++++++++++ apps/anti-abuse-oracle/tsconfig.json | 4 +- 6 files changed, 1053 insertions(+), 99 deletions(-) create mode 100644 apps/anti-abuse-oracle/src/actionLog.ts create mode 100644 apps/anti-abuse-oracle/src/identity.ts delete mode 100644 apps/anti-abuse-oracle/src/index.ts create mode 100644 apps/anti-abuse-oracle/src/server.tsx diff --git a/apps/anti-abuse-oracle/package.json b/apps/anti-abuse-oracle/package.json index a1cabe1..40aefa7 100644 --- a/apps/anti-abuse-oracle/package.json +++ b/apps/anti-abuse-oracle/package.json @@ -5,24 +5,28 @@ "scripts": { "build": "tsc", "clean": "rm -rf dist", - "dev": "ts-node-dev --respawn ./src/index.ts", + "dev": "ts-node-dev --respawn ./src/server.tsx", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "lint": "tsc --noEmit && eslint \"src/**/*.ts*\"", - "start": "node ./dist/index.js", + "start": "node ./dist/server.js", "test": "jest --detectOpenHandles" }, "jest": { "preset": "jest-presets/jest/node" }, "dependencies": { + "@audius/sdk": "5.0.0", + "@hono/node-server": "^1.13.7", "@pedalboard/basekit": "*", "@pedalboard/logger": "*", "@pedalboard/storage": "*", "body-parser": "^1.19.0", "cors": "^2.8.5", + "dotenv": "^16.4.7", "express": "^4.17.1", + "hono": "^4.6.17", "morgan": "^1.10.0", - "@audius/sdk": "5.0.0" + "postgres": "^3.4.5" }, "devDependencies": { "@types/body-parser": "^1.19.0", diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts new file mode 100644 index 0000000..43b201a --- /dev/null +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -0,0 +1,477 @@ +import 'dotenv/config' + +import postgres from 'postgres' +import fetch from 'node-fetch' +import { useFingerprintDeviceCount } from './identity' + +export const sql = postgres(process.env.discoveryDbUrl || '') + +const MIN_SCORE = -100 +const MAX_SCORE = 300 + +type TipRow = { + sender: UserDetails + receiver: UserDetails + amount: number + timestamp: Date +} + +export async function recentTips() { + const tips = await sql` + select + ( + ${sql.unsafe(buildUserDetails)} + where user_id = sender_user_id + ) as sender, + ( + ${sql.unsafe(buildUserDetails)} + where user_id = receiver_user_id + ) as receiver, + amount, + created_at as "timestamp" + from user_tips + order by slot desc limit 1000;` + return tips +} + +// +// User Details +// +export type UserDetails = { + id: number + handle: string + name: string + img: string + isVerified: boolean + + amount?: number +} + +const buildUserDetails = ` +select json_build_object( + 'id', user_id, + 'handle', handle, + 'name', name, + 'isVerified', is_verified, + 'img', profile_picture_sizes +) as user +from users +` + +export async function getUser(handle: string) { + const rows = await sql` + ${sql.unsafe(buildUserDetails)} + where + handle_lc = ${handle.toLowerCase()} + LIMIT 1 + ` + if (!rows.length) return + return rows[0].user as UserDetails +} + +export async function queryUsers({ ids }: { ids: number[] }) { + const rows = await sql` + ${sql.unsafe(buildUserDetails)} + where + user_id in ${sql(ids)} + ` + return rows.map((r) => r.user) as UserDetails[] +} + +export async function getRecentUsers(page: number) { + const rows = await sql` + ${sql.unsafe(buildUserDetails)} + where handle_lc is not null + order by created_at desc + LIMIT 10 OFFSET ${(page + 27) * 10} + ` + if (!rows.length) return + return rows.map((row) => row.user as UserDetails) +} + +export async function getUserScore(userId: number) { + const rows = await sql` + select + track_count > 0 as "hasTracks", + playlist_count > 0 as "hasPlaylists", + track_save_count > 1 as "saveCount", + repost_count > 1 as "repostCount", + following_count > 6 as "followingCount", + (select count(*) > 0 from plays where user_id = ${userId}) as "hasPlays" + from aggregate_user + where user_id = ${userId} + ` + if (!rows.length) return + return rows[0] +} + +export async function getUserNormalizedScore(userId: number) { + const rows = await sql` +WITH scoped_users as ( +select * from users +where user_id = ${userId} +order by created_at desc +limit 100 +), +play_activity AS ( + SELECT user_id, + COUNT(DISTINCT date_trunc('minute', plays.created_at)) AS play_count + FROM plays + WHERE user_id IS NOT NULL + AND user_id in (select user_id from scoped_users) + GROUP BY user_id +), +fast_challenge_completion AS ( + SELECT users.user_id, + handle_lc, + users.created_at, + COUNT(*) AS challenge_count, + ARRAY_AGG(user_challenges.challenge_id) AS challenge_ids + FROM users + LEFT JOIN user_challenges ON users.user_id = user_challenges.user_id + WHERE user_challenges.is_complete + AND user_challenges.completed_at - users.created_at <= INTERVAL '3 minutes' + AND user_challenges.challenge_id not in ('m', 'b') + AND users.user_id in (select user_id from scoped_users) + GROUP BY users.user_id, users.handle_lc, users.created_at + ORDER BY users.created_at DESC +), +aggregate_scores AS ( + SELECT + users.handle_lc, + users.created_at, + COALESCE(play_activity.play_count, 0) AS play_count, + COALESCE(fast_challenge_completion.challenge_count, 0) AS challenge_count, + COALESCE(aggregate_user.following_count, 0) AS following_count, + COALESCE(aggregate_user.follower_count, 0) AS follower_count + FROM users + LEFT JOIN play_activity ON users.user_id = play_activity.user_id + LEFT JOIN fast_challenge_completion ON users.user_id = fast_challenge_completion.user_id + LEFT JOIN aggregate_user ON aggregate_user.user_id = users.user_id + WHERE users.handle_lc IS NOT NULL + AND users.user_id in (select user_id from scoped_users) + ORDER BY users.created_at DESC +) +SELECT + a.handle_lc, + a.created_at as "timestamp", + a.play_count, + a.follower_count, + a.challenge_count, + a.following_count +FROM aggregate_scores a + ` + const { + handle_lc, + timestamp, + play_count, + follower_count, + challenge_count, + following_count + } = rows[0] + + // Convert values to numbers + const playCount = Number(play_count) + const followerCount = Number(follower_count) + const challengeCount = Number(challenge_count) + const followingCount = Number(following_count) + + const numberOfUserWithFingerprint = (await useFingerprintDeviceCount(userId))! + + const overallScore = + playCount + + followerCount - + challengeCount - + (followingCount < 5 ? -1 : 0) - + numberOfUserWithFingerprint + const normalizedScore = Math.min( + (overallScore - MIN_SCORE) / (MAX_SCORE - MIN_SCORE), + 1 + ) + return { + handleLowerCase: handle_lc, + timestamp, + playCount: play_count, + followerCount: follower_count, + challengeCount: challenge_count, + followingCount: following_count, + fingerprintCount: numberOfUserWithFingerprint, + overallScore, + normalizedScore + } +} + +export async function getAAOAttestation(handle: string) { + const url = `https://antiabuseoracle.audius.co/abuse/${handle}` + try { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Error fetching attestation: ${response.statusText}`) + } + const data = await response.json() + const flagged = data.some( + (item: { trigger: boolean; action: string; rule: number }) => + item.trigger === true && + item.action === 'fail' && + item.rule in [0, 0, 1, 2, 2, 3, 4, 8, 10, 11, 12, 13, 16, 18] + ) + return flagged + } catch (error) { + console.error('Error fetching AAO attestation:', error, handle) + } +} +// +// TrackDetails +// +export type TrackDetails = { + id: string + title: string + img: string +} + +const buildTrackDetails = ` +select json_build_object( + 'id', track_id, + 'title', title, + 'img', cover_art_sizes, + 'artist_handle', handle, + 'artist_name', name +) +from tracks +join users on owner_id = user_id +` + +// +// PlaylistDetails +// +export type PlaylistDetails = { + id: number + title: string + img: string +} + +const buildPlaylistDetails = ` +select json_build_object( + 'id', playlist_id, + 'title', playlist_name, + 'img', playlist_image_sizes_multihash +) +from playlists +` + +// +// ActionRow +// +export type ActionRow = { + timestamp: Date + verb: string + target: string + details: Record +} + +export async function actionLogForUser(userId: number) { + const limit = 100 + return await sql` + +with separate_actions as ( + +-- follow user + ( + select + created_at as "timestamp", + 'follow' as "verb", + 'user' as "target", + ( + ${sql.unsafe(buildUserDetails)} + where users.user_id = follows.followee_user_id + ) as "details" + from + follows + where follower_user_id = ${userId} + and is_delete = false + order by created_at desc + limit ${limit} + ) + +-- user sign up +union all + ( + select + created_at, + 'sign up', + 'user', + json_build_object( + ) + from + users + where user_id = ${userId} + order by created_at desc + limit ${limit} + ) + +-- repost track +union all + + ( + select + created_at, + 'repost', + 'track', + ( + ${sql.unsafe(buildTrackDetails)} + where track_id = repost_item_id + ) + from + reposts + where user_id = ${userId} + and repost_type = 'track' + and is_delete = false + order by created_at desc + limit ${limit} + ) + +-- repost playlist +union all + + ( + select + created_at, + 'repost', + 'playlist', + ( + ${sql.unsafe(buildPlaylistDetails)} + where playlist_id = repost_item_id + ) + from + reposts + where user_id = ${userId} + and repost_type = 'playlist' + and is_delete = false + order by created_at desc + limit ${limit} + ) + +-- save track +union all + + ( + select + created_at, + 'save', + 'track', + ( + ${sql.unsafe(buildTrackDetails)} + where track_id = save_item_id + ) + from + saves + where user_id = ${userId} + and save_type = 'track' + and is_delete = false + order by created_at desc + limit ${limit} + ) + +-- save playlist +union all + + ( + select + created_at, + 'save', + 'playlist', + ( + ${sql.unsafe(buildPlaylistDetails)} + where playlist_id = save_item_id + ) + from + saves + where user_id = ${userId} + and save_type = 'playlist' + and is_delete = false + order by created_at desc + limit ${limit} + ) + +-- create track +-- create playlist + +-- tip +union all + ( + select + created_at, + 'tip', + 'user', + ( + select json_build_object( + 'id', user_id, + 'handle', handle, + 'name', name, + 'img', profile_picture_sizes, + 'amount', amount + ) + from users + where user_id = receiver_user_id + ) + from user_tips + where sender_user_id = ${userId} + order by created_at desc + limit ${limit} + ) + +-- listens +union all + ( + select + created_at, + 'listen', + 'track', + ( + ${sql.unsafe(buildTrackDetails)} + where track_id = play_item_id + ) + from plays + where user_id = ${userId} + order by created_at desc + limit ${limit} + ) + +-- challenge +union all + ( + select + completed_at as created_at, + 'completed challenge', + challenge_id, + json_build_object( + 'amount', amount + ) + from user_challenges + where user_id = ${userId} + and is_complete + order by completed_at desc + limit ${limit} + ) + +-- challenge_disbursements +union all + ( + select + created_at, + 'disbursed challenge', + challenge_id, + json_build_object( + 'amount', amount + ) + from challenge_disbursements + where user_id = ${userId} + order by created_At desc + limit ${limit} + ) + +) + +select * from separate_actions order by timestamp desc; +` +} diff --git a/apps/anti-abuse-oracle/src/identity.ts b/apps/anti-abuse-oracle/src/identity.ts new file mode 100644 index 0000000..02a80a3 --- /dev/null +++ b/apps/anti-abuse-oracle/src/identity.ts @@ -0,0 +1,50 @@ +import 'dotenv/config' + +import postgres from 'postgres' + +export const sql = postgres(process.env.identityDbUrl || '') + +type FingerprintCount = { + fingerprint: string + userCount: number + userIds: number[] +} + +export async function userFingerprints(userId: number) { + const rows: FingerprintCount[] = await sql` + select + "visitorId" as "fingerprint", + count(distinct "userId") as "userCount", + array_agg("userId") as "userIds" + from "Fingerprints" + where "visitorId" in ( + select "visitorId" from "Fingerprints" where "userId" = ${userId} + ) + group by 1 order by 2 desc limit 90; + ` + + for (const row of rows) { + row.userIds.sort() + } + + return rows +} + +export async function useFingerprintDeviceCount(userId: number) { + const rows = await sql` + SELECT + MAX("userCount") AS "maxUserCount" + FROM ( + SELECT + "visitorId", + COUNT(DISTINCT "userId") AS "userCount" + FROM "Fingerprints" + WHERE "visitorId" IN ( + SELECT "visitorId" FROM "Fingerprints" WHERE "userId" = ${userId} + ) + GROUP BY "visitorId" + ) t; + ` + console.log('asdf rows: ', rows) + return rows[0].maxUserCount ?? 0 +} diff --git a/apps/anti-abuse-oracle/src/index.ts b/apps/anti-abuse-oracle/src/index.ts deleted file mode 100644 index 4ec38cf..0000000 --- a/apps/anti-abuse-oracle/src/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -import express from 'express' -import { config } from './config' -import { logger } from './logger' -import { knex } from 'knex' -import { SolanaUtils } from '@audius/sdk' -import bn from 'bn.js' -import cors from 'cors' - -// Initialize Knex -const db = knex({ - client: 'pg', // Change to 'mysql' or other if needed - connection: config.discoveryDbConnectionString -}) - -const main = async () => { - const { serverHost, serverPort } = config - - const app = express() - app.use(express.json()) - app.use(cors()) - app.post('/attestation/:handle', async (req, res) => { - const { - body: { challengeId, challengeSpecifier, amount }, - params: { handle } - } = req - if ( - !challengeId || - !challengeSpecifier || - amount === null || - amount === undefined - ) { - res.status(500).json({ error: 'Missing body parameters' }) - } - - const userWalletWithPlays = await db('users') - .join('plays', 'plays.user_id', '=', 'users.user_id') - .where('users.handle_lc', handle.toLowerCase()) - .select('users.wallet') - .first() - - if (!userWalletWithPlays) { - res.json({ result: false }) - } - - try { - const bnAmount = SolanaUtils.uiAudioToBNWaudio(amount) - const identifier = SolanaUtils.constructTransferId( - challengeId, - challengeSpecifier - ) - const toSignStr = SolanaUtils.constructAttestation( - userWalletWithPlays.wallet, - bnAmount, - identifier - ) - const { signature, recoveryId } = SolanaUtils.signBytes( - Buffer.from(toSignStr), - config.privateSignerAddress - ) - const result = new bn(Uint8Array.of(...signature, recoveryId)).toString( - 'hex' - ) - - res.json({ result }) - } catch (error) { - logger.error(`Something went wrong: ${error}`) - } - }) - - app.get('/attestation/:handle', async (req, res) => { - const { handle } = req.params - - try { - // Query database for attestation matching the handle - const hasPlays = await db('users') - .join('plays', 'plays.user_id', '=', 'users.user_id') - .where('users.handle_lc', handle.toLowerCase()) - .first() // Returns undefined if no match - if (!hasPlays) { - res.json({ isOk: false }) - } - - res.json({ isOk: true }) - } catch (error) { - logger.error({ error }, 'Database query failed') - res.status(500).json({ error: 'Internal server error' }) - } - }) - - app.listen(serverPort, serverHost, () => { - logger.info({ serverHost, serverPort }, 'server initialized') - }) -} - -main().catch((e) => logger.error({ error: e }, 'Fatal error in main!')) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx new file mode 100644 index 0000000..82b37ed --- /dev/null +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -0,0 +1,516 @@ +import { serve } from '@hono/node-server' +import { Hono } from 'hono' +import { basicAuth } from 'hono/basic-auth' +import { + actionLogForUser, + getUser, + getRecentUsers, + getUserNormalizedScore, + getUserScore, + recentTips, + sql, + type ActionRow, + type TrackDetails, + type UserDetails, + getAAOAttestation, + queryUsers +} from './actionLog' +import { logger } from 'hono/logger' +import { config } from './config' +import { SolanaUtils, Utils } from '@audius/sdk' +import bn from 'bn.js' +import { userFingerprints } from './identity' + +let CONTENT_NODE = 'https://creatornode2.audius.co' +let FRONTEND = 'https://audius.co' + +if (config.environment == 'stage') { + CONTENT_NODE = 'https://creatornode10.staging.audius.co' + FRONTEND = 'https://staging.audius.co' +} + +let { AAO_AUTH_USER, AAO_AUTH_PASSWORD } = process.env +if (!AAO_AUTH_USER) { + AAO_AUTH_USER = 'test' + console.warn('AAO_AUTH_USER not set. Falling back to: ', AAO_AUTH_USER) +} +if (!AAO_AUTH_PASSWORD) { + AAO_AUTH_PASSWORD = 'test' + console.warn( + 'AAO_AUTH_PASSWORD not set. Falling back to: ', + AAO_AUTH_PASSWORD + ) +} + +const app = new Hono() + +app.use(logger()) + +app.all('/attestation/:handle', async (c) => { + const handle = c.req.param('handle').toLowerCase() + const { challengeId, challengeSpecifier, amount } = await c.req.json() + + const users = + await sql`select user_id, wallet from users where handle_lc = ${handle}` + const user = users[0] + if (!user) return c.text(`handle not found: ${handle}`, 404) + + // TODO: check score + + try { + const bnAmount = SolanaUtils.uiAudioToBNWaudio(amount) + const identifier = SolanaUtils.constructTransferId( + challengeId, + challengeSpecifier + ) + const toSignStr = SolanaUtils.constructAttestation( + user.wallet, + bnAmount, + identifier + ) + const { signature, recoveryId } = SolanaUtils.signBytes( + Buffer.from(toSignStr), + config.privateSignerAddress + ) + const result = new bn(Uint8Array.of(...signature, recoveryId)).toString( + 'hex' + ) + + return c.json({ result }) + } catch (error) { + console.log(`Something went wrong: ${error}`) + return c.text(`Something went wrong`, 500) + } +}) + +// +// UI +// + +app.use( + '/attestation/ui/*', + basicAuth({ + username: AAO_AUTH_USER, + password: AAO_AUTH_PASSWORD + }) +) + +app.get('/attestation/ui', async (c) => { + const tips = await recentTips() + + let lastDate = '' + function dateHeader(timestamp: Date) { + const d = timestamp.toDateString() + if (d != lastDate) { + lastDate = d + return ( + <> + + +
{d}
+ + + + Timestamp + Sender + Receiver + Amount + + + ) + } + return null + } + + return c.html( + +

Recent Tips

+ + + {tips.map((tip) => ( + <> + {dateHeader(tip.timestamp)} + + + + + + + + ))} + +
{tip.timestamp.toLocaleTimeString()} + + {tip.sender.handle} + + + + {tip.receiver.handle} + + {tip.amount / 100_000_000}
+
+ ) +}) + +app.get('/attestation/ui/user', async (c) => { + const idOrHandle = c.req.query('q') || '1' + const user = await getUser(idOrHandle) + if (!user) return c.text(`user id not found: ${idOrHandle}`, 404) + const signals = await getUserScore(user.id) + const userScore = (await getUserNormalizedScore(user.id))! + + if (!signals) return c.text(`user id not found: ${idOrHandle}`, 404) + + const fingerprints = await userFingerprints(user.id) + const fingerprintUsers = await queryUsers({ + ids: fingerprints.flatMap((f) => f.userIds) + }) + + let lastDate = '' + function dateHeader(timestamp: Date) { + const d = timestamp.toDateString() + if (d != lastDate) { + lastDate = d + return ( + + +
{d}
+ + + ) + } + return null + } + + const rows = await actionLogForUser(user.id) + return c.html( + +
+
+ +
+
{user.name}
+
+ +
{user.id}
+
{Utils.encodeHashId(user.id)}
+ {user.isVerified &&
Verified
} +
+
+
+ {(userScore.normalizedScore * 100).toFixed(0)}% +
{' '} + {Object.entries(signals).map(([name, ok]) => ( +
+ {name} +
+ ))} +
+
+
+

Score Breakdown

+ + + + + + + + + + + + + + + + + + + + + +
Play CountFollower CountFast Challenge CountFollowing CountFingerprint CountOverall Score
{userScore.playCount}{userScore.followerCount}{userScore.challengeCount}{userScore.followingCount}{userScore.fingerprintCount}{userScore.overallScore}
+ +

Fingerprints

+ + + + + + + + + + {fingerprints.map((f) => ( + + + + + + ))} + +
FingerprintUser CountUsers
{f.fingerprint}{f.userCount} + {f.userIds + .slice(0, 20) + .map((id) => fingerprintUsers.find((u) => u.id == id)) + .filter(Boolean) + .map((u) => ( + + {u!.handle} + + ))} +
+ +

Actions

+ + {rows.map((r) => ( + <> + {dateHeader(r.timestamp)} + + + + + + + + ))} +
{r.timestamp.toLocaleTimeString()}{r.verb}{r.target}{renderDetails(r)}
+
+ +
+ ) +}) + +app.get('/attestation/ui/recent-users', async (c) => { + const page = parseInt(c.req.query('page') || '1') + const recentUsers = await getRecentUsers(page) + const userScores = recentUsers + ? await Promise.all( + recentUsers.map(async (user) => { + const [userScore, flagged] = await Promise.all([ + getUserNormalizedScore(user.id), + getAAOAttestation(user.handle) + ]) + return { + ...userScore, + flagged + } + }) + ) + : [] + + let lastDate = '' + function dateHeader(timestamp: Date) { + const d = timestamp.toDateString() + if (d != lastDate) { + lastDate = d + return ( + <> + + +
{d}
+ + + + Timestamp + Handle + Listen Activity + Follower Count + Following Count + Fast Challenges + Overall Score + Normalized Score + + + ) + } + return null + } + + return c.html( + +

Recent Users

+ + + {userScores.map((userScore) => ( + <> + {dateHeader(userScore.timestamp)} + + + + + + + + + + + + ))} + +
{userScore.timestamp.toLocaleTimeString()} + + {userScore.handleLowerCase} + + {userScore.playCount}{userScore.followerCount}{userScore.followingCount}{userScore.challengeCount}{userScore.overallScore}{userScore.normalizedScore}
+ + +
+ ) +}) + +function renderDetails(row: ActionRow) { + switch (row.target) { + case 'track': + case 'playlist': { + const track = row.details as TrackDetails + return ( +
+ {/* */} + {track.title} +
+ ) + } + case 'user': { + const user = row.details as UserDetails + return ( +
+ {/* */} + {user.amount &&
${user.amount / 100_000_000}
} + +
+ ) + } + default: + return
{JSON.stringify(row.details)}
+ } +} + +function Image({ img, size }: { img: string; size?: number }) { + size ||= 50 + if (!img) + return ( +
+ ) + return ( + + ) +} + +type LayoutProps = { + title?: string + container?: boolean + // eslint-disable-next-line @typescript-eslint/no-explicit-any + children: any +} +function Layout(props: LayoutProps) { + return ( + + + {props.title || 'AAO'} + + + + + + + +
+ +
+
+ AAO +
+
+ +
+ + Recent Users + +
+
+ {props.children} +
+ +
+ + + ) +} + +const port = config.serverPort || 4200 +serve( + { + fetch: app.fetch, + port + }, + (info) => { + console.log(`Server is running on http://localhost:${info.port}`) + } +) diff --git a/apps/anti-abuse-oracle/tsconfig.json b/apps/anti-abuse-oracle/tsconfig.json index d5f3855..a7cf2bb 100644 --- a/apps/anti-abuse-oracle/tsconfig.json +++ b/apps/anti-abuse-oracle/tsconfig.json @@ -3,7 +3,9 @@ "lib": ["es2020"], "module": "CommonJS", "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" }, "exclude": ["node_modules"], "extends": "../../packages/tsconfig/base.json", From aac03f735ce9b08f186dcde048075a91c397d6bb Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Tue, 4 Mar 2025 13:44:54 -0800 Subject: [PATCH 05/34] Use cross fetch in AAO plugin (#11505) --- apps/anti-abuse-oracle/package.json | 3 ++- apps/anti-abuse-oracle/src/actionLog.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/anti-abuse-oracle/package.json b/apps/anti-abuse-oracle/package.json index 40aefa7..c56866a 100644 --- a/apps/anti-abuse-oracle/package.json +++ b/apps/anti-abuse-oracle/package.json @@ -26,7 +26,8 @@ "express": "^4.17.1", "hono": "^4.6.17", "morgan": "^1.10.0", - "postgres": "^3.4.5" + "postgres": "^3.4.5", + "cross-fetch": "4.0.0" }, "devDependencies": { "@types/body-parser": "^1.19.0", diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index 43b201a..7c53b2d 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import postgres from 'postgres' -import fetch from 'node-fetch' +import fetch from 'cross-fetch' import { useFingerprintDeviceCount } from './identity' export const sql = postgres(process.env.discoveryDbUrl || '') From ee28cf3f62ade833d95e11c2ce2cc1112e58211a Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Wed, 5 Mar 2025 12:08:31 -0800 Subject: [PATCH 06/34] Add cors to attestation plugin (#11509) --- apps/anti-abuse-oracle/src/actionLog.ts | 2 +- apps/anti-abuse-oracle/src/identity.ts | 2 +- apps/anti-abuse-oracle/src/server.tsx | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index 7c53b2d..6c94670 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -4,7 +4,7 @@ import postgres from 'postgres' import fetch from 'cross-fetch' import { useFingerprintDeviceCount } from './identity' -export const sql = postgres(process.env.discoveryDbUrl || '') +export const sql = postgres(process.env.audius_db_url || '') const MIN_SCORE = -100 const MAX_SCORE = 300 diff --git a/apps/anti-abuse-oracle/src/identity.ts b/apps/anti-abuse-oracle/src/identity.ts index 02a80a3..0d5ab75 100644 --- a/apps/anti-abuse-oracle/src/identity.ts +++ b/apps/anti-abuse-oracle/src/identity.ts @@ -2,7 +2,7 @@ import 'dotenv/config' import postgres from 'postgres' -export const sql = postgres(process.env.identityDbUrl || '') +export const sql = postgres(process.env.IDENTITY_DB_URL || '') type FingerprintCount = { fingerprint: string diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 82b37ed..cf49ec2 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -20,6 +20,7 @@ import { config } from './config' import { SolanaUtils, Utils } from '@audius/sdk' import bn from 'bn.js' import { userFingerprints } from './identity' +import { cors } from 'hono/cors' let CONTENT_NODE = 'https://creatornode2.audius.co' let FRONTEND = 'https://audius.co' @@ -45,8 +46,9 @@ if (!AAO_AUTH_PASSWORD) { const app = new Hono() app.use(logger()) +app.use('/attestation/*', cors()) -app.all('/attestation/:handle', async (c) => { +app.post('/attestation/:handle', async (c) => { const handle = c.req.param('handle').toLowerCase() const { challengeId, challengeSpecifier, amount } = await c.req.json() From dcaac1a17fd28a4e9428e5a00d5fb142353f2980 Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Thu, 6 Mar 2025 16:09:22 -0500 Subject: [PATCH 07/34] Use score in aao response (#11522) --- apps/anti-abuse-oracle/src/server.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index cf49ec2..0e49f82 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -55,9 +55,13 @@ app.post('/attestation/:handle', async (c) => { const users = await sql`select user_id, wallet from users where handle_lc = ${handle}` const user = users[0] - if (!user) return c.text(`handle not found: ${handle}`, 404) + if (!user) return c.json({ error: `handle not found: ${handle}` }, 404) - // TODO: check score + // pass / fail + const userScore = await getUserNormalizedScore(user.id) + if (userScore.overallScore < 0) { + return c.json({ error: 'denied' }, 400) + } try { const bnAmount = SolanaUtils.uiAudioToBNWaudio(amount) @@ -81,7 +85,7 @@ app.post('/attestation/:handle', async (c) => { return c.json({ result }) } catch (error) { console.log(`Something went wrong: ${error}`) - return c.text(`Something went wrong`, 500) + return c.json({ error: `Something went wrong` }, 500) } }) From e4743df0a4482758ef5f081e1a3155f36d328db1 Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Mon, 10 Mar 2025 18:25:31 -0400 Subject: [PATCH 08/34] fix attest endpoint (#11552) --- apps/anti-abuse-oracle/src/server.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 0e49f82..96b3fd4 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -58,7 +58,7 @@ app.post('/attestation/:handle', async (c) => { if (!user) return c.json({ error: `handle not found: ${handle}` }, 404) // pass / fail - const userScore = await getUserNormalizedScore(user.id) + const userScore = await getUserNormalizedScore(user.user_id) if (userScore.overallScore < 0) { return c.json({ error: 'denied' }, 400) } From bc81da8576c05fff2177a4babe943f1c720b5464 Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Tue, 11 Mar 2025 12:12:01 -0700 Subject: [PATCH 09/34] Add user scoring for in app notif (#11570) --- apps/anti-abuse-oracle/src/actionLog.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index 6c94670..4b0a3c3 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -83,7 +83,7 @@ export async function getRecentUsers(page: number) { ${sql.unsafe(buildUserDetails)} where handle_lc is not null order by created_at desc - LIMIT 10 OFFSET ${(page + 27) * 10} + LIMIT 10 OFFSET ${page * 10} ` if (!rows.length) return return rows.map((row) => row.user as UserDetails) @@ -181,7 +181,7 @@ FROM aggregate_scores a const overallScore = playCount + followerCount - - challengeCount - + challengeCount + (followingCount < 5 ? -1 : 0) - numberOfUserWithFingerprint const normalizedScore = Math.min( From 19cc00f9d1061db1aa41f4c1d0520c8dc83c0b2b Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Thu, 13 Mar 2025 14:00:35 -0700 Subject: [PATCH 10/34] Improve AAO tool UI (#11613) --- apps/anti-abuse-oracle/src/identity.ts | 1 - apps/anti-abuse-oracle/src/server.tsx | 192 +++++++++++++------------ 2 files changed, 102 insertions(+), 91 deletions(-) diff --git a/apps/anti-abuse-oracle/src/identity.ts b/apps/anti-abuse-oracle/src/identity.ts index 0d5ab75..22cd480 100644 --- a/apps/anti-abuse-oracle/src/identity.ts +++ b/apps/anti-abuse-oracle/src/identity.ts @@ -45,6 +45,5 @@ export async function useFingerprintDeviceCount(userId: number) { GROUP BY "visitorId" ) t; ` - console.log('asdf rows: ', rows) return rows[0].maxUserCount ?? 0 } diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 96b3fd4..29042bc 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -102,7 +102,20 @@ app.use( ) app.get('/attestation/ui', async (c) => { - const tips = await recentTips() + const page = parseInt(c.req.query('page') || '1') + const recentUsers = await getRecentUsers(page) + const userScores = recentUsers + ? await Promise.all( + recentUsers.map(async (user) => { + const [userScore] = await Promise.all([ + getUserNormalizedScore(user.id) + ]) + return { + ...userScore + } + }) + ) + : [] let lastDate = '' function dateHeader(timestamp: Date) { @@ -118,9 +131,13 @@ app.get('/attestation/ui', async (c) => { Timestamp - Sender - Receiver - Amount + Handle + Listen Activity + Follower Count + Following Count + Fast Challenges + Overall Score + Normalized Score ) @@ -130,34 +147,48 @@ app.get('/attestation/ui', async (c) => { return c.html( -

Recent Tips

+

Recent Users

- {tips.map((tip) => ( + {userScores.map((userScore) => ( <> - {dateHeader(tip.timestamp)} - - - + {dateHeader(userScore.timestamp)} + + - + + + + + + ))}
{tip.timestamp.toLocaleTimeString()} - - {tip.sender.handle} - -
{userScore.timestamp.toLocaleTimeString()} - {tip.receiver.handle} + {userScore.handleLowerCase} {tip.amount / 100_000_000}{userScore.playCount}{userScore.followerCount}{userScore.followingCount}{userScore.challengeCount}{userScore.overallScore}{userScore.normalizedScore}
+ +
) }) @@ -240,12 +271,36 @@ app.get('/attestation/ui/user', async (c) => { - {userScore.playCount} - {userScore.followerCount} - {userScore.challengeCount} - {userScore.followingCount} - {userScore.fingerprintCount} - {userScore.overallScore} + 0 ? 'text-green-500' : ''}> + {userScore.playCount} + + 0 ? 'text-green-500' : ''}> + {userScore.followerCount} + + 0 ? 'text-red-500' : ''}> + {userScore.challengeCount} + + + {userScore.followingCount} + + 0 ? 'text-red-500' : ''}> + {userScore.fingerprintCount} + + = 0 + ? 'text-green-500' + : 'text-red-500' + } + > + {userScore.overallScore} + @@ -304,23 +359,8 @@ app.get('/attestation/ui/user', async (c) => { ) }) -app.get('/attestation/ui/recent-users', async (c) => { - const page = parseInt(c.req.query('page') || '1') - const recentUsers = await getRecentUsers(page) - const userScores = recentUsers - ? await Promise.all( - recentUsers.map(async (user) => { - const [userScore, flagged] = await Promise.all([ - getUserNormalizedScore(user.id), - getAAOAttestation(user.handle) - ]) - return { - ...userScore, - flagged - } - }) - ) - : [] +app.get('/attestation/ui/recent-tips', async (c) => { + const tips = await recentTips() let lastDate = '' function dateHeader(timestamp: Date) { @@ -336,13 +376,9 @@ app.get('/attestation/ui/recent-users', async (c) => { Timestamp - Handle - Listen Activity - Follower Count - Following Count - Fast Challenges - Overall Score - Normalized Score + Sender + Receiver + Amount ) @@ -352,58 +388,34 @@ app.get('/attestation/ui/recent-users', async (c) => { return c.html( -

Recent Users

+

Recent Tips

- {userScores.map((userScore) => ( + {tips.map((tip) => ( <> - {dateHeader(userScore.timestamp)} - - + {dateHeader(tip.timestamp)} + + - - - - - - + + ))}
{userScore.timestamp.toLocaleTimeString()}
{tip.timestamp.toLocaleTimeString()} - {userScore.handleLowerCase} + {tip.sender.handle} {userScore.playCount}{userScore.followerCount}{userScore.followingCount}{userScore.challengeCount}{userScore.overallScore}{userScore.normalizedScore} + + {tip.receiver.handle} + + {tip.amount / 100_000_000}
- -
) }) @@ -496,8 +508,8 @@ function Layout(props: LayoutProps) { placeholder='Search ID or Handle' /> - - Recent Users + + Recent Tips
From 01b93f94757260cb8a6524bd40e0dcd6377ff3bf Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Wed, 26 Mar 2025 15:00:17 -0700 Subject: [PATCH 11/34] Manual block/allow from AAO (#11654) --- apps/anti-abuse-oracle/src/actionLog.ts | 26 ++- apps/anti-abuse-oracle/src/server.tsx | 206 +++++++++++++++++++++--- 2 files changed, 208 insertions(+), 24 deletions(-) diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index 4b0a3c3..95bd89f 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -143,11 +143,14 @@ aggregate_scores AS ( COALESCE(play_activity.play_count, 0) AS play_count, COALESCE(fast_challenge_completion.challenge_count, 0) AS challenge_count, COALESCE(aggregate_user.following_count, 0) AS following_count, - COALESCE(aggregate_user.follower_count, 0) AS follower_count + COALESCE(aggregate_user.follower_count, 0) AS follower_count, + COALESCE(aggregate_user.score, 0) AS shadowban_score, + anti_abuse_blocked_users.is_blocked FROM users LEFT JOIN play_activity ON users.user_id = play_activity.user_id LEFT JOIN fast_challenge_completion ON users.user_id = fast_challenge_completion.user_id LEFT JOIN aggregate_user ON aggregate_user.user_id = users.user_id + LEFT JOIN anti_abuse_blocked_users ON anti_abuse_blocked_users.handle_lc = users.handle_lc WHERE users.handle_lc IS NOT NULL AND users.user_id in (select user_id from scoped_users) ORDER BY users.created_at DESC @@ -158,7 +161,9 @@ SELECT a.play_count, a.follower_count, a.challenge_count, - a.following_count + a.following_count, + a.shadowban_score, + a.is_blocked FROM aggregate_scores a ` const { @@ -167,7 +172,9 @@ FROM aggregate_scores a play_count, follower_count, challenge_count, - following_count + following_count, + shadowban_score, + is_blocked } = rows[0] // Convert values to numbers @@ -175,15 +182,24 @@ FROM aggregate_scores a const followerCount = Number(follower_count) const challengeCount = Number(challenge_count) const followingCount = Number(following_count) + const shadowbanScore = Number(shadowban_score) const numberOfUserWithFingerprint = (await useFingerprintDeviceCount(userId))! - const overallScore = + let overallScore = playCount + followerCount - challengeCount + (followingCount < 5 ? -1 : 0) - numberOfUserWithFingerprint + + // override score + if (is_blocked === true) { + overallScore = -1 + } else if (is_blocked === false) { + overallScore = 1 + } + const normalizedScore = Math.min( (overallScore - MIN_SCORE) / (MAX_SCORE - MIN_SCORE), 1 @@ -196,6 +212,8 @@ FROM aggregate_scores a challengeCount: challenge_count, followingCount: following_count, fingerprintCount: numberOfUserWithFingerprint, + isBlocked: is_blocked, + shadowbanScore, overallScore, normalizedScore } diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 29042bc..43c446f 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -12,7 +12,6 @@ import { type ActionRow, type TrackDetails, type UserDetails, - getAAOAttestation, queryUsers } from './actionLog' import { logger } from 'hono/logger' @@ -43,11 +42,111 @@ if (!AAO_AUTH_PASSWORD) { ) } +async function ensureTableExists() { + try { + await sql` + CREATE TABLE IF NOT EXISTS anti_abuse_blocked_users ( + handle_lc VARCHAR(255) PRIMARY KEY, + is_blocked BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ` + } catch (error) { + console.error( + 'Error ensuring anti_abuse_blocked_users table exists:', + error + ) + process.exit(1) // Exit the process if table creation fails + } +} + +ensureTableExists() + const app = new Hono() app.use(logger()) app.use('/attestation/*', cors()) +app.post( + '/attestation/block-user', + basicAuth({ + username: AAO_AUTH_USER, + password: AAO_AUTH_PASSWORD + }), + async (c) => { + const formData = await c.req.parseBody() + const handle = formData.handle as string + if (!handle) { + return c.text('Handle is required', 400) + } + + try { + await sql` + INSERT INTO anti_abuse_blocked_users ( + handle_lc, + is_blocked, + created_at, + updated_at + ) VALUES ( + ${handle.toLowerCase()}, + TRUE, -- is_blocked + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) + ON CONFLICT (handle_lc) + DO UPDATE SET + is_blocked = EXCLUDED.is_blocked, + updated_at = CURRENT_TIMESTAMP; + ` + return c.redirect(`/attestation/ui/user?q=${encodeURIComponent(handle)}`) + } catch (error) { + console.error('Error blocking user:', error) + return c.text('Failed to block user', 500) + } + } +) + +app.post( + '/attestation/unblock-user', + basicAuth({ + username: AAO_AUTH_USER, + password: AAO_AUTH_PASSWORD + }), + async (c) => { + const formData = await c.req.parseBody() + + const handle = formData.handle as string + if (!handle) { + return c.text('Handle is required', 400) + } + + try { + await sql` + INSERT INTO anti_abuse_blocked_users ( + handle_lc, + is_blocked, + created_at, + updated_at + ) VALUES ( + ${handle.toLocaleLowerCase()}, + FALSE, -- is_blocked + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) + ON CONFLICT (handle_lc) + DO UPDATE SET + is_blocked = EXCLUDED.is_blocked, + updated_at = CURRENT_TIMESTAMP; + ` + return c.redirect(`/attestation/ui/user?q=${encodeURIComponent(handle)}`) + } catch (error) { + console.error('Error unblocking user:', error) + return c.text('Failed to block user', 500) + } + } +) + app.post('/attestation/:handle', async (c) => { const handle = c.req.param('handle').toLowerCase() const { challengeId, challengeSpecifier, amount } = await c.req.json() @@ -102,7 +201,7 @@ app.use( ) app.get('/attestation/ui', async (c) => { - const page = parseInt(c.req.query('page') || '1') + const page = parseInt(c.req.query('page') || '0') const recentUsers = await getRecentUsers(page) const userScores = recentUsers ? await Promise.all( @@ -136,8 +235,9 @@ app.get('/attestation/ui', async (c) => { Follower Count Following Count Fast Challenges + Fingerprint + Override Overall Score - Normalized Score ) @@ -166,8 +266,15 @@ app.get('/attestation/ui', async (c) => { {userScore.followerCount} {userScore.followingCount} {userScore.challengeCount} + {userScore.fingerprintCount} + + {userScore.isBlocked + ? 'Blocked' + : userScore.isBlocked === false + ? 'Allowed' + : ''} + {userScore.overallScore} - {userScore.normalizedScore} ))} @@ -243,7 +350,7 @@ app.get('/attestation/ui/user', async (c) => {
{(userScore.normalizedScore * 100).toFixed(0)}%
{' '} @@ -265,8 +372,6 @@ app.get('/attestation/ui/user', async (c) => { Follower Count Fast Challenge Count Following Count - Fingerprint Count - Overall Score @@ -289,21 +394,85 @@ app.get('/attestation/ui/user', async (c) => { > {userScore.followingCount} - 0 ? 'text-red-500' : ''}> - {userScore.fingerprintCount} - = 0 - ? 'text-green-500' - : 'text-red-500' + userScore.shadowbanScore < 0 + ? 'text-red-500' + : 'text-green-500' } - > - {userScore.overallScore} - + >{`${userScore.shadowbanScore < 0 ? 'Shadowbanned' : 'Not shadowbanned'} from notifications.`} + + + + + + + + + + + + + + +
Fingerprint CountOverrideOverall Score
0 ? 'text-red-500' : ''}> + {userScore.fingerprintCount} + + {userScore.isBlocked + ? 'Blocked' + : userScore.isBlocked === false + ? 'Allowed' + : 'N/A'} + = 0 ? 'text-green-500' : 'text-red-500' + } + > + {userScore.overallScore} + + {`${userScore.overallScore < 0 ? 'Blocked' : 'Not blocked'} from claiming and DMs.`} + + {userScore.overallScore < 0 ? ( +
+ + +
+ ) : ( +
+ + +
+ )} +

Fingerprints

@@ -505,12 +674,9 @@ function Layout(props: LayoutProps) { type='search' class='input' autocomplete={'off'} - placeholder='Search ID or Handle' + placeholder='Search user handle' /> - - Recent Tips -
{props.children} From b666f997b8083f5313b964cfd36e17520df51c85 Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Fri, 4 Apr 2025 14:21:59 -0700 Subject: [PATCH 12/34] Add chat block to AAO (#11728) --- apps/anti-abuse-oracle/src/actionLog.ts | 80 ++++--------------------- apps/anti-abuse-oracle/src/server.tsx | 10 ++++ 2 files changed, 22 insertions(+), 68 deletions(-) diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index 95bd89f..53747a3 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -107,64 +107,15 @@ export async function getUserScore(userId: number) { export async function getUserNormalizedScore(userId: number) { const rows = await sql` -WITH scoped_users as ( -select * from users -where user_id = ${userId} -order by created_at desc -limit 100 -), -play_activity AS ( - SELECT user_id, - COUNT(DISTINCT date_trunc('minute', plays.created_at)) AS play_count - FROM plays - WHERE user_id IS NOT NULL - AND user_id in (select user_id from scoped_users) - GROUP BY user_id -), -fast_challenge_completion AS ( - SELECT users.user_id, - handle_lc, - users.created_at, - COUNT(*) AS challenge_count, - ARRAY_AGG(user_challenges.challenge_id) AS challenge_ids - FROM users - LEFT JOIN user_challenges ON users.user_id = user_challenges.user_id - WHERE user_challenges.is_complete - AND user_challenges.completed_at - users.created_at <= INTERVAL '3 minutes' - AND user_challenges.challenge_id not in ('m', 'b') - AND users.user_id in (select user_id from scoped_users) - GROUP BY users.user_id, users.handle_lc, users.created_at - ORDER BY users.created_at DESC -), -aggregate_scores AS ( - SELECT - users.handle_lc, - users.created_at, - COALESCE(play_activity.play_count, 0) AS play_count, - COALESCE(fast_challenge_completion.challenge_count, 0) AS challenge_count, - COALESCE(aggregate_user.following_count, 0) AS following_count, - COALESCE(aggregate_user.follower_count, 0) AS follower_count, - COALESCE(aggregate_user.score, 0) AS shadowban_score, - anti_abuse_blocked_users.is_blocked - FROM users - LEFT JOIN play_activity ON users.user_id = play_activity.user_id - LEFT JOIN fast_challenge_completion ON users.user_id = fast_challenge_completion.user_id - LEFT JOIN aggregate_user ON aggregate_user.user_id = users.user_id - LEFT JOIN anti_abuse_blocked_users ON anti_abuse_blocked_users.handle_lc = users.handle_lc - WHERE users.handle_lc IS NOT NULL - AND users.user_id in (select user_id from scoped_users) - ORDER BY users.created_at DESC -) -SELECT - a.handle_lc, - a.created_at as "timestamp", - a.play_count, - a.follower_count, - a.challenge_count, - a.following_count, - a.shadowban_score, - a.is_blocked -FROM aggregate_scores a + SELECT + user_scores.handle_lc, + users.created_at as timestamp, + user_scores.*, + anti_abuse_blocked_users.is_blocked + FROM get_user_scores(${[userId]}) as user_scores + LEFT JOIN users on users.user_id = user_scores.user_id + LEFT JOIN anti_abuse_blocked_users ON anti_abuse_blocked_users.handle_lc = user_scores.handle_lc + ` const { handle_lc, @@ -173,25 +124,17 @@ FROM aggregate_scores a follower_count, challenge_count, following_count, + chat_block_count, shadowban_score, is_blocked } = rows[0] // Convert values to numbers - const playCount = Number(play_count) - const followerCount = Number(follower_count) - const challengeCount = Number(challenge_count) - const followingCount = Number(following_count) const shadowbanScore = Number(shadowban_score) const numberOfUserWithFingerprint = (await useFingerprintDeviceCount(userId))! - let overallScore = - playCount + - followerCount - - challengeCount + - (followingCount < 5 ? -1 : 0) - - numberOfUserWithFingerprint + let overallScore = shadowbanScore - numberOfUserWithFingerprint // override score if (is_blocked === true) { @@ -211,6 +154,7 @@ FROM aggregate_scores a followerCount: follower_count, challengeCount: challenge_count, followingCount: following_count, + chatBlockCount: chat_block_count, fingerprintCount: numberOfUserWithFingerprint, isBlocked: is_blocked, shadowbanScore, diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 43c446f..dfcc4e7 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -235,6 +235,7 @@ app.get('/attestation/ui', async (c) => {
+ @@ -266,6 +267,7 @@ app.get('/attestation/ui', async (c) => { + + @@ -394,6 +397,13 @@ app.get('/attestation/ui/user', async (c) => { > {userScore.followingCount} + - - - - - - - @@ -263,19 +256,6 @@ app.get('/attestation/ui', async (c) => { {userScore.handleLowerCase} - - - - - - - @@ -354,7 +334,7 @@ app.get('/attestation/ui/user', async (c) => {
- {(userScore.normalizedScore * 100).toFixed(0)}% + {userScore.overallScore}
{' '} {Object.entries(signals).map(([name, ok]) => (
{
+ @@ -404,6 +385,16 @@ app.get('/attestation/ui/user', async (c) => { > {userScore.chatBlockCount} + + - + From 810abfaded4fad35aec8a31bf5438ace0a58e028 Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Fri, 25 Apr 2025 15:50:48 -0700 Subject: [PATCH 15/34] Add deliverable email to AAO and optimize perf (#11943) --- apps/anti-abuse-oracle/src/actionLog.ts | 31 +++++++++- apps/anti-abuse-oracle/src/identity.ts | 7 +++ apps/anti-abuse-oracle/src/server.tsx | 76 ++++++++++++++++--------- 3 files changed, 84 insertions(+), 30 deletions(-) diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index 1c11d65..de2ee66 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -2,7 +2,7 @@ import 'dotenv/config' import postgres from 'postgres' import fetch from 'cross-fetch' -import { useFingerprintDeviceCount } from './identity' +import { useEmailDeliverable, useFingerprintDeviceCount } from './identity' export const sql = postgres(process.env.audius_db_url || '') @@ -89,6 +89,26 @@ export async function getRecentUsers(page: number) { return rows.map((row) => row.user as UserDetails) } +export type ClaimDetails = { + disbursement_date: string + user_id: number + handle: string + sign_up_date: Date + challenge_id: string + amount: number +} +export async function getRecentClaims(page: number) { + const rows = await sql` + select challenge_disbursements.created_at as disbursement_date, handle_lc as handle, users.user_id, users.created_at as sign_up_date, challenge_disbursements.challenge_id, ROUND(CAST(challenge_disbursements.amount AS numeric) / 100000000, 0) as amount + from challenge_disbursements + join users on users.user_id = challenge_disbursements.user_id + order by challenge_disbursements.created_at desc + limit 10 offset ${page * 10} + ` + if (!rows.length) return + return rows.map((row) => row as ClaimDetails) +} + export async function getUserScore(userId: number) { const rows = await sql` select @@ -112,7 +132,7 @@ export async function getUserNormalizedScore(userId: number) { users.created_at as timestamp, user_scores.*, anti_abuse_blocked_users.is_blocked - FROM get_user_scores(${[userId]}) as user_scores + FROM get_user_score(${userId}) as user_scores LEFT JOIN users on users.user_id = user_scores.user_id LEFT JOIN anti_abuse_blocked_users ON anti_abuse_blocked_users.handle_lc = user_scores.handle_lc @@ -134,9 +154,13 @@ export async function getUserNormalizedScore(userId: number) { const shadowbanScore = Number(shadowban_score) const numberOfUserWithFingerprint = (await useFingerprintDeviceCount(userId))! - let overallScore = shadowbanScore - numberOfUserWithFingerprint + const isEmailDeliverable = await useEmailDeliverable(userId) + if (!isEmailDeliverable) { + overallScore -= 1000 + } + // override score if (is_blocked === true) { overallScore = -1000 @@ -158,6 +182,7 @@ export async function getUserNormalizedScore(userId: number) { chatBlockCount: chat_block_count, fingerprintCount: numberOfUserWithFingerprint, isAudiusImpersonator: is_audius_impersonator, + isEmailDeliverable, isBlocked: is_blocked, shadowbanScore, overallScore, diff --git a/apps/anti-abuse-oracle/src/identity.ts b/apps/anti-abuse-oracle/src/identity.ts index 22cd480..b320625 100644 --- a/apps/anti-abuse-oracle/src/identity.ts +++ b/apps/anti-abuse-oracle/src/identity.ts @@ -47,3 +47,10 @@ export async function useFingerprintDeviceCount(userId: number) { ` return rows[0].maxUserCount ?? 0 } + +export async function useEmailDeliverable(userId: number) { + const rows = await sql` + select "isEmailDeliverable" from "Users" where "blockchainUserId" = ${userId} + ` + return rows[0].isEmailDeliverable +} diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index d3123a2..75e05f9 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -4,7 +4,7 @@ import { basicAuth } from 'hono/basic-auth' import { actionLogForUser, getUser, - getRecentUsers, + getRecentClaims, getUserNormalizedScore, getUserScore, recentTips, @@ -202,20 +202,15 @@ app.use( app.get('/attestation/ui', async (c) => { const page = parseInt(c.req.query('page') || '0') - const recentUsers = await getRecentUsers(page) - const userScores = recentUsers - ? await Promise.all( - recentUsers.map(async (user) => { - const [userScore] = await Promise.all([ - getUserNormalizedScore(user.id) - ]) - return { - ...userScore - } - }) - ) - : [] - + const recentClaims = await getRecentClaims(page) + const userScores = Object.fromEntries( + await Promise.all( + (recentClaims || []).map(async (claim) => [ + claim.handle, + await getUserNormalizedScore(claim.user_id) + ]) + ) + ) let lastDate = '' function dateHeader(timestamp: Date) { const d = timestamp?.toDateString() @@ -229,9 +224,12 @@ app.get('/attestation/ui', async (c) => { - + - + + + + ) @@ -241,22 +239,40 @@ app.get('/attestation/ui', async (c) => { return c.html( -

Recent Users

+

Recent Claims

+

+ + Red rows indicate who would be blocked under current scoring. + +

Follower Count Following Count Fast ChallengesChat Blocks Fingerprint Override Overall Score{userScore.followerCount} {userScore.followingCount} {userScore.challengeCount}{userScore.chatBlockCount} {userScore.fingerprintCount} {userScore.isBlocked @@ -372,6 +374,7 @@ app.get('/attestation/ui/user', async (c) => { Follower Count Fast Challenge Count Following CountChat Block Count
+ {userScore.chatBlockCount} + Date: Mon, 7 Apr 2025 13:34:57 -0700 Subject: [PATCH 13/34] Update anti abuse UI to include impersonator (#11770) --- apps/anti-abuse-oracle/src/actionLog.ts | 8 +++--- apps/anti-abuse-oracle/src/server.tsx | 35 +++++++++---------------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index 53747a3..1c11d65 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -125,7 +125,8 @@ export async function getUserNormalizedScore(userId: number) { challenge_count, following_count, chat_block_count, - shadowban_score, + is_audius_impersonator, + score: shadowban_score, is_blocked } = rows[0] @@ -138,9 +139,9 @@ export async function getUserNormalizedScore(userId: number) { // override score if (is_blocked === true) { - overallScore = -1 + overallScore = -1000 } else if (is_blocked === false) { - overallScore = 1 + overallScore = 1000 } const normalizedScore = Math.min( @@ -156,6 +157,7 @@ export async function getUserNormalizedScore(userId: number) { followingCount: following_count, chatBlockCount: chat_block_count, fingerprintCount: numberOfUserWithFingerprint, + isAudiusImpersonator: is_audius_impersonator, isBlocked: is_blocked, shadowbanScore, overallScore, diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index dfcc4e7..f6e6b33 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -231,13 +231,6 @@ app.get('/attestation/ui', async (c) => {
Timestamp HandleListen ActivityFollower CountFollowing CountFast ChallengesChat BlocksFingerprintOverride Overall Score
{userScore.playCount}{userScore.followerCount}{userScore.followingCount}{userScore.challengeCount}{userScore.chatBlockCount}{userScore.fingerprintCount} - {userScore.isBlocked - ? 'Blocked' - : userScore.isBlocked === false - ? 'Allowed' - : ''} - {userScore.overallScore}
Fast Challenge Count Following Count Chat Block CountAudius Impersonator
+ {userScore.isAudiusImpersonator.toString()} + { let lastDate = '' function dateHeader(timestamp: Date) { - const d = timestamp.toDateString() + const d = timestamp?.toDateString() if (d != lastDate) { lastDate = d return ( From 1b0d8587f5e7a43bba8041584f11361c8f8bcf78 Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Tue, 15 Apr 2025 13:52:59 -0700 Subject: [PATCH 14/34] Fix AAO error on missing timestamp (#11809) --- apps/anti-abuse-oracle/src/server.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index f6e6b33..d3123a2 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -218,7 +218,7 @@ app.get('/attestation/ui', async (c) => { let lastDate = '' function dateHeader(timestamp: Date) { - const d = timestamp.toDateString() + const d = timestamp?.toDateString() if (d != lastDate) { lastDate = d return ( @@ -298,7 +298,7 @@ app.get('/attestation/ui/user', async (c) => { let lastDate = '' function dateHeader(timestamp: Date) { - const d = timestamp.toDateString() + const d = timestamp?.toDateString() if (d != lastDate) { lastDate = d return ( @@ -511,7 +511,7 @@ app.get('/attestation/ui/user', async (c) => { <> {dateHeader(r.timestamp)}
{r.timestamp.toLocaleTimeString()}{r.timestamp?.toLocaleTimeString()} {r.verb} {r.target} {renderDetails(r)}
TimestampClaim Timestamp HandleOverall ScoreSign Up TimestampScoreChallenge IDAmount
- {userScores.map((userScore) => ( + {recentClaims?.map((recentClaim) => ( <> - {dateHeader(userScore.timestamp)} - - + {dateHeader(new Date(recentClaim.disbursement_date))} + + - + + + + ))} @@ -380,7 +396,9 @@ app.get('/attestation/ui/user', async (c) => {
{userScore.timestamp.toLocaleTimeString()}
+ {new Date(recentClaim.disbursement_date).toLocaleTimeString()} + + {' '} - {userScore.handleLowerCase} + {recentClaim.handle} {userScore.overallScore}{recentClaim.sign_up_date.toLocaleString()}{userScores[recentClaim.handle].overallScore}{recentClaim.challenge_id}{recentClaim.amount}
0 + ? 'text-red-500' + : 'text-green-500' } > {userScore.chatBlockCount} @@ -408,7 +426,8 @@ app.get('/attestation/ui/user', async (c) => { - + + @@ -417,6 +436,9 @@ app.get('/attestation/ui/user', async (c) => { + Date: Mon, 23 Jun 2025 16:33:45 -0600 Subject: [PATCH 18/34] Increase aao override score (#12383) --- apps/anti-abuse-oracle/src/actionLog.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index d5b5249..8fed690 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -166,9 +166,9 @@ export async function getUserNormalizedScore(userId: number, wallet: string) { // override score if (is_blocked === true) { - overallScore = -1000 + overallScore = -100000 } else if (is_blocked === false) { - overallScore = 1000 + overallScore = 100000 } const normalizedScore = Math.min( From a9aa34ed857c73d169dbfae92dd2336cd51ca7b7 Mon Sep 17 00:00:00 2001 From: Ray Jacobson Date: Tue, 5 Aug 2025 09:39:24 -0700 Subject: [PATCH 19/34] [API-299] Add AAO check in solana-relay (#12660) ### Description Makes a request to AAO service's new /attestation/check route. I wish it could do db stuff, but it requires a lot of custom code + linking identity service db. ### How Has This Been Tested? _Please describe the tests that you ran to verify your changes. Provide repro instructions & any configuration._ after some ugly env wrangling ``` # aao npm run dev curl localhost:6002/attestation/check?wallet=0xf6b983a131755072c7ec1505910c8bb5c25e2215 # solana relay # i added a test endpoint that just called my helper and validated the response worked as well ``` --- apps/anti-abuse-oracle/src/server.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 3572627..169227f 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -68,6 +68,22 @@ const app = new Hono() app.use(logger()) app.use('/attestation/*', cors()) +app.get('/attestation/check', async (c) => { + const wallet = c.req.query('wallet') + if (!wallet) return c.json({ error: 'wallet is required' }, 400) + + const users = + await sql`select user_id, wallet from users where wallet = ${wallet.toLowerCase()}` + const user = users[0] + if (!user) return c.json({ error: `wallet not found: ${wallet}` }, 404) + + const userScore = await getUserNormalizedScore(user.user_id, user.wallet) + if (userScore.overallScore < 0) { + return c.json({ data: 'blocked' }, 400) + } + return c.json({ data: 'allowed' }, 200) +}) + app.post( '/attestation/block-user', basicAuth({ From 3f59e1539f665a33f705d46bde42e5bc1d730dbf Mon Sep 17 00:00:00 2001 From: Ray Jacobson Date: Wed, 1 Oct 2025 18:13:10 -0700 Subject: [PATCH 20/34] Move audius-protocol to apps (#13078) ### Description Prep for renaming github repo to `apps` ### How Has This Been Tested? _Please describe the tests that you ran to verify your changes. Provide repro instructions & any configuration._ ``` mv audius-protocol apps cd apps npm i audius-compose connect audius-compose up npm run web:dev ``` 1. Created account 2. Uploaded track --- apps/anti-abuse-oracle/src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/anti-abuse-oracle/src/config.ts b/apps/anti-abuse-oracle/src/config.ts index 5afc7c0..2e6060e 100644 --- a/apps/anti-abuse-oracle/src/config.ts +++ b/apps/anti-abuse-oracle/src/config.ts @@ -44,14 +44,14 @@ const readConfig = (): Config => { default: 'dev' }), audius_discprov_url: str({ - default: 'http://audius-protocol-discovery-provider-1' + default: 'http://audius-discovery-provider-1' }), audius_db_url: str({ default: 'postgresql+psycopg2://postgres:postgres@db:5432/discovery_provider_1' }), audius_redis_url: str({ - default: 'redis://audius-protocol-discovery-provider-redis-1:6379/00' + default: 'redis://audius-discovery-redis-1:6379/00' }), anti_abuse_oracle_server_host: str({ default: '0.0.0.0' }), anti_abuse_oracle_server_port: num({ default: 6003 }), From f298f4833d11b9d17964ef2a7beafde44872baa0 Mon Sep 17 00:00:00 2001 From: Ray Jacobson Date: Wed, 22 Oct 2025 15:19:12 -0400 Subject: [PATCH 21/34] Update aao listen streak (#13286) ### Description ### How Has This Been Tested? _Please describe the tests that you ran to verify your changes. Provide repro instructions & any configuration._ --- apps/anti-abuse-oracle/package.json | 12 ++++---- apps/anti-abuse-oracle/src/config.ts | 8 +++-- apps/anti-abuse-oracle/src/identity.ts | 7 +++++ apps/anti-abuse-oracle/src/sdk.ts | 24 +++++++++++++++ apps/anti-abuse-oracle/src/server.tsx | 41 ++++++++++++++++++++------ 5 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 apps/anti-abuse-oracle/src/sdk.ts diff --git a/apps/anti-abuse-oracle/package.json b/apps/anti-abuse-oracle/package.json index c56866a..a2d9e35 100644 --- a/apps/anti-abuse-oracle/package.json +++ b/apps/anti-abuse-oracle/package.json @@ -15,7 +15,8 @@ "preset": "jest-presets/jest/node" }, "dependencies": { - "@audius/sdk": "5.0.0", + "@audius/sdk": "*", + "@audius/sdk-legacy": "npm:@audius/sdk@5.0.0", "@hono/node-server": "^1.13.7", "@pedalboard/basekit": "*", "@pedalboard/logger": "*", @@ -31,8 +32,8 @@ }, "devDependencies": { "@types/body-parser": "^1.19.0", - "@types/cors": "^2.8.10", - "@types/express": "^4.17.12", + "@types/cors": "2.8.10", + "@types/express": "4.17.12", "@types/jest": "^26.0.22", "@types/morgan": "^1.9.2", "@types/node": "^15.12.2", @@ -41,12 +42,13 @@ "esbuild-register": "^3.3.2", "eslint": "8.56.0", "eslint-config-custom-server": "*", - "jest": "^26.6.3", + "ts-node": "10.9.2", + "jest": "26.6.3", "jest-presets": "*", "nodemon": "^2.0.15", "supertest": "^6.1.3", "tsconfig": "*", - "typescript": "^4.5.3", + "typescript": "5.4.5", "envalid": "8.0.0" } } diff --git a/apps/anti-abuse-oracle/src/config.ts b/apps/anti-abuse-oracle/src/config.ts index 2e6060e..94572c7 100644 --- a/apps/anti-abuse-oracle/src/config.ts +++ b/apps/anti-abuse-oracle/src/config.ts @@ -14,6 +14,8 @@ export const InstructionsProgram = new PublicKey( 'Sysvar1nstructions1111111111111111111111111' ) +export type Environment = 'dev' | 'stage' | 'prod' + // reads .env file based on environment const readDotEnv = () => { const environment = process.env.audius_discprov_env || 'dev' @@ -24,7 +26,7 @@ const readDotEnv = () => { } type Config = { - environment: string + environment: Environment discoveryDbConnectionString: string redisUrl: string serverHost: string @@ -34,7 +36,7 @@ type Config = { let cachedConfig: Config | null = null -const readConfig = (): Config => { +export const readConfig = (): Config => { if (cachedConfig !== null) return cachedConfig readDotEnv() @@ -60,7 +62,7 @@ const readConfig = (): Config => { }) cachedConfig = { - environment: env.audius_discprov_env, + environment: env.audius_discprov_env as Environment, discoveryDbConnectionString: env.audius_db_url, redisUrl: env.audius_redis_url, serverHost: env.anti_abuse_oracle_server_host, diff --git a/apps/anti-abuse-oracle/src/identity.ts b/apps/anti-abuse-oracle/src/identity.ts index cd20c6f..174108e 100644 --- a/apps/anti-abuse-oracle/src/identity.ts +++ b/apps/anti-abuse-oracle/src/identity.ts @@ -54,3 +54,10 @@ export async function useEmailDeliverable(wallet: string) { ` return rows[0].isEmailDeliverable } + +export async function useEmail(userId: number) { + const rows = await sql` + select "email" from "Users" where "blockchainUserId" = ${userId} + ` + return rows[0].email +} diff --git a/apps/anti-abuse-oracle/src/sdk.ts b/apps/anti-abuse-oracle/src/sdk.ts new file mode 100644 index 0000000..7df9042 --- /dev/null +++ b/apps/anti-abuse-oracle/src/sdk.ts @@ -0,0 +1,24 @@ +import { AudiusSdk, sdk } from '@audius/sdk' +import { readConfig, Environment } from './config' + +const environmentToSdkEnvironment: Record< + Environment, + 'development' | 'staging' | 'production' +> = { + dev: 'development', + stage: 'staging', + prod: 'production' +} + +let audiusSdk: AudiusSdk | undefined = undefined + +export const getAudiusSdk = () => { + if (audiusSdk === undefined) { + const config = readConfig() + audiusSdk = sdk({ + appName: 'anti-abuse-oracle', + environment: environmentToSdkEnvironment[config.environment] + }) + } + return audiusSdk +} diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 169227f..7112598 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -16,10 +16,12 @@ import { } from './actionLog' import { logger } from 'hono/logger' import { config } from './config' -import { SolanaUtils, Utils } from '@audius/sdk' +import { HashId } from '@audius/sdk' +import { SolanaUtils, Utils } from '@audius/sdk-legacy' import bn from 'bn.js' -import { userFingerprints } from './identity' +import { useEmail, userFingerprints } from './identity' import { cors } from 'hono/cors' +import { getAudiusSdk } from './sdk' let CONTENT_NODE = 'https://creatornode2.audius.co' let FRONTEND = 'https://audius.co' @@ -42,6 +44,10 @@ if (!AAO_AUTH_PASSWORD) { ) } +const rewardAmountRatio = 10 + +const sdk = getAudiusSdk() + async function ensureTableExists() { try { await sql` @@ -167,19 +173,31 @@ app.post('/attestation/:handle', async (c) => { const handle = c.req.param('handle').toLowerCase() const { challengeId, challengeSpecifier, amount } = await c.req.json() - const users = - await sql`select user_id, wallet from users where handle_lc = ${handle}` - const user = users[0] - if (!user) return c.json({ error: `handle not found: ${handle}` }, 404) + const { data: users } = await sdk.full.users.getUserByHandle({ handle }) + if (!users || !users[0]) { + return c.json({ error: `handle not found: ${handle}` }, 404) + } + const user = users[0]! // pass / fail - const userScore = await getUserNormalizedScore(user.user_id, user.wallet) + const userScore = await getUserNormalizedScore( + HashId.parse(user.id), + user.wallet + ) // Reward attestation proportional to user score confidence - if (userScore.overallScore < (amount as number) / 10) { + if (userScore.overallScore < (amount as number) / rewardAmountRatio) { return c.json({ error: 'denied' }, 400) } + // Custom rules for specific challenges + if (challengeId === 'e') { + if (user.totalAudioBalance < 50) { + return c.json({ error: 'denied' }, 400) + } + } + console.log('userScore', userScore, user) + try { const bnAmount = SolanaUtils.uiAudioToBNWaudio(amount) const identifier = SolanaUtils.constructTransferId( @@ -321,6 +339,7 @@ app.get('/attestation/ui/user', async (c) => { const idOrHandle = c.req.query('q') || '1' const user = await getUser(idOrHandle) if (!user) return c.text(`user id not found: ${idOrHandle}`, 404) + const email = await useEmail(user.id) const signals = await getUserScore(user.id) const userScore = (await getUserNormalizedScore(user.id, user.wallet))! @@ -358,7 +377,7 @@ app.get('/attestation/ui/user', async (c) => {
{user.id}
@@ -381,6 +400,10 @@ app.get('/attestation/ui/user', async (c) => {
+

+ Allowed to claim up to{' '} + {rewardAmountRatio * userScore.overallScore} $AUDIO per reward. +

Score Breakdown

Fingerprint CountFingerprint CountDeliverable Email Override Overall Score
0 ? 'text-red-500' : ''}> {userScore.fingerprintCount} + {userScore.isEmailDeliverable.toString()} + Date: Thu, 8 May 2025 22:28:03 -0700 Subject: [PATCH 16/34] Fix AAO on local dev by querying for wallet instead of user ID (#12089) --- apps/anti-abuse-oracle/src/actionLog.ts | 9 ++++++--- apps/anti-abuse-oracle/src/identity.ts | 4 ++-- apps/anti-abuse-oracle/src/server.tsx | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index de2ee66..d5b5249 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -40,6 +40,7 @@ export async function recentTips() { export type UserDetails = { id: number handle: string + wallet: string name: string img: string isVerified: boolean @@ -51,6 +52,7 @@ const buildUserDetails = ` select json_build_object( 'id', user_id, 'handle', handle, + 'wallet', wallet, 'name', name, 'isVerified', is_verified, 'img', profile_picture_sizes @@ -93,13 +95,14 @@ export type ClaimDetails = { disbursement_date: string user_id: number handle: string + wallet: string sign_up_date: Date challenge_id: string amount: number } export async function getRecentClaims(page: number) { const rows = await sql` - select challenge_disbursements.created_at as disbursement_date, handle_lc as handle, users.user_id, users.created_at as sign_up_date, challenge_disbursements.challenge_id, ROUND(CAST(challenge_disbursements.amount AS numeric) / 100000000, 0) as amount + select challenge_disbursements.created_at as disbursement_date, handle_lc as handle, users.wallet as wallet, users.user_id, users.created_at as sign_up_date, challenge_disbursements.challenge_id, ROUND(CAST(challenge_disbursements.amount AS numeric) / 100000000, 0) as amount from challenge_disbursements join users on users.user_id = challenge_disbursements.user_id order by challenge_disbursements.created_at desc @@ -125,7 +128,7 @@ export async function getUserScore(userId: number) { return rows[0] } -export async function getUserNormalizedScore(userId: number) { +export async function getUserNormalizedScore(userId: number, wallet: string) { const rows = await sql` SELECT user_scores.handle_lc, @@ -156,7 +159,7 @@ export async function getUserNormalizedScore(userId: number) { const numberOfUserWithFingerprint = (await useFingerprintDeviceCount(userId))! let overallScore = shadowbanScore - numberOfUserWithFingerprint - const isEmailDeliverable = await useEmailDeliverable(userId) + const isEmailDeliverable = await useEmailDeliverable(wallet) if (!isEmailDeliverable) { overallScore -= 1000 } diff --git a/apps/anti-abuse-oracle/src/identity.ts b/apps/anti-abuse-oracle/src/identity.ts index b320625..cd20c6f 100644 --- a/apps/anti-abuse-oracle/src/identity.ts +++ b/apps/anti-abuse-oracle/src/identity.ts @@ -48,9 +48,9 @@ export async function useFingerprintDeviceCount(userId: number) { return rows[0].maxUserCount ?? 0 } -export async function useEmailDeliverable(userId: number) { +export async function useEmailDeliverable(wallet: string) { const rows = await sql` - select "isEmailDeliverable" from "Users" where "blockchainUserId" = ${userId} + select "isEmailDeliverable" from "Users" where "walletAddress" = ${wallet} ` return rows[0].isEmailDeliverable } diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 75e05f9..91e8bad 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -157,7 +157,7 @@ app.post('/attestation/:handle', async (c) => { if (!user) return c.json({ error: `handle not found: ${handle}` }, 404) // pass / fail - const userScore = await getUserNormalizedScore(user.user_id) + const userScore = await getUserNormalizedScore(user.user_id, user.wallet) if (userScore.overallScore < 0) { return c.json({ error: 'denied' }, 400) } @@ -207,7 +207,7 @@ app.get('/attestation/ui', async (c) => { await Promise.all( (recentClaims || []).map(async (claim) => [ claim.handle, - await getUserNormalizedScore(claim.user_id) + await getUserNormalizedScore(claim.user_id, claim.wallet) ]) ) ) @@ -303,7 +303,7 @@ app.get('/attestation/ui/user', async (c) => { const user = await getUser(idOrHandle) if (!user) return c.text(`user id not found: ${idOrHandle}`, 404) const signals = await getUserScore(user.id) - const userScore = (await getUserNormalizedScore(user.id))! + const userScore = (await getUserNormalizedScore(user.id, user.wallet))! if (!signals) return c.text(`user id not found: ${idOrHandle}`, 404) From 7448a172e114a252ba6c2a97d1838e22dadf70dd Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Fri, 30 May 2025 13:53:55 -0700 Subject: [PATCH 17/34] AAO attestation threshold proportional to reward + karma (#12238) --- apps/anti-abuse-oracle/src/server.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 91e8bad..3572627 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -158,7 +158,9 @@ app.post('/attestation/:handle', async (c) => { // pass / fail const userScore = await getUserNormalizedScore(user.user_id, user.wallet) - if (userScore.overallScore < 0) { + + // Reward attestation proportional to user score confidence + if (userScore.overallScore < (amount as number) / 10) { return c.json({ error: 'denied' }, 400) } @@ -252,7 +254,8 @@ app.get('/attestation/ui', async (c) => { {dateHeader(new Date(recentClaim.disbursement_date))}
From 5849f64d6c1f6d84b43f08fa48c6d70b6ba19817 Mon Sep 17 00:00:00 2001 From: Randy Schott <1815175+schottra@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:35:47 -0400 Subject: [PATCH 22/34] [API-369] Skip ratio check for dvl challenge (#13349) --- apps/anti-abuse-oracle/src/server.tsx | 32 ++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 7112598..e7cfe8c 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -45,6 +45,7 @@ if (!AAO_AUTH_PASSWORD) { } const rewardAmountRatio = 10 +const skipValidationChallenges = ['dvl'] const sdk = getAudiusSdk() @@ -179,25 +180,26 @@ app.post('/attestation/:handle', async (c) => { } const user = users[0]! - // pass / fail - const userScore = await getUserNormalizedScore( - HashId.parse(user.id), - user.wallet - ) - - // Reward attestation proportional to user score confidence - if (userScore.overallScore < (amount as number) / rewardAmountRatio) { - return c.json({ error: 'denied' }, 400) - } + if (!skipValidationChallenges.includes(challengeId)) { + // pass / fail + const userScore = await getUserNormalizedScore( + HashId.parse(user.id), + user.wallet + ) - // Custom rules for specific challenges - if (challengeId === 'e') { - if (user.totalAudioBalance < 50) { + // Reward attestation proportional to user score confidence + if (userScore.overallScore < (amount as number) / rewardAmountRatio) { return c.json({ error: 'denied' }, 400) } - } - console.log('userScore', userScore, user) + // Custom rules for specific challenges + if (challengeId === 'e') { + if (user.totalAudioBalance < 50) { + return c.json({ error: 'denied' }, 400) + } + } + console.log('userScore', userScore, user) + } try { const bnAmount = SolanaUtils.uiAudioToBNWaudio(amount) const identifier = SolanaUtils.constructTransferId( From 60387745f600b3c6e89d8ff935aea01ed140805d Mon Sep 17 00:00:00 2001 From: Ray Jacobson Date: Thu, 6 Nov 2025 09:22:33 -0500 Subject: [PATCH 23/34] Silver tier for endless streak (#13319) ### Description Require silver tier for claiming listen streak. Note: users can still "earn" the reward to keep the streak going, but claiming is gated on silver. ### How Has This Been Tested? _Please describe the tests that you ran to verify your changes. Provide repro instructions & any configuration._ `npm run web:stage` --- apps/anti-abuse-oracle/src/server.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index e7cfe8c..e7c7427 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -194,7 +194,7 @@ app.post('/attestation/:handle', async (c) => { // Custom rules for specific challenges if (challengeId === 'e') { - if (user.totalAudioBalance < 50) { + if (user.totalAudioBalance < 100) { return c.json({ error: 'denied' }, 400) } } From 4a1d281f20dec06096712b27b60bda8621d7062c Mon Sep 17 00:00:00 2001 From: KJ Date: Wed, 12 Nov 2025 11:11:45 -0500 Subject: [PATCH 24/34] Update user score calculation for users without a profile picture (#13399) ### Description Small update to add a reduction in user score if the user is missing their profile picure. Following the update to allow users to sign up without providing one ### How Has This Been Tested? Difficult to test, but wanted to get the code in PR --- apps/anti-abuse-oracle/src/actionLog.ts | 2 ++ apps/anti-abuse-oracle/src/server.tsx | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/apps/anti-abuse-oracle/src/actionLog.ts b/apps/anti-abuse-oracle/src/actionLog.ts index 8fed690..c82979c 100644 --- a/apps/anti-abuse-oracle/src/actionLog.ts +++ b/apps/anti-abuse-oracle/src/actionLog.ts @@ -149,6 +149,7 @@ export async function getUserNormalizedScore(userId: number, wallet: string) { following_count, chat_block_count, is_audius_impersonator, + has_profile_picture, score: shadowban_score, is_blocked } = rows[0] @@ -185,6 +186,7 @@ export async function getUserNormalizedScore(userId: number, wallet: string) { chatBlockCount: chat_block_count, fingerprintCount: numberOfUserWithFingerprint, isAudiusImpersonator: is_audius_impersonator, + hasProfilePicture: has_profile_picture, isEmailDeliverable, isBlocked: is_blocked, shadowbanScore, diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index e7c7427..ba12046 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -415,6 +415,7 @@ app.get('/attestation/ui/user', async (c) => { + @@ -447,6 +448,16 @@ app.get('/attestation/ui/user', async (c) => { > {userScore.chatBlockCount} +
Fast Challenge Count Following Count Chat Block CountHas Profile Picture Audius Impersonator
+ {userScore.hasProfilePicture.toString()}{' '} + {!userScore.hasProfilePicture ? '(-100)' : ''} + Date: Tue, 2 Dec 2025 10:19:25 -0700 Subject: [PATCH 25/34] Update 'e' challenge to require verification (#13471) --- apps/anti-abuse-oracle/src/server.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index ba12046..0bc4325 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -194,7 +194,7 @@ app.post('/attestation/:handle', async (c) => { // Custom rules for specific challenges if (challengeId === 'e') { - if (user.totalAudioBalance < 100) { + if (!user.isVerified) { return c.json({ error: 'denied' }, 400) } } From 7c1adbe2939229c1813f02a35c8fa63d4dbd6f25 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Fri, 2 Jan 2026 15:28:42 -0800 Subject: [PATCH 26/34] Drop staging (#13540) Drop staging CI, env, and all code references Updates CI to deploy to RC for changes on main --- apps/anti-abuse-oracle/src/config.ts | 2 +- apps/anti-abuse-oracle/src/sdk.ts | 3 +-- apps/anti-abuse-oracle/src/server.tsx | 9 ++------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/anti-abuse-oracle/src/config.ts b/apps/anti-abuse-oracle/src/config.ts index 94572c7..7658ea2 100644 --- a/apps/anti-abuse-oracle/src/config.ts +++ b/apps/anti-abuse-oracle/src/config.ts @@ -14,7 +14,7 @@ export const InstructionsProgram = new PublicKey( 'Sysvar1nstructions1111111111111111111111111' ) -export type Environment = 'dev' | 'stage' | 'prod' +export type Environment = 'dev' | 'prod' // reads .env file based on environment const readDotEnv = () => { diff --git a/apps/anti-abuse-oracle/src/sdk.ts b/apps/anti-abuse-oracle/src/sdk.ts index 7df9042..7d7cc20 100644 --- a/apps/anti-abuse-oracle/src/sdk.ts +++ b/apps/anti-abuse-oracle/src/sdk.ts @@ -3,10 +3,9 @@ import { readConfig, Environment } from './config' const environmentToSdkEnvironment: Record< Environment, - 'development' | 'staging' | 'production' + 'development' | 'production' > = { dev: 'development', - stage: 'staging', prod: 'production' } diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 0bc4325..c0c7ef9 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -23,13 +23,8 @@ import { useEmail, userFingerprints } from './identity' import { cors } from 'hono/cors' import { getAudiusSdk } from './sdk' -let CONTENT_NODE = 'https://creatornode2.audius.co' -let FRONTEND = 'https://audius.co' - -if (config.environment == 'stage') { - CONTENT_NODE = 'https://creatornode10.staging.audius.co' - FRONTEND = 'https://staging.audius.co' -} +const CONTENT_NODE = 'https://creatornode2.audius.co' +const FRONTEND = 'https://audius.co' let { AAO_AUTH_USER, AAO_AUTH_PASSWORD } = process.env if (!AAO_AUTH_USER) { From 23ff083f7f44f8cefa14e8f440cd1078d50bc2ed Mon Sep 17 00:00:00 2001 From: Ray Jacobson Date: Mon, 5 Jan 2026 15:27:12 -0800 Subject: [PATCH 27/34] pad aao signature to 130 (#13546) --- apps/anti-abuse-oracle/src/server.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index c0c7ef9..adcfd90 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -210,9 +210,9 @@ app.post('/attestation/:handle', async (c) => { Buffer.from(toSignStr), config.privateSignerAddress ) - const result = new bn(Uint8Array.of(...signature, recoveryId)).toString( - 'hex' - ) + const result = new bn(Uint8Array.of(...signature, recoveryId)) + .toString('hex') + .padStart(130, '0') return c.json({ result }) } catch (error) { From 4c2ce0f9558f182aa2a7f134bfab30497a281501 Mon Sep 17 00:00:00 2001 From: Ray Jacobson Date: Mon, 2 Feb 2026 16:49:05 -0800 Subject: [PATCH 28/34] Update rewards UI (#13637) * Remove trending rewards banner * Update styles for claim UI * Update card sorting, styling * Consolidate competition rewards and achievement rewards into single rewards * Require verification on specific reward types * Update mobile + web image simulator_screenshot_A8668EA3-5770-4517-969E-C3AC4DEB9E32 image --- apps/anti-abuse-oracle/src/server.tsx | 49 +++++++++++++++++++-------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index adcfd90..e7708ed 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -40,7 +40,32 @@ if (!AAO_AUTH_PASSWORD) { } const rewardAmountRatio = 10 -const skipValidationChallenges = ['dvl'] +const openRewards = [ + 'dvl', // daily volume rewards + 't', // tastemaker + 'tp', // trending playlists + 'tt', // trending + 'tut', // trending underground + 'b', // audio match buy (from verified user) + 'rd', // referred (by verified user) + 'w', // remix contest winner (from verified user) + 'cs', // cosign (from verified user) +] + +const verifiedRewards = [ + 'u', // uploads + 's', // audio match sell + 'r', // referral + 'c', // first comment + 'cp', // comment pin + 'e', // listen streak + 'fp', // first playlist + 'm', // mobile install + 'p', // profile completion + 'p1', // 250 plays + 'p2', // 1000 plays + 'p3', // 10000 plays +] const sdk = getAudiusSdk() @@ -174,27 +199,21 @@ app.post('/attestation/:handle', async (c) => { return c.json({ error: `handle not found: ${handle}` }, 404) } const user = users[0]! - - if (!skipValidationChallenges.includes(challengeId)) { - // pass / fail + if (verifiedRewards.includes(challengeId)) { + if (!user.isVerified) { + return c.json({ error: 'denied' }, 400) + } + } + if (openRewards.includes(challengeId)) { const userScore = await getUserNormalizedScore( HashId.parse(user.id), user.wallet ) - - // Reward attestation proportional to user score confidence - if (userScore.overallScore < (amount as number) / rewardAmountRatio) { + if (userScore.overallScore < -1000) { return c.json({ error: 'denied' }, 400) } - - // Custom rules for specific challenges - if (challengeId === 'e') { - if (!user.isVerified) { - return c.json({ error: 'denied' }, 400) - } - } - console.log('userScore', userScore, user) } + try { const bnAmount = SolanaUtils.uiAudioToBNWaudio(amount) const identifier = SolanaUtils.constructTransferId( From e3db4e6f95b417eed2fe27dfb89184682eff781e Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 10 Feb 2026 15:52:59 -0800 Subject: [PATCH 29/34] Drop trending playlist reward and notification (#13684) --- apps/anti-abuse-oracle/src/server.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index e7708ed..9a99440 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -43,7 +43,6 @@ const rewardAmountRatio = 10 const openRewards = [ 'dvl', // daily volume rewards 't', // tastemaker - 'tp', // trending playlists 'tt', // trending 'tut', // trending underground 'b', // audio match buy (from verified user) From 0ee4db46416f709c09f73737df5b48e55a1bbe2f Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Wed, 11 Feb 2026 10:49:30 -0800 Subject: [PATCH 30/34] Use non-full user endpoints (#13685) --- apps/anti-abuse-oracle/src/server.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 9a99440..8950540 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -193,11 +193,10 @@ app.post('/attestation/:handle', async (c) => { const handle = c.req.param('handle').toLowerCase() const { challengeId, challengeSpecifier, amount } = await c.req.json() - const { data: users } = await sdk.full.users.getUserByHandle({ handle }) - if (!users || !users[0]) { + const { data: user } = await sdk.users.getUserByHandle({ handle }) + if (!user) { return c.json({ error: `handle not found: ${handle}` }, 404) } - const user = users[0]! if (verifiedRewards.includes(challengeId)) { if (!user.isVerified) { return c.json({ error: 'denied' }, 400) From 8a9d67e1af1d86abe09fd8075b4a10bd71449919 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Fri, 13 Feb 2026 11:55:41 -0800 Subject: [PATCH 31/34] Revert apis (#13700) --- apps/anti-abuse-oracle/src/server.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 8950540..9a99440 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -193,10 +193,11 @@ app.post('/attestation/:handle', async (c) => { const handle = c.req.param('handle').toLowerCase() const { challengeId, challengeSpecifier, amount } = await c.req.json() - const { data: user } = await sdk.users.getUserByHandle({ handle }) - if (!user) { + const { data: users } = await sdk.full.users.getUserByHandle({ handle }) + if (!users || !users[0]) { return c.json({ error: `handle not found: ${handle}` }, 404) } + const user = users[0]! if (verifiedRewards.includes(challengeId)) { if (!user.isVerified) { return c.json({ error: 'denied' }, 400) From fbe7ea8bfe127bbfaceaa5ab848827972521ce9e Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 19 Feb 2026 12:14:40 -0800 Subject: [PATCH 32/34] Migrate to v1 methods and types (#13728) --- apps/anti-abuse-oracle/src/server.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 9a99440..41866a3 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -193,7 +193,7 @@ app.post('/attestation/:handle', async (c) => { const handle = c.req.param('handle').toLowerCase() const { challengeId, challengeSpecifier, amount } = await c.req.json() - const { data: users } = await sdk.full.users.getUserByHandle({ handle }) + const { data: users } = await sdk.users.getUserByHandle({ handle }) if (!users || !users[0]) { return c.json({ error: `handle not found: ${handle}` }, 404) } From 6a6e07bf02da7ac81879c916c21546aaf15cd19a Mon Sep 17 00:00:00 2001 From: Ray Jacobson Date: Tue, 31 Mar 2026 17:22:21 -0700 Subject: [PATCH 33/34] Remove reward amount ratio from claim message (#14035) Removed reward amount ratio display from the UI. --- apps/anti-abuse-oracle/src/server.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 41866a3..35646db 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -39,7 +39,6 @@ if (!AAO_AUTH_PASSWORD) { ) } -const rewardAmountRatio = 10 const openRewards = [ 'dvl', // daily volume rewards 't', // tastemaker @@ -415,10 +414,6 @@ app.get('/attestation/ui/user', async (c) => { -

- Allowed to claim up to{' '} - {rewardAmountRatio * userScore.overallScore} $AUDIO per reward. -

Score Breakdown

From fe3923b74bbe4ff3711a2e86bb99995e91fb8dab Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Mon, 6 Apr 2026 13:40:27 -0700 Subject: [PATCH 34/34] Fix getUser --- apps/anti-abuse-oracle/src/server.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/anti-abuse-oracle/src/server.tsx b/apps/anti-abuse-oracle/src/server.tsx index 35646db..91fbbe4 100644 --- a/apps/anti-abuse-oracle/src/server.tsx +++ b/apps/anti-abuse-oracle/src/server.tsx @@ -192,11 +192,10 @@ app.post('/attestation/:handle', async (c) => { const handle = c.req.param('handle').toLowerCase() const { challengeId, challengeSpecifier, amount } = await c.req.json() - const { data: users } = await sdk.users.getUserByHandle({ handle }) - if (!users || !users[0]) { + const { data: user } = await sdk.users.getUserByHandle({ handle }) + if (!user) { return c.json({ error: `handle not found: ${handle}` }, 404) } - const user = users[0]! if (verifiedRewards.includes(challengeId)) { if (!user.isVerified) { return c.json({ error: 'denied' }, 400)