Merge branch 'main' into loans2

This commit is contained in:
James Grugett 2022-08-15 22:07:52 -05:00
commit ef7763eb63
11 changed files with 181 additions and 138 deletions

View File

@ -14,19 +14,21 @@ export type Bet = {
probBefore: number probBefore: number
probAfter: number probAfter: number
sale?: {
amount: number // amount user makes from sale
betId: string // id of bet being sold
// TODO: add sale time?
}
fees: Fees fees: Fees
isSold?: boolean // true if this BUY bet has been sold
isAnte?: boolean isAnte?: boolean
isLiquidityProvision?: boolean isLiquidityProvision?: boolean
isRedemption?: boolean isRedemption?: boolean
challengeSlug?: string challengeSlug?: string
// Props for bets in DPM contract below.
// A bet is either a BUY or a SELL that sells all of a previous buy.
isSold?: boolean // true if this BUY bet has been sold
// This field marks a SELL bet.
sale?: {
amount: number // amount user makes from sale
betId: string // id of BUY bet being sold
}
} & Partial<LimitProps> } & Partial<LimitProps>
export type NumericBet = Bet & { export type NumericBet = Bet & {

View File

@ -1,4 +1,4 @@
import { maxBy } from 'lodash' import { maxBy, sortBy, sum, sumBy } from 'lodash'
import { Bet, LimitBet } from './bet' import { Bet, LimitBet } from './bet'
import { import {
calculateCpmmSale, calculateCpmmSale,
@ -133,10 +133,46 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
: calculateDpmPayout(contract, bet, outcome) : calculateDpmPayout(contract, bet, outcome)
} }
function getCpmmInvested(yourBets: Bet[]) {
const totalShares: { [outcome: string]: number } = {}
const totalSpent: { [outcome: string]: number } = {}
const sortedBets = sortBy(yourBets, 'createdTime')
for (const bet of sortedBets) {
const { outcome, shares, amount } = bet
if (amount > 0) {
totalShares[outcome] = (totalShares[outcome] ?? 0) + shares
totalSpent[outcome] = (totalSpent[outcome] ?? 0) + amount
} else if (amount < 0) {
const averagePrice = totalSpent[outcome] / totalShares[outcome]
totalShares[outcome] = totalShares[outcome] + shares
totalSpent[outcome] = totalSpent[outcome] + averagePrice * shares
}
}
return sum(Object.values(totalSpent))
}
function getDpmInvested(yourBets: Bet[]) {
const sortedBets = sortBy(yourBets, 'createdTime')
return sumBy(sortedBets, (bet) => {
const { amount, sale } = bet
if (sale) {
const originalBet = sortedBets.find((b) => b.id === sale.betId)
if (originalBet) return -originalBet.amount
return 0
}
return amount
})
}
export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) { export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
const { resolution } = contract const { resolution } = contract
const isCpmm = contract.mechanism === 'cpmm-1'
let currentInvested = 0
let totalInvested = 0 let totalInvested = 0
let payout = 0 let payout = 0
let loan = 0 let loan = 0
@ -162,7 +198,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
saleValue -= amount saleValue -= amount
} }
currentInvested += amount
loan += loanAmount ?? 0 loan += loanAmount ?? 0
payout += resolution payout += resolution
? calculatePayout(contract, bet, resolution) ? calculatePayout(contract, bet, resolution)
@ -174,12 +209,13 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
const profit = payout + saleValue + redeemed - totalInvested const profit = payout + saleValue + redeemed - totalInvested
const profitPercent = (profit / totalInvested) * 100 const profitPercent = (profit / totalInvested) * 100
const invested = isCpmm ? getCpmmInvested(yourBets) : getDpmInvested(yourBets)
const hasShares = Object.values(totalShares).some( const hasShares = Object.values(totalShares).some(
(shares) => !floatingEqual(shares, 0) (shares) => !floatingEqual(shares, 0)
) )
return { return {
invested: Math.max(0, currentInvested), invested,
payout, payout,
netPayout, netPayout,
profit, profit,

View File

@ -1,6 +1,6 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod' import { z } from 'zod'
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash' import { difference, mapValues, groupBy, sumBy } from 'lodash'
import { import {
Contract, Contract,
@ -22,6 +22,8 @@ import { isManifoldId } from '../../common/envs/constants'
import { removeUndefinedProps } from '../../common/util/object' import { removeUndefinedProps } from '../../common/util/object'
import { LiquidityProvision } from '../../common/liquidity-provision' import { LiquidityProvision } from '../../common/liquidity-provision'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
import { getContractBetMetrics } from '../../common/calculate'
import { floatingEqual } from '../../common/util/math'
const bodySchema = z.object({ const bodySchema = z.object({
contractId: z.string(), contractId: z.string(),
@ -162,7 +164,7 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
const userPayoutsWithoutLoans = groupPayoutsByUser(payouts) const userPayoutsWithoutLoans = groupPayoutsByUser(payouts)
await sendResolutionEmails( await sendResolutionEmails(
openBets, bets,
userPayoutsWithoutLoans, userPayoutsWithoutLoans,
creator, creator,
creatorPayout, creatorPayout,
@ -188,7 +190,7 @@ const processPayouts = async (payouts: Payout[], isDeposit = false) => {
} }
const sendResolutionEmails = async ( const sendResolutionEmails = async (
openBets: Bet[], bets: Bet[],
userPayouts: { [userId: string]: number }, userPayouts: { [userId: string]: number },
creator: User, creator: User,
creatorPayout: number, creatorPayout: number,
@ -197,14 +199,15 @@ const sendResolutionEmails = async (
resolutionProbability?: number, resolutionProbability?: number,
resolutions?: { [outcome: string]: number } resolutions?: { [outcome: string]: number }
) => { ) => {
const nonWinners = difference(
uniq(openBets.map(({ userId }) => userId)),
Object.keys(userPayouts)
)
const investedByUser = mapValues( const investedByUser = mapValues(
groupBy(openBets, (bet) => bet.userId), groupBy(bets, (bet) => bet.userId),
(bets) => sumBy(bets, (bet) => bet.amount) (bets) => getContractBetMetrics(contract, bets).invested
) )
const investedUsers = Object.keys(investedByUser).filter(
(userId) => !floatingEqual(investedByUser[userId], 0)
)
const nonWinners = difference(investedUsers, Object.keys(userPayouts))
const emailPayouts = [ const emailPayouts = [
...Object.entries(userPayouts), ...Object.entries(userPayouts),
...nonWinners.map((userId) => [userId, 0] as const), ...nonWinners.map((userId) => [userId, 0] as const),

View File

@ -1,6 +1,6 @@
import Router from 'next/router' import Router from 'next/router'
import clsx from 'clsx' import clsx from 'clsx'
import { MouseEvent } from 'react' import { MouseEvent, useState } from 'react'
import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid' import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid'
export function Avatar(props: { export function Avatar(props: {
@ -10,7 +10,8 @@ export function Avatar(props: {
size?: number | 'xs' | 'sm' size?: number | 'xs' | 'sm'
className?: string className?: string
}) { }) {
const { username, avatarUrl, noLink, size, className } = props const { username, noLink, size, className } = props
const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl)
const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10 const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10
const onClick = const onClick =
@ -35,6 +36,11 @@ export function Avatar(props: {
src={avatarUrl} src={avatarUrl}
onClick={onClick} onClick={onClick}
alt={username} alt={username}
onError={() => {
// If the image doesn't load, clear the avatarUrl to show the default
// Mostly for localhost, when getting a 403 from googleusercontent
setAvatarUrl('')
}}
/> />
) : ( ) : (
<UserCircleIcon <UserCircleIcon

View File

@ -428,76 +428,35 @@ export function BetsSummary(props: {
: 'NO' : 'NO'
: 'YES' : 'YES'
return ( const canSell =
<Row className={clsx('flex-wrap gap-4 sm:flex-nowrap sm:gap-6', className)}> isYourBets &&
{!isCpmm && (
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Invested
</div>
<div className="whitespace-nowrap">{formatMoney(invested)}</div>
</Col>
)}
{resolution ? (
<Col>
<div className="text-sm text-gray-500">Payout</div>
<div className="whitespace-nowrap">
{formatMoney(payout)} <ProfitBadge profitPercent={profitPercent} />
</div>
</Col>
) : isBinary ? (
<>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <YesLabel />
</div>
<div className="whitespace-nowrap">{formatMoney(yesWinnings)}</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <NoLabel />
</div>
<div className="whitespace-nowrap">{formatMoney(noWinnings)}</div>
</Col>
</>
) : isPseudoNumeric ? (
<>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if {'>='} {formatLargeNumber(contract.max)}
</div>
<div className="whitespace-nowrap">{formatMoney(yesWinnings)}</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if {'<='} {formatLargeNumber(contract.min)}
</div>
<div className="whitespace-nowrap">{formatMoney(noWinnings)}</div>
</Col>
</>
) : (
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Current value
</div>
<div className="whitespace-nowrap">{formatMoney(payout)}</div>
</Col>
)}
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">Profit</div>
<div className="whitespace-nowrap">
{formatMoney(profit)} <ProfitBadge profitPercent={profitPercent} />
{isYourBets &&
isCpmm && isCpmm &&
(isBinary || isPseudoNumeric) && (isBinary || isPseudoNumeric) &&
!isClosed && !isClosed &&
!resolution && !resolution &&
hasShares && hasShares &&
sharesOutcome && sharesOutcome &&
user && ( user
return (
<Col className={clsx(className, 'gap-4')}>
<Row className="flex-wrap gap-4 sm:flex-nowrap sm:gap-6">
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Invested
</div>
<div className="whitespace-nowrap">{formatMoney(invested)}</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">Profit</div>
<div className="whitespace-nowrap">
{formatMoney(profit)} <ProfitBadge profitPercent={profitPercent} />
</div>
</Col>
{canSell && (
<> <>
<button <button
className="btn btn-sm ml-2" className="btn btn-sm self-end"
onClick={() => setShowSellModal(true)} onClick={() => setShowSellModal(true)}
> >
Sell Sell
@ -514,9 +473,60 @@ export function BetsSummary(props: {
)} )}
</> </>
)} )}
</Row>
<Row className="flex-wrap-none gap-4">
{resolution ? (
<Col>
<div className="text-sm text-gray-500">Payout</div>
<div className="whitespace-nowrap">
{formatMoney(payout)}{' '}
<ProfitBadge profitPercent={profitPercent} />
</div> </div>
</Col> </Col>
) : isBinary ? (
<>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <YesLabel />
</div>
<div className="whitespace-nowrap">
{formatMoney(yesWinnings)}
</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <NoLabel />
</div>
<div className="whitespace-nowrap">{formatMoney(noWinnings)}</div>
</Col>
</>
) : isPseudoNumeric ? (
<>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if {'>='} {formatLargeNumber(contract.max)}
</div>
<div className="whitespace-nowrap">
{formatMoney(yesWinnings)}
</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if {'<='} {formatLargeNumber(contract.min)}
</div>
<div className="whitespace-nowrap">{formatMoney(noWinnings)}</div>
</Col>
</>
) : (
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Current value
</div>
<div className="whitespace-nowrap">{formatMoney(payout)}</div>
</Col>
)}
</Row> </Row>
</Col>
) )
} }

View File

@ -83,7 +83,7 @@ export function ContractSearch(props: {
highlightOptions?: ContractHighlightOptions highlightOptions?: ContractHighlightOptions
onContractClick?: (contract: Contract) => void onContractClick?: (contract: Contract) => void
hideOrderSelector?: boolean hideOrderSelector?: boolean
overrideGridClassName?: string gridClassName?: string
cardHideOptions?: { cardHideOptions?: {
hideGroupLink?: boolean hideGroupLink?: boolean
hideQuickBet?: boolean hideQuickBet?: boolean
@ -98,7 +98,7 @@ export function ContractSearch(props: {
defaultFilter, defaultFilter,
additionalFilter, additionalFilter,
onContractClick, onContractClick,
overrideGridClassName, gridClassName,
hideOrderSelector, hideOrderSelector,
cardHideOptions, cardHideOptions,
highlightOptions, highlightOptions,
@ -181,7 +181,7 @@ export function ContractSearch(props: {
loadMore={performQuery} loadMore={performQuery}
showTime={showTime} showTime={showTime}
onContractClick={onContractClick} onContractClick={onContractClick}
overrideGridClassName={overrideGridClassName} gridClassName={gridClassName}
highlightOptions={highlightOptions} highlightOptions={highlightOptions}
cardHideOptions={cardHideOptions} cardHideOptions={cardHideOptions}
/> />

View File

@ -20,7 +20,7 @@ export function ContractsGrid(props: {
loadMore?: () => void loadMore?: () => void
showTime?: ShowTime showTime?: ShowTime
onContractClick?: (contract: Contract) => void onContractClick?: (contract: Contract) => void
overrideGridClassName?: string gridClassName?: string
cardHideOptions?: { cardHideOptions?: {
hideQuickBet?: boolean hideQuickBet?: boolean
hideGroupLink?: boolean hideGroupLink?: boolean
@ -32,7 +32,7 @@ export function ContractsGrid(props: {
showTime, showTime,
loadMore, loadMore,
onContractClick, onContractClick,
overrideGridClassName, gridClassName,
cardHideOptions, cardHideOptions,
highlightOptions, highlightOptions,
} = props } = props
@ -66,9 +66,8 @@ export function ContractsGrid(props: {
<Col className="gap-8"> <Col className="gap-8">
<ul <ul
className={clsx( className={clsx(
overrideGridClassName 'w-full columns-1 gap-4 space-y-4 md:columns-2',
? overrideGridClassName gridClassName
: 'grid w-full grid-cols-1 gap-4 md:grid-cols-2'
)} )}
> >
{contracts.map((contract) => ( {contracts.map((contract) => (
@ -81,11 +80,10 @@ export function ContractsGrid(props: {
} }
hideQuickBet={hideQuickBet} hideQuickBet={hideQuickBet}
hideGroupLink={hideGroupLink} hideGroupLink={hideGroupLink}
className={ className={clsx(
contractIds?.includes(contract.id) 'break-inside-avoid-column',
? highlightClassName contractIds?.includes(contract.id) && highlightClassName
: undefined )}
}
/> />
))} ))}
</ul> </ul>

View File

@ -65,9 +65,7 @@ export function MarketModal(props: {
<ContractSearch <ContractSearch
hideOrderSelector hideOrderSelector
onContractClick={addContract} onContractClick={addContract}
overrideGridClassName={ gridClassName="gap-3 space-y-3"
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
}
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
highlightOptions={{ highlightOptions={{
contractIds: contracts.map((c) => c.id), contractIds: contracts.map((c) => c.id),

View File

@ -266,12 +266,16 @@ export function listenForHotContracts(
}) })
} }
export async function getHotContracts() { const trendingContractsQuery = query(
const data = await getValues<Contract>(hotContractsQuery) contracts,
return sortBy( where('isResolved', '==', false),
chooseRandomSubset(data, 10), where('visibility', '==', 'public'),
(contract) => -1 * contract.volume24Hours orderBy('popularityScore', 'desc'),
limit(10)
) )
export async function getTrendingContracts() {
return await getValues<Contract>(trendingContractsQuery)
} }
export async function getContractsBySlugs(slugs: string[]) { export async function getContractsBySlugs(slugs: string[]) {

View File

@ -607,9 +607,7 @@ function AddContractButton(props: { group: Group; user: User }) {
user={user} user={user}
hideOrderSelector={true} hideOrderSelector={true}
onContractClick={addContractToCurrentGroup} onContractClick={addContractToCurrentGroup}
overrideGridClassName={ gridClassName="gap-3 space-y-3"
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
}
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
additionalFilter={{ excludeContractIds: group.contractIds }} additionalFilter={{ excludeContractIds: group.contractIds }}
highlightOptions={{ highlightOptions={{

View File

@ -1,4 +1,4 @@
import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts' import { Contract, getTrendingContracts } from 'web/lib/firebase/contracts'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { LandingPagePanel } from 'web/components/landing-page-panel' import { LandingPagePanel } from 'web/components/landing-page-panel'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
@ -8,19 +8,7 @@ import { useSaveReferral } from 'web/hooks/use-save-referral'
import { SEO } from 'web/components/SEO' import { SEO } from 'web/components/SEO'
export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => { export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
// These hardcoded markets will be shown in the frontpage for signed-out users: const hotContracts = await getTrendingContracts()
const hotContracts = await getContractsBySlugs([
'will-max-go-to-prom-with-a-girl',
'will-ethereum-switch-to-proof-of-st',
'will-russia-control-the-majority-of',
'will-elon-musk-buy-twitter-this-yea',
'will-trump-be-charged-by-the-grand',
'will-spacex-launch-a-starship-into',
'who-will-win-the-nba-finals-champio',
'who-will-be-time-magazine-person-of',
'will-congress-hold-any-hearings-abo-e21f987033b3',
'will-at-least-10-world-cities-have',
])
return { props: { hotContracts } } return { props: { hotContracts } }
}) })