Merge branch 'main' into loans2
This commit is contained in:
commit
ef7763eb63
|
@ -14,19 +14,21 @@ export type Bet = {
|
|||
probBefore: number
|
||||
probAfter: number
|
||||
|
||||
sale?: {
|
||||
amount: number // amount user makes from sale
|
||||
betId: string // id of bet being sold
|
||||
// TODO: add sale time?
|
||||
}
|
||||
|
||||
fees: Fees
|
||||
|
||||
isSold?: boolean // true if this BUY bet has been sold
|
||||
isAnte?: boolean
|
||||
isLiquidityProvision?: boolean
|
||||
isRedemption?: boolean
|
||||
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>
|
||||
|
||||
export type NumericBet = Bet & {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { maxBy } from 'lodash'
|
||||
import { maxBy, sortBy, sum, sumBy } from 'lodash'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import {
|
||||
calculateCpmmSale,
|
||||
|
@ -133,10 +133,46 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
|
|||
: 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[]) {
|
||||
const { resolution } = contract
|
||||
const isCpmm = contract.mechanism === 'cpmm-1'
|
||||
|
||||
let currentInvested = 0
|
||||
let totalInvested = 0
|
||||
let payout = 0
|
||||
let loan = 0
|
||||
|
@ -162,7 +198,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
saleValue -= amount
|
||||
}
|
||||
|
||||
currentInvested += amount
|
||||
loan += loanAmount ?? 0
|
||||
payout += resolution
|
||||
? calculatePayout(contract, bet, resolution)
|
||||
|
@ -174,12 +209,13 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
const profit = payout + saleValue + redeemed - totalInvested
|
||||
const profitPercent = (profit / totalInvested) * 100
|
||||
|
||||
const invested = isCpmm ? getCpmmInvested(yourBets) : getDpmInvested(yourBets)
|
||||
const hasShares = Object.values(totalShares).some(
|
||||
(shares) => !floatingEqual(shares, 0)
|
||||
)
|
||||
|
||||
return {
|
||||
invested: Math.max(0, currentInvested),
|
||||
invested,
|
||||
payout,
|
||||
netPayout,
|
||||
profit,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
||||
import { difference, mapValues, groupBy, sumBy } from 'lodash'
|
||||
|
||||
import {
|
||||
Contract,
|
||||
|
@ -22,6 +22,8 @@ import { isManifoldId } from '../../common/envs/constants'
|
|||
import { removeUndefinedProps } from '../../common/util/object'
|
||||
import { LiquidityProvision } from '../../common/liquidity-provision'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { getContractBetMetrics } from '../../common/calculate'
|
||||
import { floatingEqual } from '../../common/util/math'
|
||||
|
||||
const bodySchema = z.object({
|
||||
contractId: z.string(),
|
||||
|
@ -162,7 +164,7 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
|
|||
const userPayoutsWithoutLoans = groupPayoutsByUser(payouts)
|
||||
|
||||
await sendResolutionEmails(
|
||||
openBets,
|
||||
bets,
|
||||
userPayoutsWithoutLoans,
|
||||
creator,
|
||||
creatorPayout,
|
||||
|
@ -188,7 +190,7 @@ const processPayouts = async (payouts: Payout[], isDeposit = false) => {
|
|||
}
|
||||
|
||||
const sendResolutionEmails = async (
|
||||
openBets: Bet[],
|
||||
bets: Bet[],
|
||||
userPayouts: { [userId: string]: number },
|
||||
creator: User,
|
||||
creatorPayout: number,
|
||||
|
@ -197,14 +199,15 @@ const sendResolutionEmails = async (
|
|||
resolutionProbability?: number,
|
||||
resolutions?: { [outcome: string]: number }
|
||||
) => {
|
||||
const nonWinners = difference(
|
||||
uniq(openBets.map(({ userId }) => userId)),
|
||||
Object.keys(userPayouts)
|
||||
)
|
||||
const investedByUser = mapValues(
|
||||
groupBy(openBets, (bet) => bet.userId),
|
||||
(bets) => sumBy(bets, (bet) => bet.amount)
|
||||
groupBy(bets, (bet) => bet.userId),
|
||||
(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 = [
|
||||
...Object.entries(userPayouts),
|
||||
...nonWinners.map((userId) => [userId, 0] as const),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Router from 'next/router'
|
||||
import clsx from 'clsx'
|
||||
import { MouseEvent } from 'react'
|
||||
import { MouseEvent, useState } from 'react'
|
||||
import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid'
|
||||
|
||||
export function Avatar(props: {
|
||||
|
@ -10,7 +10,8 @@ export function Avatar(props: {
|
|||
size?: number | 'xs' | 'sm'
|
||||
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 onClick =
|
||||
|
@ -35,6 +36,11 @@ export function Avatar(props: {
|
|||
src={avatarUrl}
|
||||
onClick={onClick}
|
||||
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
|
||||
|
|
|
@ -428,95 +428,105 @@ export function BetsSummary(props: {
|
|||
: 'NO'
|
||||
: 'YES'
|
||||
|
||||
const canSell =
|
||||
isYourBets &&
|
||||
isCpmm &&
|
||||
(isBinary || isPseudoNumeric) &&
|
||||
!isClosed &&
|
||||
!resolution &&
|
||||
hasShares &&
|
||||
sharesOutcome &&
|
||||
user
|
||||
|
||||
return (
|
||||
<Row className={clsx('flex-wrap gap-4 sm:flex-nowrap sm:gap-6', className)}>
|
||||
{!isCpmm && (
|
||||
<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>
|
||||
)}
|
||||
{resolution ? (
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500">Payout</div>
|
||||
<div className="whitespace-nowrap text-sm text-gray-500">Profit</div>
|
||||
<div className="whitespace-nowrap">
|
||||
{formatMoney(payout)} <ProfitBadge profitPercent={profitPercent} />
|
||||
{formatMoney(profit)} <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 &&
|
||||
(isBinary || isPseudoNumeric) &&
|
||||
!isClosed &&
|
||||
!resolution &&
|
||||
hasShares &&
|
||||
sharesOutcome &&
|
||||
user && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm ml-2"
|
||||
onClick={() => setShowSellModal(true)}
|
||||
>
|
||||
Sell
|
||||
</button>
|
||||
{showSellModal && (
|
||||
<SellSharesModal
|
||||
contract={contract}
|
||||
user={user}
|
||||
userBets={bets}
|
||||
shares={totalShares[sharesOutcome]}
|
||||
sharesOutcome={sharesOutcome}
|
||||
setOpen={setShowSellModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{canSell && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm self-end"
|
||||
onClick={() => setShowSellModal(true)}
|
||||
>
|
||||
Sell
|
||||
</button>
|
||||
{showSellModal && (
|
||||
<SellSharesModal
|
||||
contract={contract}
|
||||
user={user}
|
||||
userBets={bets}
|
||||
shares={totalShares[sharesOutcome]}
|
||||
sharesOutcome={sharesOutcome}
|
||||
setOpen={setShowSellModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
</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>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ export function ContractSearch(props: {
|
|||
highlightOptions?: ContractHighlightOptions
|
||||
onContractClick?: (contract: Contract) => void
|
||||
hideOrderSelector?: boolean
|
||||
overrideGridClassName?: string
|
||||
gridClassName?: string
|
||||
cardHideOptions?: {
|
||||
hideGroupLink?: boolean
|
||||
hideQuickBet?: boolean
|
||||
|
@ -98,7 +98,7 @@ export function ContractSearch(props: {
|
|||
defaultFilter,
|
||||
additionalFilter,
|
||||
onContractClick,
|
||||
overrideGridClassName,
|
||||
gridClassName,
|
||||
hideOrderSelector,
|
||||
cardHideOptions,
|
||||
highlightOptions,
|
||||
|
@ -181,7 +181,7 @@ export function ContractSearch(props: {
|
|||
loadMore={performQuery}
|
||||
showTime={showTime}
|
||||
onContractClick={onContractClick}
|
||||
overrideGridClassName={overrideGridClassName}
|
||||
gridClassName={gridClassName}
|
||||
highlightOptions={highlightOptions}
|
||||
cardHideOptions={cardHideOptions}
|
||||
/>
|
||||
|
|
|
@ -20,7 +20,7 @@ export function ContractsGrid(props: {
|
|||
loadMore?: () => void
|
||||
showTime?: ShowTime
|
||||
onContractClick?: (contract: Contract) => void
|
||||
overrideGridClassName?: string
|
||||
gridClassName?: string
|
||||
cardHideOptions?: {
|
||||
hideQuickBet?: boolean
|
||||
hideGroupLink?: boolean
|
||||
|
@ -32,7 +32,7 @@ export function ContractsGrid(props: {
|
|||
showTime,
|
||||
loadMore,
|
||||
onContractClick,
|
||||
overrideGridClassName,
|
||||
gridClassName,
|
||||
cardHideOptions,
|
||||
highlightOptions,
|
||||
} = props
|
||||
|
@ -66,9 +66,8 @@ export function ContractsGrid(props: {
|
|||
<Col className="gap-8">
|
||||
<ul
|
||||
className={clsx(
|
||||
overrideGridClassName
|
||||
? overrideGridClassName
|
||||
: 'grid w-full grid-cols-1 gap-4 md:grid-cols-2'
|
||||
'w-full columns-1 gap-4 space-y-4 md:columns-2',
|
||||
gridClassName
|
||||
)}
|
||||
>
|
||||
{contracts.map((contract) => (
|
||||
|
@ -81,11 +80,10 @@ export function ContractsGrid(props: {
|
|||
}
|
||||
hideQuickBet={hideQuickBet}
|
||||
hideGroupLink={hideGroupLink}
|
||||
className={
|
||||
contractIds?.includes(contract.id)
|
||||
? highlightClassName
|
||||
: undefined
|
||||
}
|
||||
className={clsx(
|
||||
'break-inside-avoid-column',
|
||||
contractIds?.includes(contract.id) && highlightClassName
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
@ -65,9 +65,7 @@ export function MarketModal(props: {
|
|||
<ContractSearch
|
||||
hideOrderSelector
|
||||
onContractClick={addContract}
|
||||
overrideGridClassName={
|
||||
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
|
||||
}
|
||||
gridClassName="gap-3 space-y-3"
|
||||
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||
highlightOptions={{
|
||||
contractIds: contracts.map((c) => c.id),
|
||||
|
|
|
@ -266,12 +266,16 @@ export function listenForHotContracts(
|
|||
})
|
||||
}
|
||||
|
||||
export async function getHotContracts() {
|
||||
const data = await getValues<Contract>(hotContractsQuery)
|
||||
return sortBy(
|
||||
chooseRandomSubset(data, 10),
|
||||
(contract) => -1 * contract.volume24Hours
|
||||
)
|
||||
const trendingContractsQuery = query(
|
||||
contracts,
|
||||
where('isResolved', '==', false),
|
||||
where('visibility', '==', 'public'),
|
||||
orderBy('popularityScore', 'desc'),
|
||||
limit(10)
|
||||
)
|
||||
|
||||
export async function getTrendingContracts() {
|
||||
return await getValues<Contract>(trendingContractsQuery)
|
||||
}
|
||||
|
||||
export async function getContractsBySlugs(slugs: string[]) {
|
||||
|
|
|
@ -607,9 +607,7 @@ function AddContractButton(props: { group: Group; user: User }) {
|
|||
user={user}
|
||||
hideOrderSelector={true}
|
||||
onContractClick={addContractToCurrentGroup}
|
||||
overrideGridClassName={
|
||||
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
|
||||
}
|
||||
gridClassName="gap-3 space-y-3"
|
||||
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
|
||||
additionalFilter={{ excludeContractIds: group.contractIds }}
|
||||
highlightOptions={{
|
||||
|
|
|
@ -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 { LandingPagePanel } from 'web/components/landing-page-panel'
|
||||
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'
|
||||
|
||||
export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
|
||||
// These hardcoded markets will be shown in the frontpage for signed-out users:
|
||||
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',
|
||||
])
|
||||
const hotContracts = await getTrendingContracts()
|
||||
return { props: { hotContracts } }
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user