Free response (#47)
* Answer datatype and MULTI outcome type for Contract
* Create free answer contract
* Automatically sort Tailwind classes with Prettier (#45)
* Add Prettier Tailwind plugin
* Autoformat Tailwind classes with Prettier
* Allow for non-binary contracts in contract page and related components
* logo with white inside, transparent bg
* Create answer
* Some UI for showing answers
* Answer bet panel
* Convert rest of calcuate file to generic multi contracts
* Working betting with ante'd NONE answer
* Numbered answers. Layout & calculation tweaks
* Can bet. More layout tweaks!
* Resolve answer UI
* Resolve multi market
* Resolved market UI
* Fix feed and cards for multi contracts
* Sell bets. Various fixes
* Tweaks for trades page
* Always dev mode
* Create answer bet has isAnte: true
* Fix card showing 0% for multi contracts
* Fix grouped bets feed for multi outcomes
* None option converted to none of the above label at bottom of list. Button to resolve none.
* Tweaks to no answers yet, resolve button layout
* Show ante bets on new answers in the feed
* Update placeholder text for description
* Consolidate firestore rules for subcollections
* Remove Contract and Bet type params. Use string type for outcomes.
* Increase char limit to 10k for answers. Preserve line breaks.
* Don't show resolve options after answer chosen
* Fix type error in script
* Remove NONE resolution option
* Change outcomeType to include 'MULTI' and 'FREE_RESPONSE'
* Show bet probability change and payout when creating answer
* User info change: also change answers
* Append answers to contract field 'answers'
* sort trades by resolved
* Don't include trailing !:,.; in links
* Stop flooring inputs into formatMoney
* Revert "Stop flooring inputs into formatMoney"
This reverts commit 2f7ab18429
.
* Consistently floor user.balance
* Expand create panel on focus
From Richard Hanania's feedback
* welcome email: include link to manifold
* Fix home page in dev on branches that are not free-response
* Close emails (#50)
* script init for stephen dev
* market close emails
* order of operations
* template email
* sendMarketCloseEmail: handle unsubscribe
* remove debugging
* marketCloseEmails: every hour
* sendMarketCloseEmails: check undefined
* marketCloseEmails: "every hour" => "every 1 hours"
* Set up a read API using Vercel serverless functions (#49)
* Set up read API using Vercel serverless functions
Featuring:
/api/v0/markets
/api/v0/market/[contractId]
/api/v0/slug/[contractSlug]
* Include tags in API
* Tweaks. Remove filter for only binary contract
* Fix bet probability change for NO bets
* Put back isProd calculation
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: mantikoros <95266179+mantikoros@users.noreply.github.com>
This commit is contained in:
parent
d3fdd4cd1f
commit
b2501d8145
31
common/answer.ts
Normal file
31
common/answer.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { User } from './user'
|
||||||
|
|
||||||
|
export type Answer = {
|
||||||
|
id: string
|
||||||
|
number: number
|
||||||
|
contractId: string
|
||||||
|
createdTime: number
|
||||||
|
|
||||||
|
userId: string
|
||||||
|
username: string
|
||||||
|
name: string
|
||||||
|
avatarUrl?: string
|
||||||
|
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNoneAnswer = (contractId: string, creator: User) => {
|
||||||
|
const { username, name, avatarUrl } = creator
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: '0',
|
||||||
|
number: 0,
|
||||||
|
contractId,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
userId: creator.id,
|
||||||
|
username,
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
text: 'None',
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,3 +61,30 @@ export function getAnteBets(
|
||||||
|
|
||||||
return { yesBet, noBet }
|
return { yesBet, noBet }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFreeAnswerAnte(
|
||||||
|
creator: User,
|
||||||
|
contract: Contract,
|
||||||
|
anteBetId: string
|
||||||
|
) {
|
||||||
|
const { totalBets, totalShares } = contract
|
||||||
|
const amount = totalBets['0']
|
||||||
|
const shares = totalShares['0']
|
||||||
|
|
||||||
|
const { createdTime } = contract
|
||||||
|
|
||||||
|
const anteBet: Bet = {
|
||||||
|
id: anteBetId,
|
||||||
|
userId: creator.id,
|
||||||
|
contractId: contract.id,
|
||||||
|
amount,
|
||||||
|
shares,
|
||||||
|
outcome: '0',
|
||||||
|
probBefore: 0,
|
||||||
|
probAfter: 1,
|
||||||
|
createdTime,
|
||||||
|
isAnte: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return anteBet
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ export type Bet = {
|
||||||
contractId: string
|
contractId: string
|
||||||
|
|
||||||
amount: number // bet size; negative if SELL bet
|
amount: number // bet size; negative if SELL bet
|
||||||
outcome: 'YES' | 'NO'
|
outcome: string
|
||||||
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
||||||
|
|
||||||
probBefore: number
|
probBefore: number
|
||||||
|
|
|
@ -1,69 +1,72 @@
|
||||||
|
import * as _ from 'lodash'
|
||||||
import { Bet } from './bet'
|
import { Bet } from './bet'
|
||||||
import { Contract } from './contract'
|
import { Contract } from './contract'
|
||||||
import { FEES } from './fees'
|
import { FEES } from './fees'
|
||||||
|
|
||||||
export function getProbability(totalShares: { YES: number; NO: number }) {
|
export function getProbability(totalShares: { [outcome: string]: number }) {
|
||||||
const { YES: y, NO: n } = totalShares
|
// For binary contracts only.
|
||||||
return y ** 2 / (y ** 2 + n ** 2)
|
return getOutcomeProbability(totalShares, 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutcomeProbability(
|
||||||
|
totalShares: {
|
||||||
|
[outcome: string]: number
|
||||||
|
},
|
||||||
|
outcome: string
|
||||||
|
) {
|
||||||
|
const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||||
|
const shares = totalShares[outcome] ?? 0
|
||||||
|
return shares ** 2 / squareSum
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProbabilityAfterBet(
|
export function getProbabilityAfterBet(
|
||||||
totalShares: { YES: number; NO: number },
|
totalShares: {
|
||||||
outcome: 'YES' | 'NO',
|
[outcome: string]: number
|
||||||
|
},
|
||||||
|
outcome: string,
|
||||||
bet: number
|
bet: number
|
||||||
) {
|
) {
|
||||||
const shares = calculateShares(totalShares, bet, outcome)
|
const shares = calculateShares(totalShares, bet, outcome)
|
||||||
|
|
||||||
const [YES, NO] =
|
const prevShares = totalShares[outcome] ?? 0
|
||||||
outcome === 'YES'
|
const newTotalShares = { ...totalShares, [outcome]: prevShares + shares }
|
||||||
? [totalShares.YES + shares, totalShares.NO]
|
|
||||||
: [totalShares.YES, totalShares.NO + shares]
|
|
||||||
|
|
||||||
return getProbability({ YES, NO })
|
return getOutcomeProbability(newTotalShares, outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateShares(
|
export function calculateShares(
|
||||||
totalShares: { YES: number; NO: number },
|
totalShares: {
|
||||||
|
[outcome: string]: number
|
||||||
|
},
|
||||||
bet: number,
|
bet: number,
|
||||||
betChoice: 'YES' | 'NO'
|
betChoice: string
|
||||||
) {
|
) {
|
||||||
const [yesShares, noShares] = [totalShares.YES, totalShares.NO]
|
const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||||
|
const shares = totalShares[betChoice] ?? 0
|
||||||
|
|
||||||
const c = 2 * bet * Math.sqrt(yesShares ** 2 + noShares ** 2)
|
const c = 2 * bet * Math.sqrt(squareSum)
|
||||||
|
|
||||||
return betChoice === 'YES'
|
return Math.sqrt(bet ** 2 + shares ** 2 + c) - shares
|
||||||
? Math.sqrt(bet ** 2 + yesShares ** 2 + c) - yesShares
|
|
||||||
: Math.sqrt(bet ** 2 + noShares ** 2 + c) - noShares
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateEstimatedWinnings(
|
|
||||||
totalShares: { YES: number; NO: number },
|
|
||||||
shares: number,
|
|
||||||
betChoice: 'YES' | 'NO'
|
|
||||||
) {
|
|
||||||
const ind = betChoice === 'YES' ? 1 : 0
|
|
||||||
|
|
||||||
const yesShares = totalShares.YES + ind * shares
|
|
||||||
const noShares = totalShares.NO + (1 - ind) * shares
|
|
||||||
|
|
||||||
const estPool = Math.sqrt(yesShares ** 2 + noShares ** 2)
|
|
||||||
const total = ind * yesShares + (1 - ind) * noShares
|
|
||||||
|
|
||||||
return ((1 - FEES) * (shares * estPool)) / total
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateRawShareValue(
|
export function calculateRawShareValue(
|
||||||
totalShares: { YES: number; NO: number },
|
totalShares: {
|
||||||
|
[outcome: string]: number
|
||||||
|
},
|
||||||
shares: number,
|
shares: number,
|
||||||
betChoice: 'YES' | 'NO'
|
betChoice: string
|
||||||
) {
|
) {
|
||||||
const [yesShares, noShares] = [totalShares.YES, totalShares.NO]
|
const currentValue = Math.sqrt(
|
||||||
const currentValue = Math.sqrt(yesShares ** 2 + noShares ** 2)
|
_.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||||
|
)
|
||||||
|
|
||||||
const postSaleValue =
|
const postSaleValue = Math.sqrt(
|
||||||
betChoice === 'YES'
|
_.sumBy(Object.keys(totalShares), (outcome) =>
|
||||||
? Math.sqrt(Math.max(0, yesShares - shares) ** 2 + noShares ** 2)
|
outcome === betChoice
|
||||||
: Math.sqrt(yesShares ** 2 + Math.max(0, noShares - shares) ** 2)
|
? Math.max(0, totalShares[outcome] - shares) ** 2
|
||||||
|
: totalShares[outcome] ** 2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return currentValue - postSaleValue
|
return currentValue - postSaleValue
|
||||||
}
|
}
|
||||||
|
@ -73,17 +76,22 @@ export function calculateMoneyRatio(
|
||||||
bet: Bet,
|
bet: Bet,
|
||||||
shareValue: number
|
shareValue: number
|
||||||
) {
|
) {
|
||||||
const { totalShares, pool } = contract
|
const { totalShares, totalBets, pool } = contract
|
||||||
|
const { outcome, amount } = bet
|
||||||
|
|
||||||
const p = getProbability(totalShares)
|
const p = getOutcomeProbability(totalShares, outcome)
|
||||||
|
|
||||||
const actual = pool.YES + pool.NO - shareValue
|
const actual = _.sum(Object.values(pool)) - shareValue
|
||||||
|
|
||||||
const betAmount =
|
const betAmount = p * amount
|
||||||
bet.outcome === 'YES' ? p * bet.amount : (1 - p) * bet.amount
|
|
||||||
|
|
||||||
const expected =
|
const expected =
|
||||||
p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO - betAmount
|
_.sumBy(
|
||||||
|
Object.keys(totalBets),
|
||||||
|
(outcome) =>
|
||||||
|
getOutcomeProbability(totalShares, outcome) *
|
||||||
|
(totalBets as { [outcome: string]: number })[outcome]
|
||||||
|
) - betAmount
|
||||||
|
|
||||||
if (actual <= 0 || expected <= 0) return 0
|
if (actual <= 0 || expected <= 0) return 0
|
||||||
|
|
||||||
|
@ -91,14 +99,13 @@ export function calculateMoneyRatio(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateShareValue(contract: Contract, bet: Bet) {
|
export function calculateShareValue(contract: Contract, bet: Bet) {
|
||||||
const shareValue = calculateRawShareValue(
|
const { pool, totalShares } = contract
|
||||||
contract.totalShares,
|
const { shares, outcome } = bet
|
||||||
bet.shares,
|
|
||||||
bet.outcome
|
const shareValue = calculateRawShareValue(totalShares, shares, outcome)
|
||||||
)
|
|
||||||
const f = calculateMoneyRatio(contract, bet, shareValue)
|
const f = calculateMoneyRatio(contract, bet, shareValue)
|
||||||
|
|
||||||
const myPool = contract.pool[bet.outcome]
|
const myPool = pool[outcome]
|
||||||
const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool)
|
const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool)
|
||||||
return adjShareValue
|
return adjShareValue
|
||||||
}
|
}
|
||||||
|
@ -109,11 +116,7 @@ export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
||||||
return deductFees(amount, winnings)
|
return deductFees(amount, winnings)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculatePayout(
|
export function calculatePayout(contract: Contract, bet: Bet, outcome: string) {
|
||||||
contract: Contract,
|
|
||||||
bet: Bet,
|
|
||||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
|
||||||
) {
|
|
||||||
if (outcome === 'CANCEL') return calculateCancelPayout(contract, bet)
|
if (outcome === 'CANCEL') return calculateCancelPayout(contract, bet)
|
||||||
if (outcome === 'MKT') return calculateMktPayout(contract, bet)
|
if (outcome === 'MKT') return calculateMktPayout(contract, bet)
|
||||||
|
|
||||||
|
@ -121,67 +124,100 @@ export function calculatePayout(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateCancelPayout(contract: Contract, bet: Bet) {
|
export function calculateCancelPayout(contract: Contract, bet: Bet) {
|
||||||
const totalBets = contract.totalBets.YES + contract.totalBets.NO
|
const { totalBets, pool } = contract
|
||||||
const pool = contract.pool.YES + contract.pool.NO
|
const betTotal = _.sum(Object.values(totalBets))
|
||||||
|
const poolTotal = _.sum(Object.values(pool))
|
||||||
|
|
||||||
return (bet.amount / totalBets) * pool
|
return (bet.amount / betTotal) * poolTotal
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateStandardPayout(
|
export function calculateStandardPayout(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
bet: Bet,
|
bet: Bet,
|
||||||
outcome: 'YES' | 'NO'
|
outcome: string
|
||||||
) {
|
) {
|
||||||
const { amount, outcome: betOutcome, shares } = bet
|
const { amount, outcome: betOutcome, shares } = bet
|
||||||
if (betOutcome !== outcome) return 0
|
if (betOutcome !== outcome) return 0
|
||||||
|
|
||||||
const { totalShares, phantomShares } = contract
|
const { totalShares, phantomShares, pool } = contract
|
||||||
if (totalShares[outcome] === 0) return 0
|
if (!totalShares[outcome]) return 0
|
||||||
|
|
||||||
const pool = contract.pool.YES + contract.pool.NO
|
const poolTotal = _.sum(Object.values(pool))
|
||||||
const total = totalShares[outcome] - phantomShares[outcome]
|
|
||||||
|
|
||||||
const winnings = (shares / total) * pool
|
const total =
|
||||||
|
totalShares[outcome] - (phantomShares ? phantomShares[outcome] : 0)
|
||||||
|
|
||||||
|
const winnings = (shares / total) * poolTotal
|
||||||
// profit can be negative if using phantom shares
|
// profit can be negative if using phantom shares
|
||||||
return amount + (1 - FEES) * Math.max(0, winnings - amount)
|
return amount + (1 - FEES) * Math.max(0, winnings - amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
|
export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
|
||||||
const { totalShares, pool, totalBets } = contract
|
const { totalShares, pool, totalBets } = contract
|
||||||
|
const { shares, amount, outcome } = bet
|
||||||
|
|
||||||
const ind = bet.outcome === 'YES' ? 1 : 0
|
const prevShares = totalShares[outcome] ?? 0
|
||||||
const { shares, amount } = bet
|
const prevPool = pool[outcome] ?? 0
|
||||||
|
const prevTotalBet = totalBets[outcome] ?? 0
|
||||||
|
|
||||||
const newContract = {
|
const newContract = {
|
||||||
...contract,
|
...contract,
|
||||||
totalShares: {
|
totalShares: {
|
||||||
YES: totalShares.YES + ind * shares,
|
...totalShares,
|
||||||
NO: totalShares.NO + (1 - ind) * shares,
|
[outcome]: prevShares + shares,
|
||||||
},
|
},
|
||||||
pool: {
|
pool: {
|
||||||
YES: pool.YES + ind * amount,
|
...pool,
|
||||||
NO: pool.NO + (1 - ind) * amount,
|
[outcome]: prevPool + amount,
|
||||||
},
|
},
|
||||||
totalBets: {
|
totalBets: {
|
||||||
YES: totalBets.YES + ind * amount,
|
...totalBets,
|
||||||
NO: totalBets.NO + (1 - ind) * amount,
|
[outcome]: prevTotalBet + amount,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return calculateStandardPayout(newContract, bet, bet.outcome)
|
return calculateStandardPayout(newContract, bet, outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateMktPayout(contract: Contract, bet: Bet) {
|
function calculateMktPayout(contract: Contract, bet: Bet) {
|
||||||
|
if (contract.outcomeType === 'BINARY')
|
||||||
|
return calculateBinaryMktPayout(contract, bet)
|
||||||
|
|
||||||
|
const { totalShares, pool } = contract
|
||||||
|
|
||||||
|
const totalPool = _.sum(Object.values(pool))
|
||||||
|
const sharesSquareSum = _.sumBy(
|
||||||
|
Object.values(totalShares),
|
||||||
|
(shares) => shares ** 2
|
||||||
|
)
|
||||||
|
|
||||||
|
const weightedShareTotal = _.sumBy(Object.keys(totalShares), (outcome) => {
|
||||||
|
// Avoid O(n^2) by reusing sharesSquareSum for prob.
|
||||||
|
const shares = totalShares[outcome]
|
||||||
|
const prob = shares ** 2 / sharesSquareSum
|
||||||
|
return prob * shares
|
||||||
|
})
|
||||||
|
|
||||||
|
const { outcome, amount, shares } = bet
|
||||||
|
|
||||||
|
const betP = getOutcomeProbability(totalShares, outcome)
|
||||||
|
const winnings = ((betP * shares) / weightedShareTotal) * totalPool
|
||||||
|
|
||||||
|
return deductFees(amount, winnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateBinaryMktPayout(contract: Contract, bet: Bet) {
|
||||||
|
const { resolutionProbability, totalShares, phantomShares } = contract
|
||||||
const p =
|
const p =
|
||||||
contract.resolutionProbability !== undefined
|
resolutionProbability !== undefined
|
||||||
? contract.resolutionProbability
|
? resolutionProbability
|
||||||
: getProbability(contract.totalShares)
|
: getProbability(totalShares)
|
||||||
|
|
||||||
const pool = contract.pool.YES + contract.pool.NO
|
const pool = contract.pool.YES + contract.pool.NO
|
||||||
|
|
||||||
const weightedShareTotal =
|
const weightedShareTotal =
|
||||||
p * (contract.totalShares.YES - contract.phantomShares.YES) +
|
p * (totalShares.YES - (phantomShares?.YES ?? 0)) +
|
||||||
(1 - p) * (contract.totalShares.NO - contract.phantomShares.NO)
|
(1 - p) * (totalShares.NO - (phantomShares?.NO ?? 0))
|
||||||
|
|
||||||
const { outcome, amount, shares } = bet
|
const { outcome, amount, shares } = bet
|
||||||
|
|
||||||
|
@ -197,15 +233,6 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
|
||||||
throw new Error('Contract was not resolved')
|
throw new Error('Contract was not resolved')
|
||||||
}
|
}
|
||||||
|
|
||||||
// deprecated use MKT payout
|
|
||||||
export function currentValue(contract: Contract, bet: Bet) {
|
|
||||||
const prob = getProbability(contract.pool)
|
|
||||||
const yesPayout = calculatePayout(contract, bet, 'YES')
|
|
||||||
const noPayout = calculatePayout(contract, bet, 'NO')
|
|
||||||
|
|
||||||
return prob * yesPayout + (1 - prob) * noPayout
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deductFees = (betAmount: number, winnings: number) => {
|
export const deductFees = (betAmount: number, winnings: number) => {
|
||||||
return winnings > betAmount
|
return winnings > betAmount
|
||||||
? betAmount + (1 - FEES) * (winnings - betAmount)
|
? betAmount + (1 - FEES) * (winnings - betAmount)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Answer } from './answer'
|
||||||
|
|
||||||
export type Contract = {
|
export type Contract = {
|
||||||
id: string
|
id: string
|
||||||
slug: string // auto-generated; must be unique
|
slug: string // auto-generated; must be unique
|
||||||
|
@ -11,14 +13,17 @@ export type Contract = {
|
||||||
description: string // More info about what the contract is about
|
description: string // More info about what the contract is about
|
||||||
tags: string[]
|
tags: string[]
|
||||||
lowercaseTags: string[]
|
lowercaseTags: string[]
|
||||||
outcomeType: 'BINARY' // | 'MULTI' | 'interval' | 'date'
|
|
||||||
visibility: 'public' | 'unlisted'
|
visibility: 'public' | 'unlisted'
|
||||||
|
|
||||||
|
outcomeType: 'BINARY' | 'MULTI' | 'FREE_RESPONSE'
|
||||||
|
multiOutcomes?: string[] // Used for outcomeType 'MULTI'.
|
||||||
|
answers?: Answer[] // Used for outcomeType 'FREE_RESPONSE'.
|
||||||
|
|
||||||
mechanism: 'dpm-2'
|
mechanism: 'dpm-2'
|
||||||
phantomShares: { YES: number; NO: number }
|
phantomShares?: { [outcome: string]: number }
|
||||||
pool: { YES: number; NO: number }
|
pool: { [outcome: string]: number }
|
||||||
totalShares: { YES: number; NO: number }
|
totalShares: { [outcome: string]: number }
|
||||||
totalBets: { YES: number; NO: number }
|
totalBets: { [outcome: string]: number }
|
||||||
|
|
||||||
createdTime: number // Milliseconds since epoch
|
createdTime: number // Milliseconds since epoch
|
||||||
lastUpdatedTime: number // If the question or description was changed
|
lastUpdatedTime: number // If the question or description was changed
|
||||||
|
@ -26,7 +31,7 @@ export type Contract = {
|
||||||
|
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
resolutionTime?: number // When the contract creator resolved the market
|
resolutionTime?: number // When the contract creator resolved the market
|
||||||
resolution?: outcome // Chosen by creator; must be one of outcomes
|
resolution?: string
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
closeEmailsSent?: number
|
closeEmailsSent?: number
|
||||||
|
|
||||||
|
@ -34,4 +39,4 @@ export type Contract = {
|
||||||
volume7Days: number
|
volume7Days: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type outcome = 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
export type outcomeType = 'BINARY' | 'MULTI' | 'FREE_RESPONSE'
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import { Bet } from './bet'
|
import { Bet } from './bet'
|
||||||
import { calculateShares, getProbability } from './calculate'
|
import {
|
||||||
|
calculateShares,
|
||||||
|
getProbability,
|
||||||
|
getOutcomeProbability,
|
||||||
|
} from './calculate'
|
||||||
import { Contract } from './contract'
|
import { Contract } from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
|
|
||||||
export const getNewBetInfo = (
|
export const getNewBinaryBetInfo = (
|
||||||
user: User,
|
user: User,
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
amount: number,
|
amount: number,
|
||||||
|
@ -52,3 +56,43 @@ export const getNewBetInfo = (
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getNewMultiBetInfo = (
|
||||||
|
user: User,
|
||||||
|
outcome: string,
|
||||||
|
amount: number,
|
||||||
|
contract: Contract,
|
||||||
|
newBetId: string
|
||||||
|
) => {
|
||||||
|
const { pool, totalShares, totalBets } = contract
|
||||||
|
|
||||||
|
const prevOutcomePool = pool[outcome] ?? 0
|
||||||
|
const newPool = { ...pool, [outcome]: prevOutcomePool + amount }
|
||||||
|
|
||||||
|
const shares = calculateShares(contract.totalShares, amount, outcome)
|
||||||
|
|
||||||
|
const prevShares = totalShares[outcome] ?? 0
|
||||||
|
const newTotalShares = { ...totalShares, [outcome]: prevShares + shares }
|
||||||
|
|
||||||
|
const prevTotalBets = totalBets[outcome] ?? 0
|
||||||
|
const newTotalBets = { ...totalBets, [outcome]: prevTotalBets + amount }
|
||||||
|
|
||||||
|
const probBefore = getOutcomeProbability(totalShares, outcome)
|
||||||
|
const probAfter = getOutcomeProbability(newTotalShares, outcome)
|
||||||
|
|
||||||
|
const newBet: Bet = {
|
||||||
|
id: newBetId,
|
||||||
|
userId: user.id,
|
||||||
|
contractId: contract.id,
|
||||||
|
amount,
|
||||||
|
shares,
|
||||||
|
outcome,
|
||||||
|
probBefore,
|
||||||
|
probAfter,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBalance = user.balance - amount
|
||||||
|
|
||||||
|
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||||
|
}
|
||||||
|
|
|
@ -1,32 +1,37 @@
|
||||||
import { calcStartPool } from './antes'
|
import { calcStartPool } from './antes'
|
||||||
|
import { Contract, outcomeType } from './contract'
|
||||||
import { Contract } from './contract'
|
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
import { parseTags } from './util/parse'
|
import { parseTags } from './util/parse'
|
||||||
|
import { removeUndefinedProps } from './util/object'
|
||||||
|
|
||||||
export function getNewContract(
|
export function getNewContract(
|
||||||
id: string,
|
id: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
creator: User,
|
creator: User,
|
||||||
question: string,
|
question: string,
|
||||||
|
outcomeType: outcomeType,
|
||||||
description: string,
|
description: string,
|
||||||
initialProb: number,
|
initialProb: number,
|
||||||
ante: number,
|
ante: number,
|
||||||
closeTime: number,
|
closeTime: number,
|
||||||
extraTags: string[]
|
extraTags: string[]
|
||||||
) {
|
) {
|
||||||
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
|
|
||||||
calcStartPool(initialProb, ante)
|
|
||||||
|
|
||||||
const tags = parseTags(
|
const tags = parseTags(
|
||||||
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
||||||
)
|
)
|
||||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||||
|
|
||||||
const contract: Contract = {
|
const propsByOutcomeType =
|
||||||
|
outcomeType === 'BINARY'
|
||||||
|
? getBinaryProps(initialProb, ante)
|
||||||
|
: getFreeAnswerProps(ante)
|
||||||
|
|
||||||
|
const contract: Contract = removeUndefinedProps({
|
||||||
id,
|
id,
|
||||||
slug,
|
slug,
|
||||||
outcomeType: 'BINARY',
|
mechanism: 'dpm-2',
|
||||||
|
outcomeType,
|
||||||
|
...propsByOutcomeType,
|
||||||
|
|
||||||
creatorId: creator.id,
|
creatorId: creator.id,
|
||||||
creatorName: creator.name,
|
creatorName: creator.name,
|
||||||
|
@ -38,22 +43,43 @@ export function getNewContract(
|
||||||
tags,
|
tags,
|
||||||
lowercaseTags,
|
lowercaseTags,
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
|
isResolved: false,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
lastUpdatedTime: Date.now(),
|
||||||
|
closeTime,
|
||||||
|
|
||||||
mechanism: 'dpm-2',
|
volume24Hours: 0,
|
||||||
|
volume7Days: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return contract
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBinaryProps = (initialProb: number, ante: number) => {
|
||||||
|
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
|
||||||
|
calcStartPool(initialProb, ante)
|
||||||
|
|
||||||
|
return {
|
||||||
phantomShares: { YES: phantomYes, NO: phantomNo },
|
phantomShares: { YES: phantomYes, NO: phantomNo },
|
||||||
pool: { YES: poolYes, NO: poolNo },
|
pool: { YES: poolYes, NO: poolNo },
|
||||||
totalShares: { YES: sharesYes, NO: sharesNo },
|
totalShares: { YES: sharesYes, NO: sharesNo },
|
||||||
totalBets: { YES: poolYes, NO: poolNo },
|
totalBets: { YES: poolYes, NO: poolNo },
|
||||||
isResolved: false,
|
|
||||||
|
|
||||||
createdTime: Date.now(),
|
|
||||||
lastUpdatedTime: Date.now(),
|
|
||||||
|
|
||||||
volume24Hours: 0,
|
|
||||||
volume7Days: 0,
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (closeTime) contract.closeTime = closeTime
|
|
||||||
|
const getFreeAnswerProps = (ante: number) => {
|
||||||
return contract
|
return {
|
||||||
|
pool: { '0': ante },
|
||||||
|
totalShares: { '0': ante },
|
||||||
|
totalBets: { '0': ante },
|
||||||
|
answers: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMultiProps = (
|
||||||
|
outcomes: string[],
|
||||||
|
initialProbs: number[],
|
||||||
|
ante: number
|
||||||
|
) => {
|
||||||
|
// Not implemented.
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ import * as _ from 'lodash'
|
||||||
|
|
||||||
import { Bet } from './bet'
|
import { Bet } from './bet'
|
||||||
import { deductFees, getProbability } from './calculate'
|
import { deductFees, getProbability } from './calculate'
|
||||||
import { Contract, outcome } from './contract'
|
import { Contract } from './contract'
|
||||||
import { CREATOR_FEE, FEES } from './fees'
|
import { CREATOR_FEE, FEES } from './fees'
|
||||||
|
|
||||||
export const getCancelPayouts = (contract: Contract, bets: Bet[]) => {
|
export const getCancelPayouts = (contract: Contract, bets: Bet[]) => {
|
||||||
const { pool } = contract
|
const { pool } = contract
|
||||||
const poolTotal = pool.YES + pool.NO
|
const poolTotal = _.sum(Object.values(pool))
|
||||||
console.log('resolved N/A, pool M$', poolTotal)
|
console.log('resolved N/A, pool M$', poolTotal)
|
||||||
|
|
||||||
const betSum = _.sumBy(bets, (b) => b.amount)
|
const betSum = _.sumBy(bets, (b) => b.amount)
|
||||||
|
@ -19,18 +19,17 @@ export const getCancelPayouts = (contract: Contract, bets: Bet[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStandardPayouts = (
|
export const getStandardPayouts = (
|
||||||
outcome: 'YES' | 'NO',
|
outcome: string,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
) => {
|
) => {
|
||||||
const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES')
|
const winningBets = bets.filter((bet) => bet.outcome === outcome)
|
||||||
const winningBets = outcome === 'YES' ? yesBets : noBets
|
|
||||||
|
|
||||||
const pool = contract.pool.YES + contract.pool.NO
|
const poolTotal = _.sum(Object.values(contract.pool))
|
||||||
const totalShares = _.sumBy(winningBets, (b) => b.shares)
|
const totalShares = _.sumBy(winningBets, (b) => b.shares)
|
||||||
|
|
||||||
const payouts = winningBets.map(({ userId, amount, shares }) => {
|
const payouts = winningBets.map(({ userId, amount, shares }) => {
|
||||||
const winnings = (shares / totalShares) * pool
|
const winnings = (shares / totalShares) * poolTotal
|
||||||
const profit = winnings - amount
|
const profit = winnings - amount
|
||||||
|
|
||||||
// profit can be negative if using phantom shares
|
// profit can be negative if using phantom shares
|
||||||
|
@ -45,7 +44,7 @@ export const getStandardPayouts = (
|
||||||
'resolved',
|
'resolved',
|
||||||
outcome,
|
outcome,
|
||||||
'pool',
|
'pool',
|
||||||
pool,
|
poolTotal,
|
||||||
'profits',
|
'profits',
|
||||||
profits,
|
profits,
|
||||||
'creator fee',
|
'creator fee',
|
||||||
|
@ -101,7 +100,7 @@ export const getMktPayouts = (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPayouts = (
|
export const getPayouts = (
|
||||||
outcome: outcome,
|
outcome: string,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
bets: Bet[],
|
bets: Bet[],
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
|
@ -114,5 +113,8 @@ export const getPayouts = (
|
||||||
return getMktPayouts(contract, bets, resolutionProbability)
|
return getMktPayouts(contract, bets, resolutionProbability)
|
||||||
case 'CANCEL':
|
case 'CANCEL':
|
||||||
return getCancelPayouts(contract, bets)
|
return getCancelPayouts(contract, bets)
|
||||||
|
default:
|
||||||
|
// Multi outcome.
|
||||||
|
return getStandardPayouts(outcome, contract, bets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Bet } from './bet'
|
import { Bet } from './bet'
|
||||||
import { calculateShareValue, deductFees, getProbability } from './calculate'
|
import { calculateShareValue, deductFees, getProbability } from './calculate'
|
||||||
import { Contract } from './contract'
|
import { Contract } from './contract'
|
||||||
import { CREATOR_FEE, FEES } from './fees'
|
import { CREATOR_FEE } from './fees'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
|
|
||||||
export const getSellBetInfo = (
|
export const getSellBetInfo = (
|
||||||
|
@ -10,30 +10,21 @@ export const getSellBetInfo = (
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
|
const { pool, totalShares, totalBets } = contract
|
||||||
const { id: betId, amount, shares, outcome } = bet
|
const { id: betId, amount, shares, outcome } = bet
|
||||||
|
|
||||||
const { YES: yesPool, NO: noPool } = contract.pool
|
|
||||||
const { YES: yesShares, NO: noShares } = contract.totalShares
|
|
||||||
const { YES: yesBets, NO: noBets } = contract.totalBets
|
|
||||||
|
|
||||||
const adjShareValue = calculateShareValue(contract, bet)
|
const adjShareValue = calculateShareValue(contract, bet)
|
||||||
|
|
||||||
const newPool =
|
const newPool = { ...pool, [outcome]: pool[outcome] - adjShareValue }
|
||||||
outcome === 'YES'
|
|
||||||
? { YES: yesPool - adjShareValue, NO: noPool }
|
|
||||||
: { YES: yesPool, NO: noPool - adjShareValue }
|
|
||||||
|
|
||||||
const newTotalShares =
|
const newTotalShares = {
|
||||||
outcome === 'YES'
|
...totalShares,
|
||||||
? { YES: yesShares - shares, NO: noShares }
|
[outcome]: totalShares[outcome] - shares,
|
||||||
: { YES: yesShares, NO: noShares - shares }
|
}
|
||||||
|
|
||||||
const newTotalBets =
|
const newTotalBets = { ...totalBets, [outcome]: totalBets[outcome] - amount }
|
||||||
outcome === 'YES'
|
|
||||||
? { YES: yesBets - amount, NO: noBets }
|
|
||||||
: { YES: yesBets, NO: noBets - amount }
|
|
||||||
|
|
||||||
const probBefore = getProbability(contract.totalShares)
|
const probBefore = getProbability(totalShares)
|
||||||
const probAfter = getProbability(newTotalShares)
|
const probAfter = getProbability(newTotalShares)
|
||||||
|
|
||||||
const profit = adjShareValue - amount
|
const profit = adjShareValue - amount
|
||||||
|
|
9
common/util/object.ts
Normal file
9
common/util/object.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export const removeUndefinedProps = <T>(obj: T): T => {
|
||||||
|
let newObj: any = {}
|
||||||
|
|
||||||
|
for (let key of Object.keys(obj)) {
|
||||||
|
if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return newObj
|
||||||
|
}
|
|
@ -27,20 +27,16 @@ service cloud.firestore {
|
||||||
allow delete: if resource.data.creatorId == request.auth.uid;
|
allow delete: if resource.data.creatorId == request.auth.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
match /contracts/{contractId}/bets/{betId} {
|
|
||||||
allow read;
|
|
||||||
}
|
|
||||||
|
|
||||||
match /{somePath=**}/bets/{betId} {
|
match /{somePath=**}/bets/{betId} {
|
||||||
allow read;
|
allow read;
|
||||||
}
|
}
|
||||||
|
|
||||||
match /contracts/{contractId}/comments/{commentId} {
|
match /{somePath=**}/comments/{commentId} {
|
||||||
allow read;
|
allow read;
|
||||||
allow create: if request.auth != null;
|
allow create: if request.auth != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
match /{somePath=**}/comments/{commentId} {
|
match /{somePath=**}/answers/{answerId} {
|
||||||
allow read;
|
allow read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,13 +45,9 @@ service cloud.firestore {
|
||||||
allow update, delete: if request.auth.uid == resource.data.curatorId;
|
allow update, delete: if request.auth.uid == resource.data.curatorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
match /folds/{foldId}/followers/{userId} {
|
match /{somePath=**}/followers/{userId} {
|
||||||
allow read;
|
allow read;
|
||||||
allow write: if request.auth.uid == userId;
|
allow write: if request.auth.uid == userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
match /{somePath=**}/followers/{userId} {
|
|
||||||
allow read;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { getUser, removeUndefinedProps } from './utils'
|
import { getUser } from './utils'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { cleanUsername } from '../../common/util/clean-username'
|
import { cleanUsername } from '../../common/util/clean-username'
|
||||||
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
|
import { Answer } from '../../common/answer'
|
||||||
|
|
||||||
export const changeUserInfo = functions
|
export const changeUserInfo = functions
|
||||||
.runWith({ minInstances: 1 })
|
.runWith({ minInstances: 1 })
|
||||||
|
@ -88,12 +90,23 @@ export const changeUser = async (
|
||||||
userAvatarUrl: update.avatarUrl,
|
userAvatarUrl: update.avatarUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const answerSnap = await transaction.get(
|
||||||
|
firestore
|
||||||
|
.collectionGroup('answers')
|
||||||
|
.where('username', '==', user.username)
|
||||||
|
)
|
||||||
|
const answerUpdate: Partial<Answer> = removeUndefinedProps(update)
|
||||||
|
|
||||||
await transaction.update(userRef, userUpdate)
|
await transaction.update(userRef, userUpdate)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
commentSnap.docs.map((d) => transaction.update(d.ref, commentUpdate))
|
commentSnap.docs.map((d) => transaction.update(d.ref, commentUpdate))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
answerSnap.docs.map((d) => transaction.update(d.ref, answerUpdate))
|
||||||
|
)
|
||||||
|
|
||||||
await contracts.docs.map((d) => transaction.update(d.ref, contractUpdate))
|
await contracts.docs.map((d) => transaction.update(d.ref, contractUpdate))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
111
functions/src/create-answer.ts
Normal file
111
functions/src/create-answer.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { getNewMultiBetInfo } from '../../common/new-bet'
|
||||||
|
import { Answer } from '../../common/answer'
|
||||||
|
import { getValues } from './utils'
|
||||||
|
|
||||||
|
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
|
async (
|
||||||
|
data: {
|
||||||
|
contractId: string
|
||||||
|
amount: number
|
||||||
|
text: string
|
||||||
|
},
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const userId = context?.auth?.uid
|
||||||
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
|
const { contractId, amount, text } = data
|
||||||
|
|
||||||
|
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
||||||
|
return { status: 'error', message: 'Invalid amount' }
|
||||||
|
|
||||||
|
if (!text || typeof text !== 'string' || text.length > 10000)
|
||||||
|
return { status: 'error', message: 'Invalid text' }
|
||||||
|
|
||||||
|
// Run as transaction to prevent race conditions.
|
||||||
|
return await firestore.runTransaction(async (transaction) => {
|
||||||
|
const userDoc = firestore.doc(`users/${userId}`)
|
||||||
|
const userSnap = await transaction.get(userDoc)
|
||||||
|
if (!userSnap.exists)
|
||||||
|
return { status: 'error', message: 'User not found' }
|
||||||
|
const user = userSnap.data() as User
|
||||||
|
|
||||||
|
if (user.balance < amount)
|
||||||
|
return { status: 'error', message: 'Insufficient balance' }
|
||||||
|
|
||||||
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
|
const contractSnap = await transaction.get(contractDoc)
|
||||||
|
if (!contractSnap.exists)
|
||||||
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
|
const contract = contractSnap.data() as Contract
|
||||||
|
|
||||||
|
if (contract.outcomeType !== 'FREE_RESPONSE')
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Requires a free response contract',
|
||||||
|
}
|
||||||
|
|
||||||
|
const { closeTime } = contract
|
||||||
|
if (closeTime && Date.now() > closeTime)
|
||||||
|
return { status: 'error', message: 'Trading is closed' }
|
||||||
|
|
||||||
|
const [lastAnswer] = await getValues<Answer>(
|
||||||
|
firestore
|
||||||
|
.collection(`contracts/${contractId}/answers`)
|
||||||
|
.orderBy('number', 'desc')
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!lastAnswer)
|
||||||
|
return { status: 'error', message: 'Could not fetch last answer' }
|
||||||
|
|
||||||
|
const number = lastAnswer.number + 1
|
||||||
|
const id = `${number}`
|
||||||
|
|
||||||
|
const newAnswerDoc = firestore
|
||||||
|
.collection(`contracts/${contractId}/answers`)
|
||||||
|
.doc(id)
|
||||||
|
|
||||||
|
const answerId = newAnswerDoc.id
|
||||||
|
const { username, name, avatarUrl } = user
|
||||||
|
|
||||||
|
const answer: Answer = {
|
||||||
|
id,
|
||||||
|
number,
|
||||||
|
contractId,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
userId: user.id,
|
||||||
|
username,
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
transaction.create(newAnswerDoc, answer)
|
||||||
|
|
||||||
|
const newBetDoc = firestore
|
||||||
|
.collection(`contracts/${contractId}/bets`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
|
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||||
|
getNewMultiBetInfo(user, answerId, amount, contract, newBetDoc.id)
|
||||||
|
|
||||||
|
transaction.create(newBetDoc, { ...newBet, isAnte: true })
|
||||||
|
transaction.update(contractDoc, {
|
||||||
|
pool: newPool,
|
||||||
|
totalShares: newTotalShares,
|
||||||
|
totalBets: newTotalBets,
|
||||||
|
answers: [...(contract.answers ?? []), answer],
|
||||||
|
})
|
||||||
|
transaction.update(userDoc, { balance: newBalance })
|
||||||
|
|
||||||
|
return { status: 'success', answerId, betId: newBetDoc.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
|
@ -2,11 +2,16 @@ import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { chargeUser, getUser } from './utils'
|
import { chargeUser, getUser } from './utils'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract, outcomeType } from '../../common/contract'
|
||||||
import { slugify } from '../../common/util/slugify'
|
import { slugify } from '../../common/util/slugify'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
import { getNewContract } from '../../common/new-contract'
|
import { getNewContract } from '../../common/new-contract'
|
||||||
import { getAnteBets, MINIMUM_ANTE } from '../../common/antes'
|
import {
|
||||||
|
getAnteBets,
|
||||||
|
getFreeAnswerAnte,
|
||||||
|
MINIMUM_ANTE,
|
||||||
|
} from '../../common/antes'
|
||||||
|
import { getNoneAnswer } from '../../common/answer'
|
||||||
|
|
||||||
export const createContract = functions
|
export const createContract = functions
|
||||||
.runWith({ minInstances: 1 })
|
.runWith({ minInstances: 1 })
|
||||||
|
@ -14,6 +19,7 @@ export const createContract = functions
|
||||||
async (
|
async (
|
||||||
data: {
|
data: {
|
||||||
question: string
|
question: string
|
||||||
|
outcomeType: outcomeType
|
||||||
description: string
|
description: string
|
||||||
initialProb: number
|
initialProb: number
|
||||||
ante: number
|
ante: number
|
||||||
|
@ -30,10 +36,17 @@ export const createContract = functions
|
||||||
|
|
||||||
const { question, description, initialProb, ante, closeTime, tags } = data
|
const { question, description, initialProb, ante, closeTime, tags } = data
|
||||||
|
|
||||||
if (!question || !initialProb)
|
if (!question)
|
||||||
return { status: 'error', message: 'Missing contract attributes' }
|
return { status: 'error', message: 'Missing question field' }
|
||||||
|
|
||||||
if (initialProb < 1 || initialProb > 99)
|
let outcomeType = data.outcomeType ?? 'BINARY'
|
||||||
|
if (!['BINARY', 'MULTI', 'FREE_RESPONSE'].includes(outcomeType))
|
||||||
|
return { status: 'error', message: 'Invalid outcomeType' }
|
||||||
|
|
||||||
|
if (
|
||||||
|
outcomeType === 'BINARY' &&
|
||||||
|
(!initialProb || initialProb < 1 || initialProb > 99)
|
||||||
|
)
|
||||||
return { status: 'error', message: 'Invalid initial probability' }
|
return { status: 'error', message: 'Invalid initial probability' }
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -63,6 +76,7 @@ export const createContract = functions
|
||||||
slug,
|
slug,
|
||||||
creator,
|
creator,
|
||||||
question,
|
question,
|
||||||
|
outcomeType,
|
||||||
description,
|
description,
|
||||||
initialProb,
|
initialProb,
|
||||||
ante,
|
ante,
|
||||||
|
@ -75,6 +89,7 @@ export const createContract = functions
|
||||||
await contractRef.create(contract)
|
await contractRef.create(contract)
|
||||||
|
|
||||||
if (ante) {
|
if (ante) {
|
||||||
|
if (outcomeType === 'BINARY') {
|
||||||
const yesBetDoc = firestore
|
const yesBetDoc = firestore
|
||||||
.collection(`contracts/${contract.id}/bets`)
|
.collection(`contracts/${contract.id}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
|
@ -91,6 +106,19 @@ export const createContract = functions
|
||||||
)
|
)
|
||||||
await yesBetDoc.set(yesBet)
|
await yesBetDoc.set(yesBet)
|
||||||
await noBetDoc.set(noBet)
|
await noBetDoc.set(noBet)
|
||||||
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||||
|
const noneAnswerDoc = firestore
|
||||||
|
.collection(`contracts/${contract.id}/answers`)
|
||||||
|
.doc('0')
|
||||||
|
const noneAnswer = getNoneAnswer(contract.id, creator)
|
||||||
|
await noneAnswerDoc.set(noneAnswer)
|
||||||
|
|
||||||
|
const anteBetDoc = firestore
|
||||||
|
.collection(`contracts/${contract.id}/bets`)
|
||||||
|
.doc()
|
||||||
|
const anteBet = getFreeAnswerAnte(creator, contract, anteBetDoc.id)
|
||||||
|
await anteBetDoc.set(anteBet)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status: 'success', contract }
|
return { status: 'success', contract }
|
||||||
|
|
|
@ -17,12 +17,23 @@ type market_resolved_template = {
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toDisplayResolution = (outcome: string, prob: number) => {
|
||||||
|
const display = {
|
||||||
|
YES: 'YES',
|
||||||
|
NO: 'NO',
|
||||||
|
CANCEL: 'N/A',
|
||||||
|
MKT: formatPercent(prob),
|
||||||
|
}[outcome]
|
||||||
|
|
||||||
|
return display === undefined ? `#${outcome}` : display
|
||||||
|
}
|
||||||
|
|
||||||
export const sendMarketResolutionEmail = async (
|
export const sendMarketResolutionEmail = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
payout: number,
|
payout: number,
|
||||||
creator: User,
|
creator: User,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT',
|
resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string,
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(userId)
|
const privateUser = await getPrivateUser(userId)
|
||||||
|
@ -38,13 +49,7 @@ export const sendMarketResolutionEmail = async (
|
||||||
|
|
||||||
const prob = resolutionProbability ?? getProbability(contract.totalShares)
|
const prob = resolutionProbability ?? getProbability(contract.totalShares)
|
||||||
|
|
||||||
const toDisplayResolution = {
|
const outcome = toDisplayResolution(resolution, prob)
|
||||||
YES: 'YES',
|
|
||||||
NO: 'NO',
|
|
||||||
CANCEL: 'N/A',
|
|
||||||
MKT: formatPercent(prob),
|
|
||||||
}
|
|
||||||
const outcome = toDisplayResolution[resolution]
|
|
||||||
|
|
||||||
const subject = `Resolved ${outcome}: ${contract.question}`
|
const subject = `Resolved ${outcome}: ${contract.question}`
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ export * from './sell-bet'
|
||||||
export * from './create-contract'
|
export * from './create-contract'
|
||||||
export * from './create-user'
|
export * from './create-user'
|
||||||
export * from './create-fold'
|
export * from './create-fold'
|
||||||
|
export * from './create-answer'
|
||||||
export * from './on-fold-follow'
|
export * from './on-fold-follow'
|
||||||
export * from './on-fold-delete'
|
export * from './on-fold-delete'
|
||||||
export * from './unsubscribe'
|
export * from './unsubscribe'
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { getNewBetInfo } from '../../common/new-bet'
|
import { getNewBinaryBetInfo, getNewMultiBetInfo } from '../../common/new-bet'
|
||||||
|
|
||||||
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -22,7 +22,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
||||||
return { status: 'error', message: 'Invalid amount' }
|
return { status: 'error', message: 'Invalid amount' }
|
||||||
|
|
||||||
if (outcome !== 'YES' && outcome !== 'NO')
|
if (outcome !== 'YES' && outcome !== 'NO' && isNaN(+outcome))
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
return { status: 'error', message: 'Invalid outcome' }
|
||||||
|
|
||||||
// run as transaction to prevent race conditions
|
// run as transaction to prevent race conditions
|
||||||
|
@ -42,16 +42,32 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
|
|
||||||
const { closeTime } = contract
|
const { closeTime, outcomeType } = contract
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
return { status: 'error', message: 'Trading is closed' }
|
||||||
|
|
||||||
|
if (outcomeType === 'FREE_RESPONSE') {
|
||||||
|
const answerSnap = await transaction.get(
|
||||||
|
contractDoc.collection('answers').doc(outcome)
|
||||||
|
)
|
||||||
|
if (!answerSnap.exists)
|
||||||
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
|
}
|
||||||
|
|
||||||
const newBetDoc = firestore
|
const newBetDoc = firestore
|
||||||
.collection(`contracts/${contractId}/bets`)
|
.collection(`contracts/${contractId}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||||
getNewBetInfo(user, outcome, amount, contract, newBetDoc.id)
|
outcomeType === 'BINARY'
|
||||||
|
? getNewBinaryBetInfo(
|
||||||
|
user,
|
||||||
|
outcome as 'YES' | 'NO',
|
||||||
|
amount,
|
||||||
|
contract,
|
||||||
|
newBetDoc.id
|
||||||
|
)
|
||||||
|
: getNewMultiBetInfo(user, outcome, amount, contract, newBetDoc.id)
|
||||||
|
|
||||||
transaction.create(newBetDoc, newBet)
|
transaction.create(newBetDoc, newBet)
|
||||||
transaction.update(contractDoc, {
|
transaction.update(contractDoc, {
|
||||||
|
|
|
@ -25,10 +25,25 @@ export const resolveMarket = functions
|
||||||
|
|
||||||
const { outcome, contractId, probabilityInt } = data
|
const { outcome, contractId, probabilityInt } = data
|
||||||
|
|
||||||
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
|
const contractSnap = await contractDoc.get()
|
||||||
|
if (!contractSnap.exists)
|
||||||
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
|
const contract = contractSnap.data() as Contract
|
||||||
|
const { creatorId, outcomeType } = contract
|
||||||
|
|
||||||
|
if (outcomeType === 'BINARY') {
|
||||||
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
|
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
|
||||||
return { status: 'error', message: 'Invalid outcome' }
|
return { status: 'error', message: 'Invalid outcome' }
|
||||||
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||||
|
if (outcome !== 'CANCEL' && isNaN(+outcome))
|
||||||
|
return { status: 'error', message: 'Invalid outcome' }
|
||||||
|
} else {
|
||||||
|
return { status: 'error', message: 'Invalid contract outcomeType' }
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
outcomeType === 'BINARY' &&
|
||||||
probabilityInt !== undefined &&
|
probabilityInt !== undefined &&
|
||||||
(probabilityInt < 0 ||
|
(probabilityInt < 0 ||
|
||||||
probabilityInt > 100 ||
|
probabilityInt > 100 ||
|
||||||
|
@ -36,19 +51,13 @@ export const resolveMarket = functions
|
||||||
)
|
)
|
||||||
return { status: 'error', message: 'Invalid probability' }
|
return { status: 'error', message: 'Invalid probability' }
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
if (creatorId !== userId)
|
||||||
const contractSnap = await contractDoc.get()
|
|
||||||
if (!contractSnap.exists)
|
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
|
||||||
const contract = contractSnap.data() as Contract
|
|
||||||
|
|
||||||
if (contract.creatorId !== userId)
|
|
||||||
return { status: 'error', message: 'User not creator of contract' }
|
return { status: 'error', message: 'User not creator of contract' }
|
||||||
|
|
||||||
if (contract.resolution)
|
if (contract.resolution)
|
||||||
return { status: 'error', message: 'Contract already resolved' }
|
return { status: 'error', message: 'Contract already resolved' }
|
||||||
|
|
||||||
const creator = await getUser(contract.creatorId)
|
const creator = await getUser(creatorId)
|
||||||
if (!creator) return { status: 'error', message: 'Creator not found' }
|
if (!creator) return { status: 'error', message: 'Creator not found' }
|
||||||
|
|
||||||
const resolutionProbability =
|
const resolutionProbability =
|
||||||
|
@ -112,7 +121,7 @@ const sendResolutionEmails = async (
|
||||||
userPayouts: { [userId: string]: number },
|
userPayouts: { [userId: string]: number },
|
||||||
creator: User,
|
creator: User,
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT',
|
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string,
|
||||||
resolutionProbability?: number
|
resolutionProbability?: number
|
||||||
) => {
|
) => {
|
||||||
const nonWinners = _.difference(
|
const nonWinners = _.difference(
|
||||||
|
|
|
@ -83,11 +83,15 @@ async function recalculateContract(
|
||||||
let totalShares = {
|
let totalShares = {
|
||||||
YES: Math.sqrt(p) * (phantomAnte + realAnte),
|
YES: Math.sqrt(p) * (phantomAnte + realAnte),
|
||||||
NO: Math.sqrt(1 - p) * (phantomAnte + realAnte),
|
NO: Math.sqrt(1 - p) * (phantomAnte + realAnte),
|
||||||
|
} as { [outcome: string]: number }
|
||||||
|
|
||||||
|
let pool = { YES: p * realAnte, NO: (1 - p) * realAnte } as {
|
||||||
|
[outcome: string]: number
|
||||||
}
|
}
|
||||||
|
|
||||||
let pool = { YES: p * realAnte, NO: (1 - p) * realAnte }
|
let totalBets = { YES: p * realAnte, NO: (1 - p) * realAnte } as {
|
||||||
|
[outcome: string]: number
|
||||||
let totalBets = { YES: p * realAnte, NO: (1 - p) * realAnte }
|
}
|
||||||
|
|
||||||
const betsRef = contractRef.collection('bets')
|
const betsRef = contractRef.collection('bets')
|
||||||
|
|
||||||
|
|
|
@ -77,13 +77,3 @@ export const chargeUser = (userId: string, charge: number) => {
|
||||||
|
|
||||||
return updateUserBalance(userId, -charge)
|
return updateUserBalance(userId, -charge)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeUndefinedProps = <T>(obj: T): T => {
|
|
||||||
let newObj: any = {}
|
|
||||||
|
|
||||||
for (let key of Object.keys(obj)) {
|
|
||||||
if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
return newObj
|
|
||||||
}
|
|
||||||
|
|
546
web/components/answers-panel.tsx
Normal file
546
web/components/answers-panel.tsx
Normal file
|
@ -0,0 +1,546 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
import { XIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
|
import { Answer } from '../../common/answer'
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { AmountInput } from './amount-input'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import { createAnswer, placeBet, resolveMarket } from '../lib/firebase/api-call'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
import { Avatar } from './avatar'
|
||||||
|
import { SiteLink } from './site-link'
|
||||||
|
import { DateTimeTooltip } from './datetime-tooltip'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { BuyButton, ChooseCancelSelector } from './yes-no-selector'
|
||||||
|
import { Spacer } from './layout/spacer'
|
||||||
|
import {
|
||||||
|
formatMoney,
|
||||||
|
formatPercent,
|
||||||
|
formatWithCommas,
|
||||||
|
} from '../../common/util/format'
|
||||||
|
import { InfoTooltip } from './info-tooltip'
|
||||||
|
import { useUser } from '../hooks/use-user'
|
||||||
|
import {
|
||||||
|
getProbabilityAfterBet,
|
||||||
|
getOutcomeProbability,
|
||||||
|
calculateShares,
|
||||||
|
calculatePayoutAfterCorrectBet,
|
||||||
|
} from '../../common/calculate'
|
||||||
|
import { firebaseLogin } from '../lib/firebase/users'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
import { useAnswers } from '../hooks/use-answers'
|
||||||
|
import { ResolveConfirmationButton } from './confirmation-button'
|
||||||
|
import { tradingAllowed } from '../lib/firebase/contracts'
|
||||||
|
|
||||||
|
export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) {
|
||||||
|
const { contract } = props
|
||||||
|
const { creatorId, resolution } = contract
|
||||||
|
|
||||||
|
const answers = useAnswers(contract.id) ?? props.answers
|
||||||
|
const [chosenAnswer, otherAnswers] = _.partition(
|
||||||
|
answers.filter((answer) => answer.id !== '0'),
|
||||||
|
(answer) => answer.id === resolution
|
||||||
|
)
|
||||||
|
const sortedAnswers = [
|
||||||
|
...chosenAnswer,
|
||||||
|
..._.sortBy(
|
||||||
|
otherAnswers,
|
||||||
|
(answer) => -1 * getOutcomeProbability(contract.totalShares, answer.id)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const [resolveOption, setResolveOption] = useState<
|
||||||
|
'CHOOSE' | 'CANCEL' | undefined
|
||||||
|
>()
|
||||||
|
const [answerChoice, setAnswerChoice] = useState<string | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resolveOption !== 'CHOOSE' && answerChoice) setAnswerChoice(undefined)
|
||||||
|
}, [answerChoice, resolveOption])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className="gap-3">
|
||||||
|
{sortedAnswers.map((answer) => (
|
||||||
|
<AnswerItem
|
||||||
|
key={answer.id}
|
||||||
|
answer={answer}
|
||||||
|
contract={contract}
|
||||||
|
showChoice={!resolution && resolveOption === 'CHOOSE'}
|
||||||
|
isChosen={answer.id === answerChoice}
|
||||||
|
onChoose={() => setAnswerChoice(answer.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{sortedAnswers.length === 0 ? (
|
||||||
|
<div className="text-gray-500 p-4">No answers yet...</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-500 self-end p-4">
|
||||||
|
None of the above:{' '}
|
||||||
|
{formatPercent(getOutcomeProbability(contract.totalShares, '0'))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tradingAllowed(contract) && <CreateAnswerInput contract={contract} />}
|
||||||
|
|
||||||
|
{user?.id === creatorId && !resolution && (
|
||||||
|
<AnswerResolvePanel
|
||||||
|
contract={contract}
|
||||||
|
resolveOption={resolveOption}
|
||||||
|
setResolveOption={setResolveOption}
|
||||||
|
answer={answerChoice}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnswerItem(props: {
|
||||||
|
answer: Answer
|
||||||
|
contract: Contract
|
||||||
|
showChoice: boolean
|
||||||
|
isChosen: boolean
|
||||||
|
onChoose: () => void
|
||||||
|
}) {
|
||||||
|
const { answer, contract, showChoice, isChosen, onChoose } = props
|
||||||
|
const { resolution, totalShares } = contract
|
||||||
|
const { username, avatarUrl, name, createdTime, number, text } = answer
|
||||||
|
|
||||||
|
const createdDate = dayjs(createdTime).format('MMM D')
|
||||||
|
const prob = getOutcomeProbability(totalShares, answer.id)
|
||||||
|
const probPercent = formatPercent(prob)
|
||||||
|
const wasResolvedTo = resolution === answer.id
|
||||||
|
|
||||||
|
const [isBetting, setIsBetting] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
className={clsx(
|
||||||
|
'p-4 sm:flex-row rounded gap-4',
|
||||||
|
wasResolvedTo
|
||||||
|
? 'bg-green-50 mb-8'
|
||||||
|
: isChosen
|
||||||
|
? 'bg-green-50'
|
||||||
|
: 'bg-gray-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Col className="gap-3 flex-1">
|
||||||
|
<div className="whitespace-pre-line break-words">{text}</div>
|
||||||
|
|
||||||
|
<Row className="text-gray-500 text-sm gap-2 items-center">
|
||||||
|
<SiteLink className="relative" href={`/${username}`}>
|
||||||
|
<Row className="items-center gap-2">
|
||||||
|
<Avatar avatarUrl={avatarUrl} size={6} />
|
||||||
|
<div className="truncate">{name}</div>
|
||||||
|
</Row>
|
||||||
|
</SiteLink>
|
||||||
|
|
||||||
|
<div className="">•</div>
|
||||||
|
|
||||||
|
<div className="whitespace-nowrap">
|
||||||
|
<DateTimeTooltip text="" time={contract.createdTime}>
|
||||||
|
{createdDate}
|
||||||
|
</DateTimeTooltip>
|
||||||
|
</div>
|
||||||
|
<div className="">•</div>
|
||||||
|
<div className="text-base">#{number}</div>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{isBetting ? (
|
||||||
|
<AnswerBetPanel
|
||||||
|
answer={answer}
|
||||||
|
contract={contract}
|
||||||
|
closePanel={() => setIsBetting(false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Row className="self-end sm:self-start items-center gap-4">
|
||||||
|
{!wasResolvedTo && (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'text-2xl',
|
||||||
|
tradingAllowed(contract) ? 'text-green-500' : 'text-gray-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{probPercent}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showChoice ? (
|
||||||
|
<div className="form-control py-1">
|
||||||
|
<label className="cursor-pointer label gap-2">
|
||||||
|
<span className="label-text">Choose this answer</span>
|
||||||
|
<input
|
||||||
|
className={clsx('radio', isChosen && '!bg-green-500')}
|
||||||
|
type="radio"
|
||||||
|
name="opt"
|
||||||
|
checked={isChosen}
|
||||||
|
onChange={onChoose}
|
||||||
|
value={answer.id}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{tradingAllowed(contract) && (
|
||||||
|
<BuyButton
|
||||||
|
className="justify-end self-end flex-initial btn-md !px-8"
|
||||||
|
onClick={() => {
|
||||||
|
setIsBetting(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{wasResolvedTo && (
|
||||||
|
<Col className="items-end">
|
||||||
|
<div className="text-green-700 text-xl">Chosen</div>
|
||||||
|
<div className="text-2xl text-gray-500">{probPercent}</div>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnswerBetPanel(props: {
|
||||||
|
answer: Answer
|
||||||
|
contract: Contract
|
||||||
|
closePanel: () => void
|
||||||
|
}) {
|
||||||
|
const { answer, contract, closePanel } = props
|
||||||
|
const { id: answerId } = answer
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | undefined>()
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLElement>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
inputRef.current && inputRef.current.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function submitBet() {
|
||||||
|
if (!user || !betAmount) return
|
||||||
|
|
||||||
|
if (user.balance < betAmount) {
|
||||||
|
setError('Insufficient balance')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(undefined)
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
const result = await placeBet({
|
||||||
|
amount: betAmount,
|
||||||
|
outcome: answerId,
|
||||||
|
contractId: contract.id,
|
||||||
|
}).then((r) => r.data as any)
|
||||||
|
|
||||||
|
console.log('placed bet. Result:', result)
|
||||||
|
|
||||||
|
if (result?.status === 'success') {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
closePanel()
|
||||||
|
} else {
|
||||||
|
setError(result?.error || 'Error placing bet')
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const betDisabled = isSubmitting || !betAmount || error
|
||||||
|
|
||||||
|
const initialProb = getOutcomeProbability(contract.totalShares, answer.id)
|
||||||
|
|
||||||
|
const resultProb = getProbabilityAfterBet(
|
||||||
|
contract.totalShares,
|
||||||
|
answerId,
|
||||||
|
betAmount ?? 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const shares = calculateShares(contract.totalShares, betAmount ?? 0, answerId)
|
||||||
|
|
||||||
|
const currentPayout = betAmount
|
||||||
|
? calculatePayoutAfterCorrectBet(contract, {
|
||||||
|
outcome: answerId,
|
||||||
|
amount: betAmount,
|
||||||
|
shares,
|
||||||
|
} as Bet)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
||||||
|
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className="items-start px-2 pb-2 pt-4 sm:pt-0">
|
||||||
|
<Row className="self-stretch items-center justify-between">
|
||||||
|
<div className="text-xl">Buy this answer</div>
|
||||||
|
|
||||||
|
<button className="btn-ghost btn-circle" onClick={closePanel}>
|
||||||
|
<XIcon className="w-8 h-8 text-gray-500 mx-auto" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
<div className="my-3 text-left text-sm text-gray-500">Amount </div>
|
||||||
|
<AmountInput
|
||||||
|
inputClassName="w-full"
|
||||||
|
amount={betAmount}
|
||||||
|
onChange={setBetAmount}
|
||||||
|
error={error}
|
||||||
|
setError={setError}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
inputRef={inputRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
|
||||||
|
<Row>
|
||||||
|
<div>{formatPercent(initialProb)}</div>
|
||||||
|
<div className="mx-2">→</div>
|
||||||
|
<div>{formatPercent(resultProb)}</div>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
||||||
|
Payout if chosen
|
||||||
|
<InfoTooltip
|
||||||
|
text={`Current payout for ${formatWithCommas(
|
||||||
|
shares
|
||||||
|
)} / ${formatWithCommas(
|
||||||
|
shares + contract.totalShares[answerId]
|
||||||
|
)} shares`}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
{formatMoney(currentPayout)}
|
||||||
|
<span>(+{currentReturnPercent})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
{user ? (
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'btn',
|
||||||
|
betDisabled ? 'btn-disabled' : 'btn-primary',
|
||||||
|
isSubmitting ? 'loading' : ''
|
||||||
|
)}
|
||||||
|
onClick={betDisabled ? undefined : submitBet}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Submitting...' : 'Submit trade'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||||
|
onClick={firebaseLogin}
|
||||||
|
>
|
||||||
|
Sign in to trade!
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateAnswerInput(props: { contract: Contract }) {
|
||||||
|
const { contract } = props
|
||||||
|
const [text, setText] = useState('')
|
||||||
|
const [betAmount, setBetAmount] = useState<number | undefined>(10)
|
||||||
|
const [amountError, setAmountError] = useState<string | undefined>()
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const canSubmit = text && betAmount && !amountError && !isSubmitting
|
||||||
|
|
||||||
|
const submitAnswer = async () => {
|
||||||
|
if (canSubmit) {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
const result = await createAnswer({
|
||||||
|
contractId: contract.id,
|
||||||
|
text,
|
||||||
|
amount: betAmount,
|
||||||
|
}).then((r) => r.data)
|
||||||
|
|
||||||
|
setIsSubmitting(false)
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
setText('')
|
||||||
|
setBetAmount(10)
|
||||||
|
setAmountError(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultProb = getProbabilityAfterBet(
|
||||||
|
contract.totalShares,
|
||||||
|
'new',
|
||||||
|
betAmount ?? 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const shares = calculateShares(contract.totalShares, betAmount ?? 0, 'new')
|
||||||
|
|
||||||
|
const currentPayout = betAmount
|
||||||
|
? calculatePayoutAfterCorrectBet(contract, {
|
||||||
|
outcome: 'new',
|
||||||
|
amount: betAmount,
|
||||||
|
shares,
|
||||||
|
} as Bet)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
||||||
|
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className="gap-4 p-4 bg-gray-50 rounded">
|
||||||
|
<Col className="flex-1 gap-2">
|
||||||
|
<div className="mb-1">Add your answer</div>
|
||||||
|
<Textarea
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
className="textarea textarea-bordered w-full"
|
||||||
|
placeholder="Type your answer..."
|
||||||
|
rows={1}
|
||||||
|
maxLength={10000}
|
||||||
|
/>
|
||||||
|
<div />
|
||||||
|
<Col
|
||||||
|
className={clsx(
|
||||||
|
'sm:flex-row gap-4',
|
||||||
|
text ? 'justify-between' : 'self-end'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text && (
|
||||||
|
<>
|
||||||
|
<Col className="gap-2 mt-1">
|
||||||
|
<div className="text-gray-500 text-sm">
|
||||||
|
Ante (cannot be sold)
|
||||||
|
</div>
|
||||||
|
<AmountInput
|
||||||
|
amount={betAmount}
|
||||||
|
onChange={setBetAmount}
|
||||||
|
error={amountError}
|
||||||
|
setError={setAmountError}
|
||||||
|
minimumAmount={10}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col className="gap-2 mt-1">
|
||||||
|
<div className="text-sm text-gray-500">Implied probability</div>
|
||||||
|
<Row>
|
||||||
|
<div>{formatPercent(0)}</div>
|
||||||
|
<div className="mx-2">→</div>
|
||||||
|
<div>{formatPercent(resultProb)}</div>
|
||||||
|
</Row>
|
||||||
|
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
|
||||||
|
Payout if chosen
|
||||||
|
<InfoTooltip
|
||||||
|
text={`Current payout for ${formatWithCommas(
|
||||||
|
shares
|
||||||
|
)} / ${formatWithCommas(shares)} shares`}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
{formatMoney(currentPayout)}
|
||||||
|
<span>(+{currentReturnPercent})</span>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'btn self-end mt-2',
|
||||||
|
canSubmit ? 'btn-outline' : 'btn-disabled',
|
||||||
|
isSubmitting && 'loading'
|
||||||
|
)}
|
||||||
|
disabled={!canSubmit}
|
||||||
|
onClick={submitAnswer}
|
||||||
|
>
|
||||||
|
Submit answer & bet
|
||||||
|
</button>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnswerResolvePanel(props: {
|
||||||
|
contract: Contract
|
||||||
|
resolveOption: 'CHOOSE' | 'CANCEL' | undefined
|
||||||
|
setResolveOption: (option: 'CHOOSE' | 'CANCEL' | undefined) => void
|
||||||
|
answer: string | undefined
|
||||||
|
}) {
|
||||||
|
const { contract, resolveOption, setResolveOption, answer } = props
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
|
const onResolve = async () => {
|
||||||
|
if (resolveOption === 'CHOOSE' && answer === undefined) return
|
||||||
|
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
const result = await resolveMarket({
|
||||||
|
outcome: resolveOption === 'CHOOSE' ? (answer as string) : 'CANCEL',
|
||||||
|
contractId: contract.id,
|
||||||
|
}).then((r) => r.data as any)
|
||||||
|
|
||||||
|
console.log('resolved', `#${answer}`, 'result:', result)
|
||||||
|
|
||||||
|
if (result?.status !== 'success') {
|
||||||
|
setError(result?.error || 'Error resolving market')
|
||||||
|
}
|
||||||
|
setResolveOption(undefined)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolutionButtonClass =
|
||||||
|
resolveOption === 'CANCEL'
|
||||||
|
? 'bg-yellow-400 hover:bg-yellow-500'
|
||||||
|
: resolveOption === 'CHOOSE' && answer
|
||||||
|
? 'btn-primary'
|
||||||
|
: 'btn-disabled'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className="gap-4 p-4 bg-gray-50 rounded">
|
||||||
|
<div>Resolve your market</div>
|
||||||
|
<Col className="sm:flex-row sm:items-center gap-4">
|
||||||
|
<ChooseCancelSelector
|
||||||
|
className="sm:!flex-row sm:items-center"
|
||||||
|
selected={resolveOption}
|
||||||
|
onSelect={setResolveOption}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Row
|
||||||
|
className={clsx(
|
||||||
|
'flex-1 items-center',
|
||||||
|
resolveOption ? 'justify-between' : 'justify-end'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{resolveOption && (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setResolveOption(undefined)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<ResolveConfirmationButton
|
||||||
|
onResolve={onResolve}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
openModelButtonClass={resolutionButtonClass}
|
||||||
|
submitButtonClass={resolutionButtonClass}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{!!error && <div className="text-red-500">{error}</div>}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
|
@ -49,6 +49,7 @@ export function BetPanel(props: {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { contract, className, title, selected, onBetSuccess } = props
|
const { contract, className, title, selected, onBetSuccess } = props
|
||||||
|
const { totalShares, phantomShares } = contract
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
|
@ -108,11 +109,12 @@ export function BetPanel(props: {
|
||||||
|
|
||||||
const initialProb = getProbability(contract.totalShares)
|
const initialProb = getProbability(contract.totalShares)
|
||||||
|
|
||||||
const resultProb = getProbabilityAfterBet(
|
const outcomeProb = getProbabilityAfterBet(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
betChoice || 'YES',
|
betChoice || 'YES',
|
||||||
betAmount ?? 0
|
betAmount ?? 0
|
||||||
)
|
)
|
||||||
|
const resultProb = betChoice === 'NO' ? 1 - outcomeProb : outcomeProb
|
||||||
|
|
||||||
const shares = calculateShares(
|
const shares = calculateShares(
|
||||||
contract.totalShares,
|
contract.totalShares,
|
||||||
|
@ -179,8 +181,8 @@ export function BetPanel(props: {
|
||||||
shares
|
shares
|
||||||
)} / ${formatWithCommas(
|
)} / ${formatWithCommas(
|
||||||
shares +
|
shares +
|
||||||
contract.totalShares[betChoice] -
|
totalShares[betChoice] -
|
||||||
contract.phantomShares[betChoice]
|
(phantomShares ? phantomShares[betChoice] : 0)
|
||||||
)} ${betChoice} shares`}
|
)} ${betChoice} shares`}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -18,12 +18,11 @@ import {
|
||||||
Contract,
|
Contract,
|
||||||
getContractFromId,
|
getContractFromId,
|
||||||
contractPath,
|
contractPath,
|
||||||
contractMetrics,
|
getBinaryProbPercent,
|
||||||
} from '../lib/firebase/contracts'
|
} from '../lib/firebase/contracts'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { UserLink } from './user-page'
|
import { UserLink } from './user-page'
|
||||||
import {
|
import {
|
||||||
calculateCancelPayout,
|
|
||||||
calculatePayout,
|
calculatePayout,
|
||||||
calculateSaleAmount,
|
calculateSaleAmount,
|
||||||
getProbability,
|
getProbability,
|
||||||
|
@ -32,6 +31,7 @@ import {
|
||||||
import { sellBet } from '../lib/firebase/api-call'
|
import { sellBet } from '../lib/firebase/api-call'
|
||||||
import { ConfirmationButton } from './confirmation-button'
|
import { ConfirmationButton } from './confirmation-button'
|
||||||
import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label'
|
import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label'
|
||||||
|
import { filterDefined } from '../../common/util/array'
|
||||||
|
|
||||||
type BetSort = 'newest' | 'profit' | 'resolved' | 'value'
|
type BetSort = 'newest' | 'profit' | 'resolved' | 'value'
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ export function BetsList(props: { user: User }) {
|
||||||
let disposed = false
|
let disposed = false
|
||||||
Promise.all(contractIds.map((id) => getContractFromId(id))).then(
|
Promise.all(contractIds.map((id) => getContractFromId(id))).then(
|
||||||
(contracts) => {
|
(contracts) => {
|
||||||
if (!disposed) setContracts(contracts.filter(Boolean) as Contract[])
|
if (!disposed) setContracts(filterDefined(contracts))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -183,10 +183,13 @@ export function BetsList(props: { user: User }) {
|
||||||
|
|
||||||
function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
|
function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
|
||||||
const { bets, contract } = props
|
const { bets, contract } = props
|
||||||
const { resolution } = contract
|
const { resolution, outcomeType } = contract
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(true)
|
const [collapsed, setCollapsed] = useState(true)
|
||||||
const { probPercent } = contractMetrics(contract)
|
|
||||||
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
const probPercent = getBinaryProbPercent(contract)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
@ -216,6 +219,8 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row className="items-center gap-2 text-sm text-gray-500">
|
<Row className="items-center gap-2 text-sm text-gray-500">
|
||||||
|
{isBinary && (
|
||||||
|
<>
|
||||||
{resolution ? (
|
{resolution ? (
|
||||||
<div>
|
<div>
|
||||||
Resolved <OutcomeLabel outcome={resolution} />
|
Resolved <OutcomeLabel outcome={resolution} />
|
||||||
|
@ -224,6 +229,8 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
|
||||||
<div className="text-primary text-lg">{probPercent}</div>
|
<div className="text-primary text-lg">{probPercent}</div>
|
||||||
)}
|
)}
|
||||||
<div>•</div>
|
<div>•</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<UserLink
|
<UserLink
|
||||||
name={contract.creatorName}
|
name={contract.creatorName}
|
||||||
username={contract.creatorUsername}
|
username={contract.creatorUsername}
|
||||||
|
@ -266,8 +273,8 @@ export function MyBetsSummary(props: {
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { bets, contract, onlyMKT, className } = props
|
const { bets, contract, onlyMKT, className } = props
|
||||||
const { resolution } = contract
|
const { resolution, outcomeType } = contract
|
||||||
calculateCancelPayout
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
const excludeSales = bets.filter((b) => !b.isSold && !b.sale)
|
const excludeSales = bets.filter((b) => !b.isSold && !b.sale)
|
||||||
const betsTotal = _.sumBy(excludeSales, (bet) => bet.amount)
|
const betsTotal = _.sumBy(excludeSales, (bet) => bet.amount)
|
||||||
|
@ -362,10 +369,16 @@ export function MyBetsSummary(props: {
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{isBinary ? (
|
||||||
|
<>
|
||||||
Payout at{' '}
|
Payout at{' '}
|
||||||
<span className="text-blue-400">
|
<span className="text-blue-400">
|
||||||
{formatPercent(getProbability(contract.totalShares))}
|
{formatPercent(getProbability(contract.totalShares))}
|
||||||
</span>
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Current payout</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="whitespace-nowrap">
|
<div className="whitespace-nowrap">
|
||||||
{formatMoney(marketWinnings)}
|
{formatMoney(marketWinnings)}
|
||||||
|
|
|
@ -51,3 +51,36 @@ export function ConfirmationButton(props: {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ResolveConfirmationButton(props: {
|
||||||
|
onResolve: () => void
|
||||||
|
isSubmitting: boolean
|
||||||
|
openModelButtonClass?: string
|
||||||
|
submitButtonClass?: string
|
||||||
|
}) {
|
||||||
|
const { onResolve, isSubmitting, openModelButtonClass, submitButtonClass } =
|
||||||
|
props
|
||||||
|
return (
|
||||||
|
<ConfirmationButton
|
||||||
|
id="resolution-modal"
|
||||||
|
openModelBtn={{
|
||||||
|
className: clsx(
|
||||||
|
'border-none self-start',
|
||||||
|
openModelButtonClass,
|
||||||
|
isSubmitting && 'btn-disabled loading'
|
||||||
|
),
|
||||||
|
label: 'Resolve',
|
||||||
|
}}
|
||||||
|
cancelBtn={{
|
||||||
|
label: 'Back',
|
||||||
|
}}
|
||||||
|
submitBtn={{
|
||||||
|
label: 'Resolve',
|
||||||
|
className: clsx('border-none', submitButtonClass),
|
||||||
|
}}
|
||||||
|
onSubmit={onResolve}
|
||||||
|
>
|
||||||
|
<p>Are you sure you want to resolve this market?</p>
|
||||||
|
</ConfirmationButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Contract,
|
Contract,
|
||||||
contractMetrics,
|
contractMetrics,
|
||||||
contractPath,
|
contractPath,
|
||||||
|
getBinaryProbPercent,
|
||||||
} from '../lib/firebase/contracts'
|
} from '../lib/firebase/contracts'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
@ -24,8 +25,7 @@ export function ContractCard(props: {
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, showHotVolume, showCloseTime, className } = props
|
const { contract, showHotVolume, showCloseTime, className } = props
|
||||||
const { question, resolution } = contract
|
const { question } = contract
|
||||||
const { probPercent } = contractMetrics(contract)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -66,27 +66,29 @@ export function ResolutionOrChance(props: {
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, large, className } = props
|
const { contract, large, className } = props
|
||||||
const { resolution } = contract
|
const { resolution, outcomeType } = contract
|
||||||
const { probPercent } = contractMetrics(contract)
|
const isBinary = outcomeType === 'BINARY'
|
||||||
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
||||||
|
|
||||||
const resolutionColor = {
|
const resolutionColor =
|
||||||
|
{
|
||||||
YES: 'text-primary',
|
YES: 'text-primary',
|
||||||
NO: 'text-red-400',
|
NO: 'text-red-400',
|
||||||
MKT: 'text-blue-400',
|
MKT: 'text-blue-400',
|
||||||
CANCEL: 'text-yellow-400',
|
CANCEL: 'text-yellow-400',
|
||||||
'': '', // Empty if unresolved
|
'': '', // Empty if unresolved
|
||||||
}[resolution || '']
|
}[resolution || ''] ?? 'text-primary'
|
||||||
|
|
||||||
const probColor = marketClosed ? 'text-gray-400' : 'text-primary'
|
const probColor = marketClosed ? 'text-gray-400' : 'text-primary'
|
||||||
|
|
||||||
const resolutionText = {
|
const resolutionText =
|
||||||
|
{
|
||||||
YES: 'YES',
|
YES: 'YES',
|
||||||
NO: 'NO',
|
NO: 'NO',
|
||||||
MKT: probPercent,
|
MKT: getBinaryProbPercent(contract),
|
||||||
CANCEL: 'N/A',
|
CANCEL: 'N/A',
|
||||||
'': '',
|
'': '',
|
||||||
}[resolution || '']
|
}[resolution || ''] ?? `#${resolution}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}>
|
<Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}>
|
||||||
|
@ -100,12 +102,14 @@ export function ResolutionOrChance(props: {
|
||||||
<div className={resolutionColor}>{resolutionText}</div>
|
<div className={resolutionColor}>{resolutionText}</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
isBinary && (
|
||||||
<>
|
<>
|
||||||
<div className={probColor}>{probPercent}</div>
|
<div className={probColor}>{getBinaryProbPercent(contract)}</div>
|
||||||
<div className={clsx(probColor, large ? 'text-xl' : 'text-base')}>
|
<div className={clsx(probColor, large ? 'text-xl' : 'text-base')}>
|
||||||
chance
|
chance
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
// From https://tailwindui.com/components/application-ui/lists/feeds
|
// From https://tailwindui.com/components/application-ui/lists/feeds
|
||||||
import { useState } from 'react'
|
import { Fragment, useState } from 'react'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import {
|
import {
|
||||||
BanIcon,
|
BanIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
DotsVerticalIcon,
|
DotsVerticalIcon,
|
||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
StarIcon,
|
|
||||||
UserIcon,
|
UserIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@heroicons/react/solid'
|
} from '@heroicons/react/solid'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
|
||||||
import { OutcomeLabel } from './outcome-label'
|
import { OutcomeLabel } from './outcome-label'
|
||||||
import {
|
import {
|
||||||
|
@ -34,11 +34,9 @@ import { Col } from './layout/col'
|
||||||
import { UserLink } from './user-page'
|
import { UserLink } from './user-page'
|
||||||
import { DateTimeTooltip } from './datetime-tooltip'
|
import { DateTimeTooltip } from './datetime-tooltip'
|
||||||
import { useBets } from '../hooks/use-bets'
|
import { useBets } from '../hooks/use-bets'
|
||||||
import { Bet, withoutAnteBets } from '../lib/firebase/bets'
|
import { Bet } from '../lib/firebase/bets'
|
||||||
import { Comment, mapCommentsByBetId } from '../lib/firebase/comments'
|
import { Comment, mapCommentsByBetId } from '../lib/firebase/comments'
|
||||||
import { JoinSpans } from './join-spans'
|
import { JoinSpans } from './join-spans'
|
||||||
import Textarea from 'react-expanding-textarea'
|
|
||||||
import { outcome } from '../../common/contract'
|
|
||||||
import { fromNow } from '../lib/util/time'
|
import { fromNow } from '../lib/util/time'
|
||||||
import BetRow from './bet-row'
|
import BetRow from './bet-row'
|
||||||
import { parseTags } from '../../common/util/parse'
|
import { parseTags } from '../../common/util/parse'
|
||||||
|
@ -204,7 +202,7 @@ function EditContract(props: {
|
||||||
) : (
|
) : (
|
||||||
<Row>
|
<Row>
|
||||||
<button
|
<button
|
||||||
className="btn btn-neutral btn-outline btn-sm mt-4"
|
className="btn btn-neutral btn-outline btn-xs mt-4"
|
||||||
onClick={() => setEditing(true)}
|
onClick={() => setEditing(true)}
|
||||||
>
|
>
|
||||||
{props.buttonText}
|
{props.buttonText}
|
||||||
|
@ -302,9 +300,10 @@ function TruncatedComment(props: {
|
||||||
|
|
||||||
function FeedQuestion(props: { contract: Contract }) {
|
function FeedQuestion(props: { contract: Contract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
const { creatorName, creatorUsername, createdTime, question, resolution } =
|
const { creatorName, creatorUsername, question, resolution, outcomeType } =
|
||||||
contract
|
contract
|
||||||
const { probPercent, truePool } = contractMetrics(contract)
|
const { truePool } = contractMetrics(contract)
|
||||||
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
// Currently hidden on mobile; ideally we'd fit this in somewhere.
|
// Currently hidden on mobile; ideally we'd fit this in somewhere.
|
||||||
const closeMessage =
|
const closeMessage =
|
||||||
|
@ -340,7 +339,9 @@ function FeedQuestion(props: { contract: Contract }) {
|
||||||
>
|
>
|
||||||
{question}
|
{question}
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
|
{(isBinary || resolution) && (
|
||||||
<ResolutionOrChance className="items-center" contract={contract} />
|
<ResolutionOrChance className="items-center" contract={contract} />
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<TruncatedComment
|
<TruncatedComment
|
||||||
comment={contract.description}
|
comment={contract.description}
|
||||||
|
@ -379,7 +380,7 @@ function FeedDescription(props: { contract: Contract }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function OutcomeIcon(props: { outcome?: outcome }) {
|
function OutcomeIcon(props: { outcome?: string }) {
|
||||||
const { outcome } = props
|
const { outcome } = props
|
||||||
switch (outcome) {
|
switch (outcome) {
|
||||||
case 'YES':
|
case 'YES':
|
||||||
|
@ -387,8 +388,9 @@ function OutcomeIcon(props: { outcome?: outcome }) {
|
||||||
case 'NO':
|
case 'NO':
|
||||||
return <XIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
return <XIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||||
case 'CANCEL':
|
case 'CANCEL':
|
||||||
default:
|
|
||||||
return <BanIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
return <BanIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||||
|
default:
|
||||||
|
return <CheckIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -536,7 +538,7 @@ function groupBets(
|
||||||
return items as ActivityItem[]
|
return items as ActivityItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function BetGroupSpan(props: { bets: Bet[]; outcome: 'YES' | 'NO' }) {
|
function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
|
||||||
const { bets, outcome } = props
|
const { bets, outcome } = props
|
||||||
|
|
||||||
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
|
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
|
||||||
|
@ -562,7 +564,8 @@ function FeedBetGroup(props: { activityItem: any }) {
|
||||||
const { activityItem } = props
|
const { activityItem } = props
|
||||||
const bets: Bet[] = activityItem.bets
|
const bets: Bet[] = activityItem.bets
|
||||||
|
|
||||||
const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES')
|
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
|
||||||
|
const outcomes = Object.keys(betGroups)
|
||||||
|
|
||||||
// Use the time of the last bet for the entire group
|
// Use the time of the last bet for the entire group
|
||||||
const createdTime = bets[bets.length - 1].createdTime
|
const createdTime = bets[bets.length - 1].createdTime
|
||||||
|
@ -578,9 +581,12 @@ function FeedBetGroup(props: { activityItem: any }) {
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{yesBets.length > 0 && <BetGroupSpan outcome="YES" bets={yesBets} />}
|
{outcomes.map((outcome, index) => (
|
||||||
{yesBets.length > 0 && noBets.length > 0 && <br />}
|
<Fragment key={outcome}>
|
||||||
{noBets.length > 0 && <BetGroupSpan outcome="NO" bets={noBets} />}
|
<BetGroupSpan outcome={outcome} bets={betGroups[outcome]} />
|
||||||
|
{index !== outcomes.length - 1 && <br />}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
<Timestamp time={createdTime} />
|
<Timestamp time={createdTime} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -638,12 +644,16 @@ export function ContractFeed(props: {
|
||||||
betRowClassName?: string
|
betRowClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, feedType, betRowClassName } = props
|
const { contract, feedType, betRowClassName } = props
|
||||||
const { id } = contract
|
const { id, outcomeType } = contract
|
||||||
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
let bets = useBets(id) ?? props.bets
|
let bets = useBets(contract.id) ?? props.bets
|
||||||
bets = withoutAnteBets(contract, bets)
|
bets = isBinary
|
||||||
|
? bets.filter((bet) => !bet.isAnte)
|
||||||
|
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
||||||
|
|
||||||
const comments = useComments(id) ?? props.comments
|
const comments = useComments(id) ?? props.comments
|
||||||
|
|
||||||
|
@ -711,7 +721,7 @@ export function ContractFeed(props: {
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{tradingAllowed(contract) && (
|
{isBinary && tradingAllowed(contract) && (
|
||||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {
|
import {
|
||||||
contractMetrics,
|
|
||||||
Contract,
|
Contract,
|
||||||
deleteContract,
|
deleteContract,
|
||||||
contractPath,
|
contractPath,
|
||||||
tradingAllowed,
|
tradingAllowed,
|
||||||
|
getBinaryProbPercent,
|
||||||
} from '../lib/firebase/contracts'
|
} from '../lib/firebase/contracts'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
|
@ -28,40 +28,36 @@ export const ContractOverview = (props: {
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
folds: Fold[]
|
folds: Fold[]
|
||||||
|
children?: any
|
||||||
className?: string
|
className?: string
|
||||||
}) => {
|
}) => {
|
||||||
const { contract, bets, comments, folds, className } = props
|
const { contract, bets, comments, folds, children, className } = props
|
||||||
const { resolution, creatorId, creatorName } = contract
|
const { question, resolution, creatorId, outcomeType } = contract
|
||||||
const { probPercent, truePool } = contractMetrics(contract)
|
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const isCreator = user?.id === creatorId
|
const isCreator = user?.id === creatorId
|
||||||
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
const tweetQuestion = isCreator
|
const tweetText = getTweetText(contract, isCreator)
|
||||||
? contract.question
|
|
||||||
: `${creatorName}: ${contract.question}`
|
|
||||||
const tweetDescription = resolution
|
|
||||||
? `Resolved ${resolution}!`
|
|
||||||
: `Currently ${probPercent} chance, place your bets here:`
|
|
||||||
const url = `https://manifold.markets${contractPath(contract)}`
|
|
||||||
const tweetText = `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={clsx('mb-6', className)}>
|
<Col className={clsx('mb-6', className)}>
|
||||||
<Row className="justify-between gap-4 px-2">
|
<Row className="justify-between gap-4 px-2">
|
||||||
<Col className="gap-4">
|
<Col className="gap-4">
|
||||||
<div className="text-2xl text-indigo-700 md:text-3xl">
|
<div className="text-2xl text-indigo-700 md:text-3xl">
|
||||||
<Linkify text={contract.question} />
|
<Linkify text={question} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Row className="items-center justify-between gap-4">
|
<Row className="items-center justify-between gap-4">
|
||||||
|
{(isBinary || resolution) && (
|
||||||
<ResolutionOrChance
|
<ResolutionOrChance
|
||||||
className="md:hidden"
|
className="md:hidden"
|
||||||
contract={contract}
|
contract={contract}
|
||||||
large
|
large
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{tradingAllowed(contract) && (
|
{isBinary && tradingAllowed(contract) && (
|
||||||
<BetRow
|
<BetRow
|
||||||
contract={contract}
|
contract={contract}
|
||||||
className="md:hidden"
|
className="md:hidden"
|
||||||
|
@ -73,16 +69,24 @@ export const ContractOverview = (props: {
|
||||||
<ContractDetails contract={contract} />
|
<ContractDetails contract={contract} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
{(isBinary || resolution) && (
|
||||||
<Col className="hidden items-end justify-between md:flex">
|
<Col className="hidden items-end justify-between md:flex">
|
||||||
<ResolutionOrChance className="items-end" contract={contract} large />
|
<ResolutionOrChance
|
||||||
|
className="items-end"
|
||||||
|
contract={contract}
|
||||||
|
large
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
<ContractProbGraph contract={contract} bets={bets} />
|
{isBinary && <ContractProbGraph contract={contract} bets={bets} />}
|
||||||
|
|
||||||
<Row className="mt-6 ml-4 hidden items-center justify-between gap-4 sm:flex">
|
{children}
|
||||||
|
|
||||||
|
<Row className="mt-6 hidden items-center justify-between gap-4 sm:flex">
|
||||||
{folds.length === 0 ? (
|
{folds.length === 0 ? (
|
||||||
<TagsInput className={clsx('mx-4')} contract={contract} />
|
<TagsInput className={clsx('mx-4')} contract={contract} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -91,7 +95,7 @@ export const ContractOverview = (props: {
|
||||||
<TweetButton tweetText={tweetText} />
|
<TweetButton tweetText={tweetText} />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Col className="mt-6 ml-4 gap-4 sm:hidden">
|
<Col className="mt-6 gap-4 sm:hidden">
|
||||||
<TweetButton className="self-end" tweetText={tweetText} />
|
<TweetButton className="self-end" tweetText={tweetText} />
|
||||||
{folds.length === 0 ? (
|
{folds.length === 0 ? (
|
||||||
<TagsInput contract={contract} />
|
<TagsInput contract={contract} />
|
||||||
|
@ -101,15 +105,12 @@ export const ContractOverview = (props: {
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{folds.length > 0 && (
|
{folds.length > 0 && (
|
||||||
<RevealableTagsInput className="mx-4 mt-4" contract={contract} />
|
<RevealableTagsInput className="mt-4" contract={contract} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Spacer h={12} />
|
|
||||||
|
|
||||||
{/* Show a delete button for contracts without any trading */}
|
{/* Show a delete button for contracts without any trading */}
|
||||||
{isCreator && truePool === 0 && (
|
{isCreator && bets.length === 0 && (
|
||||||
<>
|
<>
|
||||||
<Spacer h={8} />
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-xs btn-error btn-outline mt-1 max-w-fit self-end"
|
className="btn btn-xs btn-error btn-outline mt-1 max-w-fit self-end"
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
|
@ -123,6 +124,8 @@ export const ContractOverview = (props: {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Spacer h={12} />
|
||||||
|
|
||||||
<ContractFeed
|
<ContractFeed
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets}
|
bets={bets}
|
||||||
|
@ -133,3 +136,22 @@ export const ContractOverview = (props: {
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTweetText = (contract: Contract, isCreator: boolean) => {
|
||||||
|
const { question, creatorName, resolution, outcomeType } = contract
|
||||||
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
|
const tweetQuestion = isCreator
|
||||||
|
? question
|
||||||
|
: `${question} Asked by ${creatorName}.`
|
||||||
|
const tweetDescription = resolution
|
||||||
|
? `Resolved ${resolution}!`
|
||||||
|
: isBinary
|
||||||
|
? `Currently ${getBinaryProbPercent(
|
||||||
|
contract
|
||||||
|
)} chance, place your bets here:`
|
||||||
|
: `Submit your own answer:`
|
||||||
|
const url = `https://manifold.markets${contractPath(contract)}`
|
||||||
|
|
||||||
|
return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,9 @@ export function ContractProbGraph(props: { contract: Contract; bets: Bet[] }) {
|
||||||
|
|
||||||
const bets = useBetsWithoutAntes(contract, props.bets)
|
const bets = useBetsWithoutAntes(contract, props.bets)
|
||||||
|
|
||||||
const startProb = getProbability(phantomShares)
|
const startProb = getProbability(
|
||||||
|
phantomShares as { [outcome: string]: number }
|
||||||
|
)
|
||||||
|
|
||||||
const times = bets
|
const times = bets
|
||||||
? [contract.createdTime, ...bets.map((bet) => bet.createdTime)].map(
|
? [contract.createdTime, ...bets.map((bet) => bet.createdTime)].map(
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
export function OutcomeLabel(props: {
|
export function OutcomeLabel(props: {
|
||||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT' | string
|
||||||
}) {
|
}) {
|
||||||
const { outcome } = props
|
const { outcome } = props
|
||||||
|
|
||||||
if (outcome === 'YES') return <YesLabel />
|
if (outcome === 'YES') return <YesLabel />
|
||||||
if (outcome === 'NO') return <NoLabel />
|
if (outcome === 'NO') return <NoLabel />
|
||||||
if (outcome === 'MKT') return <ProbLabel />
|
if (outcome === 'MKT') return <ProbLabel />
|
||||||
return <CancelLabel />
|
if (outcome === 'CANCEL') return <CancelLabel />
|
||||||
|
return <AnswerNumberLabel number={outcome} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function YesLabel() {
|
export function YesLabel() {
|
||||||
|
@ -24,3 +25,7 @@ export function CancelLabel() {
|
||||||
export function ProbLabel() {
|
export function ProbLabel() {
|
||||||
return <span className="text-blue-400">PROB</span>
|
return <span className="text-blue-400">PROB</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AnswerNumberLabel(props: { number: string }) {
|
||||||
|
return <span className="text-primary">#{props.number}</span>
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Title } from './title'
|
||||||
import { User } from '../lib/firebase/users'
|
import { User } from '../lib/firebase/users'
|
||||||
import { YesNoCancelSelector } from './yes-no-selector'
|
import { YesNoCancelSelector } from './yes-no-selector'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { ConfirmationButton as ConfirmationButton } from './confirmation-button'
|
import { ResolveConfirmationButton } from './confirmation-button'
|
||||||
import { resolveMarket } from '../lib/firebase/api-call'
|
import { resolveMarket } from '../lib/firebase/api-call'
|
||||||
import { ProbabilitySelector } from './probability-selector'
|
import { ProbabilitySelector } from './probability-selector'
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getProbability } from '../../common/calculate'
|
||||||
|
@ -20,7 +20,7 @@ export function ResolutionPanel(props: {
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// warm up cloud function
|
// warm up cloud function
|
||||||
resolveMarket({}).catch()
|
resolveMarket({} as any).catch()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { contract, className } = props
|
const { contract, className } = props
|
||||||
|
@ -35,6 +35,8 @@ export function ResolutionPanel(props: {
|
||||||
const [error, setError] = useState<string | undefined>(undefined)
|
const [error, setError] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
const resolve = async () => {
|
const resolve = async () => {
|
||||||
|
if (!outcome) return
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const result = await resolveMarket({
|
const result = await resolveMarket({
|
||||||
|
@ -114,27 +116,12 @@ export function ResolutionPanel(props: {
|
||||||
|
|
||||||
{!!error && <div className="text-red-500">{error}</div>}
|
{!!error && <div className="text-red-500">{error}</div>}
|
||||||
|
|
||||||
<ConfirmationButton
|
<ResolveConfirmationButton
|
||||||
id="resolution-modal"
|
onResolve={resolve}
|
||||||
openModelBtn={{
|
isSubmitting={isSubmitting}
|
||||||
className: clsx(
|
openModelButtonClass={clsx('w-full mt-2', submitButtonClass)}
|
||||||
'border-none self-start mt-2 w-full',
|
submitButtonClass={submitButtonClass}
|
||||||
submitButtonClass,
|
/>
|
||||||
isSubmitting && 'btn-disabled loading'
|
|
||||||
),
|
|
||||||
label: 'Resolve',
|
|
||||||
}}
|
|
||||||
cancelBtn={{
|
|
||||||
label: 'Back',
|
|
||||||
}}
|
|
||||||
submitBtn={{
|
|
||||||
label: 'Resolve',
|
|
||||||
className: submitButtonClass,
|
|
||||||
}}
|
|
||||||
onSubmit={resolve}
|
|
||||||
>
|
|
||||||
<p>Are you sure you want to resolve this market?</p>
|
|
||||||
</ConfirmationButton>
|
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,37 @@ export function YesNoCancelSelector(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ChooseCancelSelector(props: {
|
||||||
|
selected: 'CHOOSE' | 'CANCEL' | undefined
|
||||||
|
onSelect: (selected: 'CHOOSE' | 'CANCEL') => void
|
||||||
|
className?: string
|
||||||
|
btnClassName?: string
|
||||||
|
}) {
|
||||||
|
const { selected, onSelect, className } = props
|
||||||
|
|
||||||
|
const btnClassName = clsx('px-6 flex-1', props.btnClassName)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className={clsx('gap-2', className)}>
|
||||||
|
<Button
|
||||||
|
color={selected === 'CHOOSE' ? 'green' : 'gray'}
|
||||||
|
onClick={() => onSelect('CHOOSE')}
|
||||||
|
className={clsx('whitespace-nowrap', btnClassName)}
|
||||||
|
>
|
||||||
|
Choose an answer
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color={selected === 'CANCEL' ? 'yellow' : 'gray'}
|
||||||
|
onClick={() => onSelect('CANCEL')}
|
||||||
|
className={clsx(btnClassName, '')}
|
||||||
|
>
|
||||||
|
N/A
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const fundAmounts = [500, 1000, 2500, 10000]
|
const fundAmounts = [500, 1000, 2500, 10000]
|
||||||
|
|
||||||
export function FundsSelector(props: {
|
export function FundsSelector(props: {
|
||||||
|
@ -117,6 +148,15 @@ export function FundsSelector(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function BuyButton(props: { className?: string; onClick?: () => void }) {
|
||||||
|
const { className, onClick } = props
|
||||||
|
return (
|
||||||
|
<Button className={className} onClick={onClick} color="green">
|
||||||
|
BUY
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Button(props: {
|
function Button(props: {
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
|
@ -129,7 +169,7 @@ function Button(props: {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'inline-flex flex-1 items-center justify-center rounded-md border border-transparent px-8 py-3 text-sm font-medium shadow-sm',
|
'inline-flex flex-1 items-center justify-center rounded-md border border-transparent px-8 py-3 font-medium shadow-sm',
|
||||||
color === 'green' && 'btn-primary text-white',
|
color === 'green' && 'btn-primary text-white',
|
||||||
color === 'red' && 'bg-red-400 text-white hover:bg-red-500',
|
color === 'red' && 'bg-red-400 text-white hover:bg-red-500',
|
||||||
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
||||||
|
|
13
web/hooks/use-answers.ts
Normal file
13
web/hooks/use-answers.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Answer } from '../../common/answer'
|
||||||
|
import { listenForAnswers } from '../lib/firebase/answers'
|
||||||
|
|
||||||
|
export const useAnswers = (contractId: string) => {
|
||||||
|
const [answers, setAnswers] = useState<Answer[] | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (contractId) return listenForAnswers(contractId, setAnswers)
|
||||||
|
}, [contractId])
|
||||||
|
|
||||||
|
return answers
|
||||||
|
}
|
28
web/lib/firebase/answers.ts
Normal file
28
web/lib/firebase/answers.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { collection } from 'firebase/firestore'
|
||||||
|
|
||||||
|
import { getValues, listenForValues } from './utils'
|
||||||
|
import { db } from './init'
|
||||||
|
import { Answer } from '../../../common/answer'
|
||||||
|
|
||||||
|
function getAnswersCollection(contractId: string) {
|
||||||
|
return collection(db, 'contracts', contractId, 'answers')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAllAnswers(contractId: string) {
|
||||||
|
const answers = await getValues<Answer>(getAnswersCollection(contractId))
|
||||||
|
answers.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
return answers
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenForAnswers(
|
||||||
|
contractId: string,
|
||||||
|
setAnswers: (answers: Answer[]) => void
|
||||||
|
) {
|
||||||
|
return listenForValues<Answer>(
|
||||||
|
getAnswersCollection(contractId),
|
||||||
|
(answers) => {
|
||||||
|
answers.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
setAnswers(answers)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -18,7 +18,24 @@ export const createFold = cloudFunction<
|
||||||
|
|
||||||
export const placeBet = cloudFunction('placeBet')
|
export const placeBet = cloudFunction('placeBet')
|
||||||
|
|
||||||
export const resolveMarket = cloudFunction('resolveMarket')
|
export const createAnswer = cloudFunction<
|
||||||
|
{ contractId: string; text: string; amount: number },
|
||||||
|
{
|
||||||
|
status: 'error' | 'success'
|
||||||
|
message?: string
|
||||||
|
answerId?: string
|
||||||
|
betId?: string
|
||||||
|
}
|
||||||
|
>('createAnswer')
|
||||||
|
|
||||||
|
export const resolveMarket = cloudFunction<
|
||||||
|
{
|
||||||
|
outcome: string
|
||||||
|
contractId: string
|
||||||
|
probabilityInt?: number
|
||||||
|
},
|
||||||
|
{ status: 'error' | 'success'; message?: string }
|
||||||
|
>('resolveMarket')
|
||||||
|
|
||||||
export const sellBet = cloudFunction('sellBet')
|
export const sellBet = cloudFunction('sellBet')
|
||||||
|
|
||||||
|
|
|
@ -27,21 +27,9 @@ export function contractPath(contract: Contract) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function contractMetrics(contract: Contract) {
|
export function contractMetrics(contract: Contract) {
|
||||||
const {
|
const { pool, createdTime, resolutionTime, isResolved } = contract
|
||||||
pool,
|
|
||||||
phantomShares,
|
|
||||||
totalShares,
|
|
||||||
createdTime,
|
|
||||||
resolutionTime,
|
|
||||||
isResolved,
|
|
||||||
resolutionProbability,
|
|
||||||
} = contract
|
|
||||||
|
|
||||||
const truePool = pool.YES + pool.NO
|
const truePool = _.sum(Object.values(pool))
|
||||||
const prob = resolutionProbability ?? getProbability(totalShares)
|
|
||||||
const probPercent = Math.round(prob * 100) + '%'
|
|
||||||
|
|
||||||
const startProb = getProbability(phantomShares)
|
|
||||||
|
|
||||||
const createdDate = dayjs(createdTime).format('MMM D')
|
const createdDate = dayjs(createdTime).format('MMM D')
|
||||||
|
|
||||||
|
@ -49,7 +37,16 @@ export function contractMetrics(contract: Contract) {
|
||||||
? dayjs(resolutionTime).format('MMM D')
|
? dayjs(resolutionTime).format('MMM D')
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return { truePool, probPercent, startProb, createdDate, resolvedDate }
|
return { truePool, createdDate, resolvedDate }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBinaryProbPercent(contract: Contract) {
|
||||||
|
const { totalShares, resolutionProbability } = contract
|
||||||
|
|
||||||
|
const prob = resolutionProbability ?? getProbability(totalShares)
|
||||||
|
const probPercent = Math.round(prob * 100) + '%'
|
||||||
|
|
||||||
|
return probPercent
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tradingAllowed(contract: Contract) {
|
export function tradingAllowed(contract: Contract) {
|
||||||
|
|
|
@ -12,10 +12,10 @@ import { Title } from '../../components/title'
|
||||||
import { Spacer } from '../../components/layout/spacer'
|
import { Spacer } from '../../components/layout/spacer'
|
||||||
import { User } from '../../lib/firebase/users'
|
import { User } from '../../lib/firebase/users'
|
||||||
import {
|
import {
|
||||||
contractMetrics,
|
|
||||||
Contract,
|
Contract,
|
||||||
getContractFromSlug,
|
getContractFromSlug,
|
||||||
tradingAllowed,
|
tradingAllowed,
|
||||||
|
getBinaryProbPercent,
|
||||||
} from '../../lib/firebase/contracts'
|
} from '../../lib/firebase/contracts'
|
||||||
import { SEO } from '../../components/SEO'
|
import { SEO } from '../../components/SEO'
|
||||||
import { Page } from '../../components/page'
|
import { Page } from '../../components/page'
|
||||||
|
@ -26,6 +26,9 @@ import Custom404 from '../404'
|
||||||
import { getFoldsByTags } from '../../lib/firebase/folds'
|
import { getFoldsByTags } from '../../lib/firebase/folds'
|
||||||
import { Fold } from '../../../common/fold'
|
import { Fold } from '../../../common/fold'
|
||||||
import { useFoldsWithTags } from '../../hooks/use-fold'
|
import { useFoldsWithTags } from '../../hooks/use-fold'
|
||||||
|
import { listAllAnswers } from '../../lib/firebase/answers'
|
||||||
|
import { Answer } from '../../../common/answer'
|
||||||
|
import { AnswersPanel } from '../../components/answers-panel'
|
||||||
|
|
||||||
export async function getStaticProps(props: {
|
export async function getStaticProps(props: {
|
||||||
params: { username: string; contractSlug: string }
|
params: { username: string; contractSlug: string }
|
||||||
|
@ -36,9 +39,12 @@ export async function getStaticProps(props: {
|
||||||
|
|
||||||
const foldsPromise = getFoldsByTags(contract?.tags ?? [])
|
const foldsPromise = getFoldsByTags(contract?.tags ?? [])
|
||||||
|
|
||||||
const [bets, comments] = await Promise.all([
|
const [bets, comments, answers] = await Promise.all([
|
||||||
contractId ? listAllBets(contractId) : [],
|
contractId ? listAllBets(contractId) : [],
|
||||||
contractId ? listAllComments(contractId) : [],
|
contractId ? listAllComments(contractId) : [],
|
||||||
|
contractId && contract.outcomeType === 'FREE_RESPONSE'
|
||||||
|
? listAllAnswers(contractId)
|
||||||
|
: [],
|
||||||
])
|
])
|
||||||
|
|
||||||
const folds = await foldsPromise
|
const folds = await foldsPromise
|
||||||
|
@ -50,6 +56,7 @@ export async function getStaticProps(props: {
|
||||||
slug: contractSlug,
|
slug: contractSlug,
|
||||||
bets,
|
bets,
|
||||||
comments,
|
comments,
|
||||||
|
answers,
|
||||||
folds,
|
folds,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -66,6 +73,7 @@ export default function ContractPage(props: {
|
||||||
username: string
|
username: string
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
|
answers: Answer[]
|
||||||
slug: string
|
slug: string
|
||||||
folds: Fold[]
|
folds: Fold[]
|
||||||
}) {
|
}) {
|
||||||
|
@ -82,33 +90,27 @@ export default function ContractPage(props: {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
|
||||||
const { creatorId, isResolved, resolution, question } = contract
|
const { creatorId, isResolved, question, outcomeType } = contract
|
||||||
|
|
||||||
const isCreator = user?.id === creatorId
|
const isCreator = user?.id === creatorId
|
||||||
|
const isBinary = outcomeType === 'BINARY'
|
||||||
const allowTrade = tradingAllowed(contract)
|
const allowTrade = tradingAllowed(contract)
|
||||||
const allowResolve = !isResolved && isCreator && !!user
|
const allowResolve = !isResolved && isCreator && !!user
|
||||||
|
const hasSidePanel = isBinary && (allowTrade || allowResolve)
|
||||||
|
|
||||||
const { probPercent } = contractMetrics(contract)
|
// TODO(James): Create SEO props for non-binary contracts.
|
||||||
|
const ogCardProps = isBinary ? getOpenGraphProps(contract) : undefined
|
||||||
const description = resolution
|
|
||||||
? `Resolved ${resolution}. ${contract.description}`
|
|
||||||
: `${probPercent} chance. ${contract.description}`
|
|
||||||
|
|
||||||
const ogCardProps = {
|
|
||||||
question,
|
|
||||||
probability: probPercent,
|
|
||||||
metadata: contractTextDetails(contract),
|
|
||||||
creatorName: contract.creatorName,
|
|
||||||
creatorUsername: contract.creatorUsername,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page wide={allowTrade || allowResolve}>
|
<Page wide={hasSidePanel}>
|
||||||
|
{ogCardProps && (
|
||||||
<SEO
|
<SEO
|
||||||
title={question}
|
title={question}
|
||||||
description={description}
|
description={ogCardProps.description}
|
||||||
url={`/${props.username}/${props.slug}`}
|
url={`/${props.username}/${props.slug}`}
|
||||||
ogCardProps={ogCardProps}
|
ogCardProps={ogCardProps}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Col className="w-full justify-between md:flex-row">
|
<Col className="w-full justify-between md:flex-row">
|
||||||
<div className="flex-[3] rounded border-0 border-gray-100 bg-white px-2 py-6 md:px-6 md:py-8">
|
<div className="flex-[3] rounded border-0 border-gray-100 bg-white px-2 py-6 md:px-6 md:py-8">
|
||||||
|
@ -117,11 +119,24 @@ export default function ContractPage(props: {
|
||||||
bets={bets ?? []}
|
bets={bets ?? []}
|
||||||
comments={comments ?? []}
|
comments={comments ?? []}
|
||||||
folds={folds}
|
folds={folds}
|
||||||
|
>
|
||||||
|
{contract.outcomeType === 'FREE_RESPONSE' && (
|
||||||
|
<>
|
||||||
|
<Spacer h={4} />
|
||||||
|
<AnswersPanel
|
||||||
|
contract={contract as any}
|
||||||
|
answers={props.answers}
|
||||||
/>
|
/>
|
||||||
<BetsSection contract={contract} user={user ?? null} />
|
<Spacer h={4} />
|
||||||
|
<div className="divider before:bg-gray-300 after:bg-gray-300" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ContractOverview>
|
||||||
|
|
||||||
|
<BetsSection contract={contract} user={user ?? null} bets={bets} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(allowTrade || allowResolve) && (
|
{hasSidePanel && (
|
||||||
<>
|
<>
|
||||||
<div className="md:ml-6" />
|
<div className="md:ml-6" />
|
||||||
|
|
||||||
|
@ -140,11 +155,14 @@ export default function ContractPage(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BetsSection(props: { contract: Contract; user: User | null }) {
|
function BetsSection(props: {
|
||||||
|
contract: Contract
|
||||||
|
user: User | null
|
||||||
|
bets: Bet[]
|
||||||
|
}) {
|
||||||
const { contract, user } = props
|
const { contract, user } = props
|
||||||
const bets = useBets(contract.id)
|
const isBinary = contract.outcomeType === 'BINARY'
|
||||||
|
const bets = useBets(contract.id) ?? props.bets
|
||||||
if (!bets || bets.length === 0) return <></>
|
|
||||||
|
|
||||||
// Decending creation time.
|
// Decending creation time.
|
||||||
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
|
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
|
||||||
|
@ -156,10 +174,32 @@ function BetsSection(props: { contract: Contract; user: User | null }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title className="px-2" text="Your trades" />
|
<Title className="px-2" text="Your trades" />
|
||||||
|
{isBinary && (
|
||||||
|
<>
|
||||||
<MyBetsSummary className="px-2" contract={contract} bets={userBets} />
|
<MyBetsSummary className="px-2" contract={contract} bets={userBets} />
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<ContractBetsTable contract={contract} bets={userBets} />
|
<ContractBetsTable contract={contract} bets={userBets} />
|
||||||
<Spacer h={12} />
|
<Spacer h={12} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getOpenGraphProps = (contract: Contract) => {
|
||||||
|
const { resolution, question, creatorName, creatorUsername } = contract
|
||||||
|
const probPercent = getBinaryProbPercent(contract)
|
||||||
|
|
||||||
|
const description = resolution
|
||||||
|
? `Resolved ${resolution}. ${contract.description}`
|
||||||
|
: `${probPercent} chance. ${contract.description}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
question,
|
||||||
|
probability: probPercent,
|
||||||
|
metadata: contractTextDetails(contract),
|
||||||
|
creatorName: creatorName,
|
||||||
|
creatorUsername: creatorUsername,
|
||||||
|
description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ import { Title } from '../components/title'
|
||||||
import { ProbabilitySelector } from '../components/probability-selector'
|
import { ProbabilitySelector } from '../components/probability-selector'
|
||||||
import { parseWordsAsTags } from '../../common/util/parse'
|
import { parseWordsAsTags } from '../../common/util/parse'
|
||||||
import { TagsList } from '../components/tags-list'
|
import { TagsList } from '../components/tags-list'
|
||||||
|
import { Row } from '../components/layout/row'
|
||||||
|
import { outcomeType } from '../../common/contract'
|
||||||
|
|
||||||
export default function Create() {
|
export default function Create() {
|
||||||
const [question, setQuestion] = useState('')
|
const [question, setQuestion] = useState('')
|
||||||
|
@ -61,6 +63,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
createContract({}).catch() // warm up function
|
createContract({}).catch() // warm up function
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
||||||
const [initialProb, setInitialProb] = useState(50)
|
const [initialProb, setInitialProb] = useState(50)
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [tagText, setTagText] = useState<string>(tag ?? '')
|
const [tagText, setTagText] = useState<string>(tag ?? '')
|
||||||
|
@ -105,6 +108,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
|
|
||||||
const result: any = await createContract({
|
const result: any = await createContract({
|
||||||
question,
|
question,
|
||||||
|
outcomeType,
|
||||||
description,
|
description,
|
||||||
initialProb,
|
initialProb,
|
||||||
ante,
|
ante,
|
||||||
|
@ -120,14 +124,46 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
await router.push(contractPath(result.contract as Contract))
|
await router.push(contractPath(result.contract as Contract))
|
||||||
}
|
}
|
||||||
|
|
||||||
const descriptionPlaceholder = `e.g. This market resolves to "YES" if, two weeks after closing, the...`
|
const descriptionPlaceholder =
|
||||||
|
outcomeType === 'BINARY'
|
||||||
|
? `e.g. This market resolves to "YES" if, two weeks after closing, the...`
|
||||||
|
: `e.g. I will choose the answer according to...`
|
||||||
|
|
||||||
if (!creator) return <></>
|
if (!creator) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">Answer type</span>
|
||||||
|
</label>
|
||||||
|
<Row className="form-control gap-2">
|
||||||
|
<label className="cursor-pointer label gap-2">
|
||||||
|
<input
|
||||||
|
className="radio"
|
||||||
|
type="radio"
|
||||||
|
name="opt"
|
||||||
|
checked={outcomeType === 'BINARY'}
|
||||||
|
value="BINARY"
|
||||||
|
onChange={() => setOutcomeType('BINARY')}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Yes / No</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="cursor-pointer label gap-2">
|
||||||
|
<input
|
||||||
|
className="radio"
|
||||||
|
type="radio"
|
||||||
|
name="opt"
|
||||||
|
checked={outcomeType === 'FREE_RESPONSE'}
|
||||||
|
value="FREE_RESPONSE"
|
||||||
|
onChange={() => setOutcomeType('FREE_RESPONSE')}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Free response</span>
|
||||||
|
</label>
|
||||||
|
</Row>
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
{outcomeType === 'BINARY' && (
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="mb-1">Initial probability</span>
|
<span className="mb-1">Initial probability</span>
|
||||||
|
@ -138,6 +174,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
setProbabilityInt={setInitialProb}
|
setProbabilityInt={setInitialProb}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import dayjs from 'dayjs'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
import { getProbability } from '../../common/calculate'
|
||||||
import { parseWordsAsTags } from '../../common/util/parse'
|
import { parseWordsAsTags } from '../../common/util/parse'
|
||||||
import { AmountInput } from '../components/amount-input'
|
import { AmountInput } from '../components/amount-input'
|
||||||
import { InfoTooltip } from '../components/info-tooltip'
|
import { InfoTooltip } from '../components/info-tooltip'
|
||||||
|
@ -15,11 +16,7 @@ import { Page } from '../components/page'
|
||||||
import { Title } from '../components/title'
|
import { Title } from '../components/title'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { createContract } from '../lib/firebase/api-call'
|
import { createContract } from '../lib/firebase/api-call'
|
||||||
import {
|
import { Contract, contractPath } from '../lib/firebase/contracts'
|
||||||
contractMetrics,
|
|
||||||
Contract,
|
|
||||||
contractPath,
|
|
||||||
} from '../lib/firebase/contracts'
|
|
||||||
|
|
||||||
type Prediction = {
|
type Prediction = {
|
||||||
question: string
|
question: string
|
||||||
|
@ -29,7 +26,7 @@ type Prediction = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPrediction(contract: Contract): Prediction {
|
function toPrediction(contract: Contract): Prediction {
|
||||||
const { startProb } = contractMetrics(contract)
|
const startProb = getProbability(contract.totalShares)
|
||||||
return {
|
return {
|
||||||
question: contract.question,
|
question: contract.question,
|
||||||
description: contract.description,
|
description: contract.description,
|
||||||
|
|
BIN
web/public/logo-white-inside.png
Normal file
BIN
web/public/logo-white-inside.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 139 KiB |
Loading…
Reference in New Issue
Block a user