From 34e8138e5096e1ad8a4944566328703bd080c645 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Mon, 15 Aug 2022 16:33:02 -0700 Subject: [PATCH 1/8] Show placeholder when avatarUrl errors --- web/components/avatar.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx index 19b6066e..6ca06cbb 100644 --- a/web/components/avatar.tsx +++ b/web/components/avatar.tsx @@ -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('') + }} /> ) : ( Date: Mon, 15 Aug 2022 19:04:35 -0500 Subject: [PATCH 2/8] Compute invested & display in your bets --- common/calculate.ts | 27 +++++- web/components/bets-list.tsx | 164 +++++++++++++++++++---------------- 2 files changed, 112 insertions(+), 79 deletions(-) diff --git a/common/calculate.ts b/common/calculate.ts index d25fd313..dd5b590c 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -1,4 +1,4 @@ -import { maxBy } from 'lodash' +import { maxBy, sortBy, sum } from 'lodash' import { Bet, LimitBet } from './bet' import { calculateCpmmSale, @@ -133,8 +133,29 @@ 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)) +} + export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) { const { resolution } = contract + const isCpmm = contract.mechanism === 'cpmm-1' let currentInvested = 0 let totalInvested = 0 @@ -178,8 +199,10 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) { (shares) => !floatingEqual(shares, 0) ) + const invested = isCpmm ? getCpmmInvested(yourBets) : currentInvested + return { - invested: Math.max(0, currentInvested), + invested, payout, netPayout, profit, diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index b919cccd..e8d85dba 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -428,95 +428,105 @@ export function BetsSummary(props: { : 'NO' : 'YES' + const canSell = + isYourBets && + isCpmm && + (isBinary || isPseudoNumeric) && + !isClosed && + !resolution && + hasShares && + sharesOutcome && + user + return ( - - {!isCpmm && ( + +
Invested
{formatMoney(invested)}
- )} - {resolution ? ( -
Payout
+
Profit
- {formatMoney(payout)} + {formatMoney(profit)}
- ) : isBinary ? ( - <> - -
- Payout if -
-
{formatMoney(yesWinnings)}
- - -
- Payout if -
-
{formatMoney(noWinnings)}
- - - ) : isPseudoNumeric ? ( - <> - -
- Payout if {'>='} {formatLargeNumber(contract.max)} -
-
{formatMoney(yesWinnings)}
- - -
- Payout if {'<='} {formatLargeNumber(contract.min)} -
-
{formatMoney(noWinnings)}
- - - ) : ( - -
- Current value -
-
{formatMoney(payout)}
- - )} - -
Profit
-
- {formatMoney(profit)} - {isYourBets && - isCpmm && - (isBinary || isPseudoNumeric) && - !isClosed && - !resolution && - hasShares && - sharesOutcome && - user && ( - <> - - {showSellModal && ( - - )} - + {canSell && ( + <> + + {showSellModal && ( + )} -
- -
+ + )} +
+ + {resolution ? ( + +
Payout
+
+ {formatMoney(payout)}{' '} + +
+ + ) : isBinary ? ( + <> + +
+ Payout if +
+
+ {formatMoney(yesWinnings)} +
+ + +
+ Payout if +
+
{formatMoney(noWinnings)}
+ + + ) : isPseudoNumeric ? ( + <> + +
+ Payout if {'>='} {formatLargeNumber(contract.max)} +
+
+ {formatMoney(yesWinnings)} +
+ + +
+ Payout if {'<='} {formatLargeNumber(contract.min)} +
+
{formatMoney(noWinnings)}
+ + + ) : ( + +
+ Current value +
+
{formatMoney(payout)}
+ + )} +
+ ) } From 4002c23beea064f58cbedca007e11a581625e37d Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Mon, 15 Aug 2022 17:41:53 -0700 Subject: [PATCH 3/8] Tile contract cards in masonry layout (#761) --- web/components/contract-search.tsx | 6 +++--- web/components/contract/contracts-grid.tsx | 18 ++++++++---------- web/components/editor/market-modal.tsx | 4 +--- web/pages/group/[...slugs]/index.tsx | 4 +--- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 11d65a13..41360eb7 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -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} /> diff --git a/web/components/contract/contracts-grid.tsx b/web/components/contract/contracts-grid.tsx index 05c66d56..915facd9 100644 --- a/web/components/contract/contracts-grid.tsx +++ b/web/components/contract/contracts-grid.tsx @@ -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: {
    {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 + )} /> ))}
diff --git a/web/components/editor/market-modal.tsx b/web/components/editor/market-modal.tsx index 85b7a978..02925ff3 100644 --- a/web/components/editor/market-modal.tsx +++ b/web/components/editor/market-modal.tsx @@ -65,9 +65,7 @@ export function MarketModal(props: { c.id), diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index c5255974..c66d5aa5 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -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={{ From d56435b9cd78affac37b0a516039f95e952e6759 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 15 Aug 2022 19:34:45 -0600 Subject: [PATCH 4/8] Signed out home page shows dynamic trending markets --- web/lib/firebase/contracts.ts | 16 ++++++++++------ web/pages/index.tsx | 20 ++++++-------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index 3a751c18..b31b8d04 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -266,12 +266,16 @@ export function listenForHotContracts( }) } -export async function getHotContracts() { - const data = await getValues(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(trendingContractsQuery) } export async function getContractsBySlugs(slugs: string[]) { diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 473189aa..d4ae5f47 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,4 +1,8 @@ -import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts' +import { + Contract, + getContractsBySlugs, + 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 +12,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 } } }) From cd520e6cfe8474a6b5ac33b4ca730c5b7911775b Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 15 Aug 2022 19:47:58 -0600 Subject: [PATCH 5/8] lint --- web/pages/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/web/pages/index.tsx b/web/pages/index.tsx index d4ae5f47..01c24fcf 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,8 +1,4 @@ -import { - Contract, - getContractsBySlugs, - getTrendingContracts, -} 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' From aef14e49bbfd2b6c503b6e89f8026766751d9de1 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 15 Aug 2022 21:32:53 -0500 Subject: [PATCH 6/8] Update bet type to explain dpm props --- common/bet.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/common/bet.ts b/common/bet.ts index 56e050a7..3d9d6a5a 100644 --- a/common/bet.ts +++ b/common/bet.ts @@ -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 export type NumericBet = Bet & { From e5aef763cd05634ff8df9a4dfb855bb0e26aa67b Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 15 Aug 2022 21:33:29 -0500 Subject: [PATCH 7/8] Calculate invested properly for DPM --- common/calculate.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/common/calculate.ts b/common/calculate.ts index dd5b590c..758fc3cd 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -1,4 +1,4 @@ -import { maxBy, sortBy, sum } from 'lodash' +import { maxBy, sortBy, sum, sumBy } from 'lodash' import { Bet, LimitBet } from './bet' import { calculateCpmmSale, @@ -153,11 +153,26 @@ function getCpmmInvested(yourBets: Bet[]) { 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 @@ -183,7 +198,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) { saleValue -= amount } - currentInvested += amount loan += loanAmount ?? 0 payout += resolution ? calculatePayout(contract, bet, resolution) @@ -195,12 +209,11 @@ 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) ) - const invested = isCpmm ? getCpmmInvested(yourBets) : currentInvested - return { invested, payout, From f2f77cb51e957b93441d3ade5f9015ca3838fdae Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 15 Aug 2022 21:48:00 -0500 Subject: [PATCH 8/8] Resolve market emails: fix negative amount bug with better invested calculation --- functions/src/resolve-market.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 7277f40b..6f8ea2e9 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -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),