diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index 3b61ef986d..4f325b5245 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -193,6 +193,7 @@ import { getInterestingGroupsFromViews } from 'api/get-interesting-groups-from-v import { completeCashoutSession } from 'api/gidx/complete-cashout-session' import { getCashouts } from './get-cashouts' import { getKYCStats } from './get-kyc-stats' +import { getTxns } from './get-txns' const allowCorsUnrestricted: RequestHandler = cors({}) @@ -424,6 +425,7 @@ const handlers: { [k in APIPath]: APIHandler } = { 'record-comment-view': recordCommentView, 'get-cashouts': getCashouts, 'get-kyc-stats': getKYCStats, + txns: getTxns, } Object.entries(handlers).forEach(([path, handler]) => { diff --git a/backend/api/src/get-managrams.ts b/backend/api/src/get-managrams.ts index 11e43d7416..a67d08b0e0 100644 --- a/backend/api/src/get-managrams.ts +++ b/backend/api/src/get-managrams.ts @@ -1,38 +1,13 @@ -import { millisToTs } from 'common/supabase/utils' import { type APIHandler } from './helpers/endpoint' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { convertTxn } from 'common/supabase/txns' -import { buildArray } from 'common/util/array' -import { - from, - limit, - orderBy, - renderSql, - select, - where, -} from 'shared/supabase/sql-builder' +import { getTxnsMain } from './get-txns' import { ManaPayTxn } from 'common/txn' export const getManagrams: APIHandler<'managrams'> = async (props) => { - const { limit: limitValue, toId, fromId, before, after } = props + const txns = await getTxnsMain({ + ...props, + offset: 0, + category: 'MANA_PAYMENT', + }) - const pg = createSupabaseDirectClient() - - const conditions = buildArray( - where('category = ${category}', { category: 'MANA_PAYMENT' }), - before && where('created_time < ${before}', { before: millisToTs(before) }), - after && where('created_time > ${after}', { after: millisToTs(after) }), - toId && where('to_id = ${toId}', { toId }), - fromId && where('from_id = ${fromId}', { fromId }) - ) - - const query = renderSql( - select('*'), - from('txns'), - ...conditions, - orderBy('created_time desc'), - limitValue && limit(limitValue) - ) - - return (await pg.map(query, [], convertTxn)) as ManaPayTxn[] + return txns as ManaPayTxn[] } diff --git a/backend/api/src/get-txns.ts b/backend/api/src/get-txns.ts new file mode 100644 index 0000000000..19bd3857f3 --- /dev/null +++ b/backend/api/src/get-txns.ts @@ -0,0 +1,57 @@ +import { APIHandler } from 'api/helpers/endpoint' +import { convertTxn } from 'common/supabase/txns' +import { millisToTs } from 'common/supabase/utils' +import { createSupabaseDirectClient } from 'shared/supabase/init' +import { + from, + limit, + orderBy, + renderSql, + select, + where, +} from 'shared/supabase/sql-builder' + +export const getTxns: APIHandler<'txns'> = async (props) => { + return await getTxnsMain(props) +} + +export const getTxnsMain = async (props: { + token?: string + offset: number + limit: number + before?: number + after?: number + toId?: string + fromId?: string + category?: string +}) => { + const { + token, + offset, + limit: limitValue, + before, + after, + toId, + fromId, + category, + } = props + + const pg = createSupabaseDirectClient() + + const query = renderSql( + select('*'), + from('txns'), + + token && where('token = ${token}', { token }), + before && where('created_time < ${before}', { before: millisToTs(before) }), + after && where('created_time > ${after}', { after: millisToTs(after) }), + toId && where('to_id = ${toId}', { toId }), + fromId && where('from_id = ${fromId}', { fromId }), + category && where('category = ${category}', { category }), + + orderBy('created_time desc'), + limit(limitValue, offset) + ) + + return await pg.map(query, [], convertTxn) +} diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index b033f76eef..1e7efa6452 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -726,6 +726,7 @@ export const API = (_apiTypeCheck = { props: z.object({}), returns: {} as { payout: number }, }, + // deprecated. use /txns instead managrams: { method: 'GET', visibility: 'public', @@ -1657,6 +1658,24 @@ export const API = (_apiTypeCheck = { }[] }, }, + txns: { + method: 'GET', + visibility: 'public', + authed: true, + props: z + .object({ + token: z.string().optional(), + offset: z.coerce.number().default(0), + limit: z.coerce.number().gte(0).lte(100).default(100), + before: z.coerce.number().optional(), + after: z.coerce.number().optional(), + toId: z.string().optional(), + fromId: z.string().optional(), + category: z.string().optional(), + }) + .strict(), + returns: [] as Txn[], + }, } as const) export type APIPath = keyof typeof API diff --git a/common/src/envs/constants.ts b/common/src/envs/constants.ts index eebf1d19f3..ff1ef716c2 100644 --- a/common/src/envs/constants.ts +++ b/common/src/envs/constants.ts @@ -365,6 +365,11 @@ export function supabaseConsoleContractPath(contractId: string) { return `https://supabase.com/dashboard/project/${ENV_CONFIG.supabaseInstanceId}/editor/${tableId}?filter=id%3Aeq%3A${contractId}` } +export function supabaseConsoleTxnPath(txnId: string) { + const tableId = ENV === 'DEV' ? 20014 : 25940 + return `https://supabase.com/dashboard/project/${ENV_CONFIG.supabaseInstanceId}/editor/${tableId}?filter=id%3Aeq%3A${txnId}` +} + export const GOOGLE_PLAY_APP_URL = 'https://play.google.com/store/apps/details?id=com.markets.manifold' export const APPLE_APP_URL = diff --git a/docs/docs/api.md b/docs/docs/api.md index a1670c9f87..4fd52c4fbe 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -1003,7 +1003,7 @@ Example response: ] ``` -### `GET /v0/managrams` +### `GET /v0/managrams` (Deprecated) Gets a list of managrams, ordered by creation time descending. @@ -1017,36 +1017,7 @@ Parameters: Requires no auth. -Example request: - -```bash -curl "https://api.manifold.markets/v0/managrams?toId=IPTOzEqrpkWmEzh6hwvAyY9PqFb2" -X GET -``` - -Example response: - -```json -[ - { - "id": "INKcoBUVT914i1XUJ6rG", - "data": { - "groupId": "e097e0c5-3ce0-4eb2-9ca7-6554f86b84cd", - "message": "Puzzles for Progress", - "visibility": "public" - }, - "toId": "AJwLWoo3xue32XIiAVrL5SyR1WB2", - "token": "M$", - "amount": 2500, - "fromId": "jO7sUhIDTQbAJ3w86akzncTlpRG2", - "toType": "USER", - "category": "MANA_PAYMENT", - "fromType": "USER", - "createdTime": 1695665438987, - "description": "Mana payment 2500 from MichaelWheatley to jO7sUhIDTQbAJ3w86akzncTlpRG2" - }, - ... -] -``` +_This api is deprecated in favor of the more versatile [/v0/txns/](#get-v0txns) api below._ ### `POST /v0/managram` @@ -1108,6 +1079,58 @@ See a specific user's answers to compatibility questions. Requires no auth. +### `GET /v0/txns` + +Get a list of transactions, ordered by creation date descending. + +Parameters: + +- `token`: Optional. Type of token (e.g., 'CASH', 'MANA') +- `offset`: Optional. Number of records to skip (for pagination). Default is 0. +- `limit`: Optional. Maximum number of records to return. The default and maximum are both 100. +- `before`: Optional. Include only transactions created before this timestamp. +- `after`: Optional. Include only transactions created after this timestamp. +- `toId`: Optional. Include only transactions to the user with this ID. +- `fromId`: Optional. Include only transactions from the user with this ID. +- `category`: Optional. Include only transactions of this category. + +Requires no auth. + +Example request: + +```bash +curl "https://api.manifold.markets/v0/txns?limit=10&category=MANA_PAYMENT" -X GET +``` + +Response type: An array of `Txn`. + +Example response: + +```json +[ + { + "id": "INKcoBUVT914i1XUJ6rG", + "data": { + "groupId": "e097e0c5-3ce0-4eb2-9ca7-6554f86b84cd", + "message": "Puzzles for Progress", + "visibility": "public" + }, + "toId": "AJwLWoo3xue32XIiAVrL5SyR1WB2", + "token": "M$", + "amount": 2500, + "fromId": "jO7sUhIDTQbAJ3w86akzncTlpRG2", + "toType": "USER", + "category": "MANA_PAYMENT", + "fromType": "USER", + "createdTime": 1695665438987, + "description": "Mana payment 2500 from MichaelWheatley to jO7sUhIDTQbAJ3w86akzncTlpRG2" + }, + ... +] +``` + +Note: This API corresponds to the `txns` postgres table and does not include bets and liquidity injections. + Example response (truncated): ```json diff --git a/web/pages/admin/cash-txns.tsx b/web/pages/admin/cash-txns.tsx new file mode 100644 index 0000000000..c1d04aed04 --- /dev/null +++ b/web/pages/admin/cash-txns.tsx @@ -0,0 +1,115 @@ +import { supabaseConsoleTxnPath } from 'common/envs/constants' +import { type Txn } from 'common/txn' +import { formatMoney } from 'common/util/format' +import { useCallback } from 'react' +import { Button } from 'web/components/buttons/button' +import { Col } from 'web/components/layout/col' +import { Page } from 'web/components/layout/page' +import { Row } from 'web/components/layout/row' +import { Avatar } from 'web/components/widgets/avatar' +import { Table } from 'web/components/widgets/table' +import { Title } from 'web/components/widgets/title' +import { UserLink } from 'web/components/widgets/user-link' +import { useAdmin } from 'web/hooks/use-admin' +import { usePagination } from 'web/hooks/use-pagination' +import { useDisplayUserById } from 'web/hooks/use-user-supabase' +import { api } from 'web/lib/api/api' +import { formatTime } from 'web/lib/util/time' + +export default function CashTxnsPage() { + const fetch = useCallback( + ({ limit, offset }: { limit: number; offset: number }) => + api('txns', { limit, offset, token: 'CASH' }), + [] + ) + + const pagination = usePagination({ + pageSize: 50, + prefix: [] as Txn[], + q: fetch, + }) + + // TODO: it's actually ok for anyone to see this page + const isAdmin = useAdmin() + if (!isAdmin) return <> + + return ( + + + + Cash Transactions +
+ + +
+
+ + + + + + + + + + + + + {pagination.items.map((txn) => ( + + ))} + +
IDFromToAmountCategoryCreated At
+
+ + +
+ +
+ ) +} + +const TxnRow = ({ txn }: { txn: Txn }) => { + const a = useDisplayUserById(txn.fromId) + const b = useDisplayUserById(txn.toId) + + return ( + + + + {txn.id} + + + + {txn.fromType === 'USER' && a ? ( + + + + + ) : ( + txn.fromId + )} + + + {txn.toType === 'USER' && b ? ( + + + + + ) : ( + txn.toId + )} + + {formatMoney(txn.amount, 'CASH')} + {txn.category} + {formatTime(txn.createdTime)} + + ) +}