Skip to content

Commit

Permalink
admin page to see all cash txns
Browse files Browse the repository at this point in the history
resolves MAN-1724
  • Loading branch information
sipec committed Sep 23, 2024
1 parent b3bdf83 commit f04a1aa
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 63 deletions.
2 changes: 2 additions & 0 deletions backend/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({})

Expand Down Expand Up @@ -424,6 +425,7 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
'record-comment-view': recordCommentView,
'get-cashouts': getCashouts,
'get-kyc-stats': getKYCStats,
txns: getTxns,
}

Object.entries(handlers).forEach(([path, handler]) => {
Expand Down
39 changes: 7 additions & 32 deletions backend/api/src/get-managrams.ts
Original file line number Diff line number Diff line change
@@ -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[]
}
57 changes: 57 additions & 0 deletions backend/api/src/get-txns.ts
Original file line number Diff line number Diff line change
@@ -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)
}
19 changes: 19 additions & 0 deletions common/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,7 @@ export const API = (_apiTypeCheck = {
props: z.object({}),
returns: {} as { payout: number },
},
// deprecated. use /txns instead
managrams: {
method: 'GET',
visibility: 'public',
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions common/src/envs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
85 changes: 54 additions & 31 deletions docs/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,7 @@ Example response:
]
```
### `GET /v0/managrams`
### `GET /v0/managrams` (Deprecated)
Gets a list of managrams, ordered by creation time descending.
Expand All @@ -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`
Expand Down Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions web/pages/admin/cash-txns.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Page trackPageView={false}>
<Col className="gap-4">
<Row className="items-start justify-between">
<Title>Cash Transactions</Title>
<div className="flex gap-1">
<Button onClick={pagination.getPrev} disabled={pagination.isStart}>
Previous
</Button>
<Button onClick={pagination.getNext}>Next</Button>
</div>
</Row>
<Table className="w-full">
<thead>
<tr>
<th>ID</th>
<th>From</th>
<th>To</th>
<th>Amount</th>
<th>Category</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
{pagination.items.map((txn) => (
<TxnRow key={txn.id} txn={txn} />
))}
</tbody>
</Table>
<div className="flex items-end gap-1">
<Button onClick={pagination.getPrev} disabled={pagination.isStart}>
Previous
</Button>
<Button onClick={pagination.getNext}>Next</Button>
</div>
</Col>
</Page>
)
}

const TxnRow = ({ txn }: { txn: Txn }) => {
const a = useDisplayUserById(txn.fromId)
const b = useDisplayUserById(txn.toId)

return (
<tr>
<td>
<a
className="text-primary-700 hover-underline cursor-pointer"
href={supabaseConsoleTxnPath(txn.id)}
>
{txn.id}
</a>
</td>
<td>
{txn.fromType === 'USER' && a ? (
<Row className="gap-1">
<Avatar username={a.username} avatarUrl={a.avatarUrl} size="xs" />
<UserLink user={a} />
</Row>
) : (
txn.fromId
)}
</td>
<td>
{txn.toType === 'USER' && b ? (
<Row className="gap-1">
<Avatar username={b.username} avatarUrl={b.avatarUrl} size="xs" />
<UserLink user={b} />
</Row>
) : (
txn.toId
)}
</td>
<td>{formatMoney(txn.amount, 'CASH')}</td>
<td>{txn.category}</td>
<td>{formatTime(txn.createdTime)}</td>
</tr>
)
}

0 comments on commit f04a1aa

Please sign in to comment.