Move read & update into transaction

This commit is contained in:
Ian Philips 2022-07-05 10:06:05 -06:00
parent 0aa00b75d5
commit 2e6801ff72
7 changed files with 92 additions and 86 deletions

View File

@ -58,4 +58,3 @@ export type notification_reason_types =
| 'you_referred_user' | 'you_referred_user'
| 'user_joined_to_bet_on_your_market' | 'user_joined_to_bet_on_your_market'
| 'unique_bettors_on_your_contract' | 'unique_bettors_on_your_contract'
| 'daily_bonus'

View File

@ -16,13 +16,8 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
amount: number amount: number
token: 'M$' // | 'USD' | MarketOutcome token: 'M$' // | 'USD' | MarketOutcome
category: category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
| 'CHARITY'
| 'MANALINK'
| 'TIP'
| 'REFERRAL'
| 'UNIQUE_BETTOR_BONUS'
| 'DAILY_BONUS' // | 'BET'
// Any extra data // Any extra data
data?: { [key: string]: any } data?: { [key: string]: any }
@ -61,7 +56,7 @@ type Referral = {
type Bonus = { type Bonus = {
fromType: 'BANK' fromType: 'BANK'
toType: 'USER' toType: 'USER'
category: 'DAILY_BONUS' | 'UNIQUE_BETTOR_BONUS' category: 'UNIQUE_BETTOR_BONUS'
} }
export type DonationTxn = Txn & Donation export type DonationTxn = Txn & Donation

View File

@ -57,7 +57,7 @@ export type PrivateUser = {
initialIpAddress?: string initialIpAddress?: string
apiKey?: string apiKey?: string
notificationPreferences?: notification_subscribe_types notificationPreferences?: notification_subscribe_types
lastTimeReceivedBonuses?: number lastTimeCheckedBonuses?: number
} }
export type notification_subscribe_types = 'all' | 'less' | 'none' export type notification_subscribe_types = 'all' | 'less' | 'none'

View File

