Skip to content

Commit

Permalink
mani: Balance log
Browse files Browse the repository at this point in the history
  • Loading branch information
IanPhilips committed Jan 16, 2025
1 parent 3660731 commit 66d208c
Show file tree
Hide file tree
Showing 24 changed files with 427 additions and 169 deletions.
7 changes: 7 additions & 0 deletions web/lib/util/time.ts → client-common/src/lib/time.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { DAY_MS } from 'common/util/time'

dayjs.extend(relativeTime)

Expand Down Expand Up @@ -57,3 +58,9 @@ export const getCountdownStringHoursMinutes = (endDate: Date) => {

return `${isPast ? '-' : ''} ${hoursStr} ${minutesStr}`
}
export const customFormatTime = (time: number) => {
if (time > Date.now() - DAY_MS) {
return formatJustTime(time)
}
return formatTimeShort(time)
}
123 changes: 123 additions & 0 deletions common/src/balance-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,126 @@ export const isBetChange = (
export const isTxnChange = (
change: AnyBalanceChangeType
): change is TxnBalanceChange => !('bet' in change)

export const betChangeToText = (change: BetBalanceChange) => {
const { type, bet } = change
const { outcome } = bet
return type === 'redeem_shares'
? `Redeem shares`
: type === 'loan_payment'
? `Pay back loan`
: type === 'fill_bet'
? `Fill ${outcome} order`
: type === 'sell_shares'
? `Sell ${outcome} shares`
: `Buy ${outcome}`
}

export const txnTitle = (change: TxnBalanceChange) => {
const { type, contract, user, questType, charity } = change

if (user) {
return user.username
}
if (charity) {
return charity.name
}
if (contract) {
return contract.question
}

switch (type) {
case 'QUEST_REWARD':
return questType ? questTypeToDescription(questType) : ''
case 'BETTING_STREAK_BONUS':
return 'Prediction streak bonus' // usually the question instead
case 'LOAN':
return 'Loan'
case 'LEAGUE_PRIZE':
return 'League prize'
case 'MANA_PURCHASE':
return 'Mana purchase'
case 'MARKET_BOOST_REDEEM':
return 'Claim boost'
case 'SIGNUP_BONUS':
return change.description ?? 'Signup bonus'
case 'REFERRAL':
return 'Referral bonus'
case 'CONSUME_SPICE':
case 'CONSUME_SPICE_DONE':
return `Redeem prize points for mana`
case 'CONVERT_CASH':
case 'CONVERT_CASH_DONE':
return 'Redeem sweepcash for mana'
case 'CASH_OUT':
return 'Redemption request'
case 'CASH_BONUS':
return 'Sweepcash bonus'
case 'KYC_BONUS':
return 'ID verification bonus'
default:
return type
}
}

export const txnTypeToDescription = (txnCategory: string) => {
switch (txnCategory) {
case 'MARKET_BOOST_CREATE':
return 'Boost'
case 'CANCEL_UNIQUE_BETTOR_BONUS':
return 'Cancel unique trader bonus'
case 'PRODUCE_SPICE':
case 'CONTRACT_RESOLUTION_PAYOUT':
return 'Payout'
case 'CREATE_CONTRACT_ANTE':
return 'Ante'
case 'UNIQUE_BETTOR_BONUS':
return 'Trader bonus'
case 'BETTING_STREAK_BONUS':
return 'Quests'
case 'SIGNUP_BONUS':
case 'KYC_BONUS':
return 'New user bonuses'
case 'REFERRAL':
return 'Quests'
case 'QUEST_REWARD':
return 'Quests'
case 'CONTRACT_UNDO_PRODUCE_SPICE':
case 'CONTRACT_UNDO_RESOLUTION_PAYOUT':
return 'Unresolve'
case 'CONSUME_SPICE':
case 'CONSUME_SPICE_DONE':
case 'CONVERT_CASH':
case 'CONVERT_CASH_DONE':
return ''
case 'MANA_PURCHASE':
return ''
case 'ADD_SUBSIDY':
return 'Subsidy'
case 'MARKET_BOOST_REDEEM':
return 'Leagues'
case 'BOUNTY_POSTED':
return 'Ante'
case 'BOUNTY_AWARDED':
return 'Bounty awarded'
case 'MANA_PAYMENT':
return 'User payment'
case 'CHARITY':
return 'Donation'
case 'LOAN':
return ''
default:
return null
}
}

const questTypeToDescription = (questType: QuestType) => {
switch (questType) {
case 'SHARES':
return 'Sharing bonus'
case 'MARKETS_CREATED':
return 'Creation bonus'
default:
return 'questType'
}
}
12 changes: 7 additions & 5 deletions mani/app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { auth } from 'lib/firebase/init'
import { clearData } from 'lib/auth-storage'
import { Button } from 'components/buttons/button'
import { router } from 'expo-router'
import { BalanceChangeTable } from 'components/portfolio/balance-change-table'
import { buildArray } from 'common/util/array'

export default function Profile() {
const color = useColor()
Expand Down Expand Up @@ -93,16 +95,16 @@ export default function Profile() {
/>
</Row>
<TopTabs
tabs={[
tabs={buildArray(
{
title: 'Positions',
content: <Positions />,
},
{
user && {
title: 'Balance Log',
content: <ThemedText>Balance Log</ThemedText>,
},
]}
content: <BalanceChangeTable user={user} />,
}
)}
/>
</Col>
</Page>
Expand Down
202 changes: 202 additions & 0 deletions mani/components/portfolio/balance-change-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { useState } from 'react'
import { StyleSheet, ScrollView, Pressable } from 'react-native'
import { useAPIGetter } from 'hooks/use-api-getter'
import { User } from 'common/user'
import { Row } from '../layout/row'
import { Col } from '../layout/col'
import { ThemedText } from '../themed-text'
import { useColor } from 'hooks/use-color'
import { Colors } from 'constants/colors'
import {
AnyBalanceChangeType,
BetBalanceChange,
TxnBalanceChange,
isBetChange,
isTxnChange,
txnTitle,
txnTypeToDescription,
} from 'common/balance-change'
import { formatJustDateShort, formatJustTime } from 'client-common/lib/time'
import { useRouter } from 'expo-router'
import { contractPathWithoutContract } from 'common/contract'
import dayjs from 'dayjs'
import { DAY_MS } from 'common/util/time'
import { TokenNumber } from 'components/token/token-number'

export const BalanceChangeTable = (props: { user: User }) => {
const { user } = props

const [before, setBefore] = useState<number | undefined>(undefined)
const [after, setAfter] = useState(
dayjs().startOf('day').subtract(14, 'day').valueOf()
)

const { data: allBalanceChanges } = useAPIGetter('get-balance-changes', {
userId: user.id,
before,
after,
})

const balanceChanges = allBalanceChanges ?? []

return (
<ScrollView style={styles.container}>
<Col style={{ gap: 16, paddingVertical: 16 }}>
<RenderBalanceChanges balanceChanges={balanceChanges} />
</Col>
</ScrollView>
)
}

function RenderBalanceChanges(props: {
balanceChanges: AnyBalanceChangeType[]
}) {
const { balanceChanges } = props

return (
<>
{balanceChanges.map((change, i) => {
if (isBetChange(change)) {
return (
<BetBalanceChangeRow
key={change.key}
change={change}
token={change.contract.token}
/>
)
} else if (isTxnChange(change)) {
return (
<TxnBalanceChangeRow
key={change.key}
change={change as TxnBalanceChange}
/>
)
}
})}
</>
)
}

const betChangeToText = (change: BetBalanceChange) => {
const { type, bet } = change
const { outcome } = bet
return type === 'redeem_shares'
? `Redeem shares`
: type === 'loan_payment'
? `Pay back loan`
: type === 'fill_bet'
? `Fill ${outcome} order`
: type === 'sell_shares'
? `Sell ${outcome} shares`
: `Buy ${outcome}`
}

const BetBalanceChangeRow = (props: {
change: BetBalanceChange
token: 'MANA' | 'CASH'
}) => {
const { change, token } = props
const { amount, contract, answer } = change
const { slug, question, creatorUsername } = contract
const router = useRouter()
const color = useColor()

const onPress = () => {
if (slug) {
router.push(contractPathWithoutContract(creatorUsername, slug) as any)
}
}

return (
<Pressable onPress={onPress}>
<Row style={styles.changeRow}>
<ThemedText size="sm">{amount > 0 ? '+' : '-'}</ThemedText>
<TokenNumber
amount={Math.abs(amount)}
token={token}
style={{
color: amount > 0 ? color.profitText : color.textTertiary,
}}
/>
<Col style={{ flex: 1 }}>
<ThemedText size="sm" numberOfLines={2}>
{question}
</ThemedText>
<ThemedText size="xs" color={color.textTertiary}>
{betChangeToText(change)} {answer ? ` on ${answer.text}` : ''}
</ThemedText>
</Col>
<ThemedText size="xs" color={color.textTertiary}>
{customFormatTime(change.createdTime)}
</ThemedText>
</Row>
</Pressable>
)
}

const customFormatTime = (time: number) => {
if (time > Date.now() - DAY_MS) {
return formatJustTime(time)
}
return formatJustDateShort(time)
}

const TxnBalanceChangeRow = (props: { change: TxnBalanceChange }) => {
const { change } = props
const { contract, amount, type, token, user, charity, description } = change
const router = useRouter()

const onPress = () => {
if (contract?.slug) {
router.push(
contractPathWithoutContract(
contract.creatorUsername,
contract.slug
) as any
)
} else if (user?.username) {
router.push(('/' + user.username) as any)
} else if (charity?.slug) {
router.push(('/charity/' + charity.slug) as any)
}
}

return (
<Pressable onPress={onPress}>
<Row style={styles.changeRow}>
<ThemedText size="sm">{amount > 0 ? '+' : '-'}</ThemedText>
<TokenNumber
amount={Math.abs(amount)}
token={token as any}
style={{
color: amount > 0 ? Colors.profitText : Colors.textTertiary,
}}
/>
<Col style={{ flex: 1 }}>
<ThemedText size="sm" numberOfLines={2}>
{txnTitle(change)}
</ThemedText>
<ThemedText size="xs" color={Colors.textTertiary}>
{txnTypeToDescription(type) ?? description ?? type}
</ThemedText>
</Col>
<ThemedText size="xs" color={Colors.textTertiary}>
{customFormatTime(change.createdTime)}
</ThemedText>
</Row>
</Pressable>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
changeRow: {
padding: 12,
gap: 2,
alignItems: 'center',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: Colors.border,
},
})
Loading

0 comments on commit 66d208c

Please sign in to comment.