@ -13,31 +13,38 @@ import { Contract } from '../../common/lib/contract'
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants' import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime() const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime()
const QUERY_LIMIT_SECONDS = 60
export const getdailybonuses = newEndpoint({}, async (req, auth) => { export const getdailybonuses = newEndpoint({}, async (req, auth) => {
log('Inside endpoint handler.') const { user, lastTimeCheckedBonuses } = await firestore.runTransaction(
const { user, fromUser, lastReceivedBonusTime } = async (trans) => {
await firestore.runTransaction(async (trans) => { const userSnap = await trans.get(
const userSnap = await firestore.doc(`private-users/${auth.uid}`).get() firestore.doc(`private-users/${auth.uid}`)
)
if (!userSnap.exists) throw new APIError(400, 'User not found.') if (!userSnap.exists) throw new APIError(400, 'User not found.')
const user = userSnap.data() as PrivateUser const user = userSnap.data() as PrivateUser
// TODO: switch to prod id const lastTimeCheckedBonuses = user.lastTimeCheckedBonuses ?? 0
const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account if (Date.now() - lastTimeCheckedBonuses < QUERY_LIMIT_SECONDS * 1000)
//const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID, // prod manifold account throw new APIError(
const fromSnap = await firestore.doc(`users/${fromUserId}`).get() 400,
if (!userSnap.exists) throw new APIError(400, 'From user not found.') `Limited to one query per user per ${QUERY_LIMIT_SECONDS} seconds.`
const fromUser = fromSnap.data() as User )
const lastReceivedBonusTime = user.lastTimeReceivedBonuses ?? 0 await trans.update(userSnap.ref, {
if (Date.now() - lastReceivedBonusTime < 5 * 1000) lastTimeCheckedBonuses: Date.now(),
throw new APIError(400, 'Limited to one query per user per 5 seconds.')
await trans.update(firestore.doc(`private-users/${user.id}`), {
lastTimeReceivedBonuses: Date.now(),
}) })
return { user, fromUser, lastReceivedBonusTime } return {
}) user,
lastTimeCheckedBonuses,
// get all users contracts made since implementation time }
}
)
// TODO: switch to prod id
// const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account
const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID // prod manifold account
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
const fromUser = fromSnap.data() as User
// Get all users contracts made since implementation time
const userContractsSnap = await firestore const userContractsSnap = await firestore
.collection(`contracts`) .collection(`contracts`)
.where('creatorId', '==', user.id) .where('creatorId', '==', user.id)
@ -50,8 +57,7 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
for (const contract of userContracts) { for (const contract of userContracts) {
const result = await firestore.runTransaction(async (trans) => { const result = await firestore.runTransaction(async (trans) => {
const contractId = contract.id const contractId = contract.id
// log(`Checking contract ${contractId}`) // Get all bets made on user's contracts
// get all bets made on user's contracts
const bets = ( const bets = (
await firestore await firestore
.collection(`contracts/${contractId}/bets`) .collection(`contracts/${contractId}/bets`)
@ -63,21 +69,21 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
} }
const contractBetsSnap = await trans.getAll(...bets) const contractBetsSnap = await trans.getAll(...bets)
const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet) const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet)
// log(`Got ${contractBets.length} bets`)
const uniqueBettorIdsBeforeLastResetTime = uniq( const uniqueBettorIdsBeforeLastResetTime = uniq(
contractBets contractBets
.filter((bet) => bet.createdTime < lastReceivedBonusTime) .filter((bet) => bet.createdTime < lastTimeCheckedBonuses)
.map((bet) => bet.userId) .map((bet) => bet.userId)
) )
// filter users for ONLY those that have made bets since the last daily bonus received time // Filter users for ONLY those that have made bets since the last daily bonus received time
const uniqueBettorIdsWithBetsAfterLastResetTime = uniq( const uniqueBettorIdsWithBetsAfterLastResetTime = uniq(
contractBets contractBets
.filter((bet) => bet.createdTime > lastReceivedBonusTime) .filter((bet) => bet.createdTime > lastTimeCheckedBonuses)
.map((bet) => bet.userId) .map((bet) => bet.userId)
) )
// filter for users only present in the above list
// Filter for users only present in the above list
const newUniqueBettorIds = const newUniqueBettorIds =
uniqueBettorIdsWithBetsAfterLastResetTime.filter( uniqueBettorIdsWithBetsAfterLastResetTime.filter(
(userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId) (userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId)
@ -87,10 +93,9 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
`Got ${newUniqueBettorIds.length} new unique bettors since last bonus` `Got ${newUniqueBettorIds.length} new unique bettors since last bonus`
) )
if (newUniqueBettorIds.length === 0) { if (newUniqueBettorIds.length === 0) {
// log(`No new unique bettors for contract ${contractId}`)
return nullReturn return nullReturn
} }
// create txn for each unique user // Create combined txn for all unique bettors
const bonusTxnDetails = { const bonusTxnDetails = {
contractId: contractId, contractId: contractId,
uniqueBettors: newUniqueBettorIds.length, uniqueBettors: newUniqueBettorIds.length,
@ -122,7 +127,7 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
result.txn.amount + '', result.txn.amount + '',
contract, contract,
undefined, undefined,
// No need to set the user id, they're the contract creator id // No need to set the user id, we'll use the contract creator id
undefined, undefined,
contract.slug, contract.slug,
contract.question contract.question

View File

@ -198,7 +198,7 @@ export default function Sidebar(props: { className?: string }) {
useEffect(() => { useEffect(() => {
if (!privateUser) return if (!privateUser) return
if (Date.now() - (privateUser.lastTimeReceivedBonuses ?? 0) > 1000) if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 30 * 1000)
requestBonuses({}).catch((error) => { requestBonuses({}).catch((error) => {
console.log("couldn't get bonuses:", error.message) console.log("couldn't get bonuses:", error.message)
}) })

View File

@ -42,6 +42,9 @@ export function groupNotifications(notifications: Notification[]) {
const bonusNotifications = notificationsGroupedByDay.filter( const bonusNotifications = notificationsGroupedByDay.filter(
(notification) => notification.sourceType === 'bonus' (notification) => notification.sourceType === 'bonus'
) )
const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter(
(notification) => notification.sourceType !== 'bonus'
)
if (bonusNotifications.length > 0) { if (bonusNotifications.length > 0) {
notificationGroups = notificationGroups.concat({ notificationGroups = notificationGroups.concat({
notifications: bonusNotifications, notifications: bonusNotifications,
@ -53,9 +56,7 @@ export function groupNotifications(notifications: Notification[]) {
} }
// Group notifications by contract, filtering out bonuses: // Group notifications by contract, filtering out bonuses:
const groupedNotificationsByContractId = groupBy( const groupedNotificationsByContractId = groupBy(
notificationsGroupedByDay.filter( normalNotificationsGroupedByDay,
(notification) => notification.sourceType !== 'bonus'
),
(notification) => { (notification) => {
return notification.sourceContractId return notification.sourceContractId
} }

View File

@ -37,7 +37,7 @@ import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
import { groupBy } from 'lodash' import { groupBy } from 'lodash'
export const NOTIFICATIONS_PER_PAGE = 30 export const NOTIFICATIONS_PER_PAGE = 30
export const HIGHLIGHT_DURATION = 20000 export const HIGHLIGHT_DURATION = 30 * 1000
export default function Notifications() { export default function Notifications() {
const user = useUser() const user = useUser()
@ -100,46 +100,52 @@ export default function Notifications() {
/> />
) )
)} )}
<nav {groupedNotifications.length > NOTIFICATIONS_PER_PAGE && (
className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6" <nav
aria-label="Pagination" className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
> aria-label="Pagination"
<div className="hidden sm:block"> >
<p className="text-sm text-gray-700"> <div className="hidden sm:block">
Showing{' '} <p className="text-sm text-gray-700">
<span className="font-medium"> Showing{' '}
{page === 1 <span className="font-medium">
? page {page === 1
: (page - 1) * NOTIFICATIONS_PER_PAGE} ? page
</span>{' '} : (page - 1) * NOTIFICATIONS_PER_PAGE}
to{' '} </span>{' '}
<span className="font-medium"> to{' '}
{page * NOTIFICATIONS_PER_PAGE} <span className="font-medium">
</span>{' '} {page * NOTIFICATIONS_PER_PAGE}
of{' '} </span>{' '}
<span className="font-medium"> of{' '}
{groupedNotifications.length} <span className="font-medium">
</span>{' '} {groupedNotifications.length}
results </span>{' '}
</p> results
</div> </p>
<div className="flex flex-1 justify-between sm:justify-end"> </div>
<a <div className="flex flex-1 justify-between sm:justify-end">
href="#" <a
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" href="#"
onClick={() => page > 1 && setPage(page - 1)} className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
> onClick={() => page > 1 && setPage(page - 1)}
Previous >
</a> Previous
<a </a>
href="#" <a
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" href="#"
onClick={() => setPage(page + 1)} className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
> onClick={() =>
Next page <
</a> groupedNotifications?.length /
</div> NOTIFICATIONS_PER_PAGE && setPage(page + 1)
</nav> }
>
Next
</a>
</div>
</nav>
)}
</div> </div>
) : ( ) : (
<LoadingIndicator /> <LoadingIndicator />
@ -268,7 +274,7 @@ function IncomeNotificationGroupItem(props: {
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'} className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
> >
<span> <span>
{'Income Summary: '} {'Daily Income Summary: '}
<span className={'text-primary'}>{formatMoney(totalIncome)}</span> <span className={'text-primary'}>{formatMoney(totalIncome)}</span>
</span> </span>
</div> </div>