Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2a134b3c9a | ||
|
81f69fde21 | ||
|
e3e08f449c | ||
|
97c0a5cfa8 | ||
|
f73b13644f | ||
|
7e177c0ebf | ||
|
00717c90ef | ||
|
49f60cdb80 | ||
|
5690444135 | ||
|
2359b5b337 | ||
|
eec4e87da0 | ||
|
ac76094174 | ||
|
977268e9fc |
|
@ -1,3 +1,4 @@
|
|||
import { JSONContent } from '@tiptap/core'
|
||||
import { User } from './user'
|
||||
|
||||
export type Answer = {
|
||||
|
@ -12,6 +13,7 @@ export type Answer = {
|
|||
avatarUrl?: string
|
||||
|
||||
text: string
|
||||
description?: string | JSONContent
|
||||
}
|
||||
|
||||
export const getNoneAnswer = (contractId: string, creator: User) => {
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
FreeResponseContract,
|
||||
PseudoNumericContract,
|
||||
MultipleChoiceContract,
|
||||
AnswerContract,
|
||||
} from './contract'
|
||||
import { floatingEqual } from './util/math'
|
||||
|
||||
|
@ -201,9 +202,7 @@ export function getContractBetNullMetrics() {
|
|||
}
|
||||
}
|
||||
|
||||
export function getTopAnswer(
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
) {
|
||||
export function getTopAnswer(contract: AnswerContract) {
|
||||
const { answers } = contract
|
||||
const top = maxBy(
|
||||
answers?.map((answer) => ({
|
||||
|
|
|
@ -10,6 +10,7 @@ export type AnyOutcomeType =
|
|||
| PseudoNumeric
|
||||
| FreeResponse
|
||||
| Numeric
|
||||
| Bounty
|
||||
export type AnyContractType =
|
||||
| (CPMM & Binary)
|
||||
| (CPMM & PseudoNumeric)
|
||||
|
@ -17,6 +18,7 @@ export type AnyContractType =
|
|||
| (DPM & FreeResponse)
|
||||
| (DPM & Numeric)
|
||||
| (DPM & MultipleChoice)
|
||||
| (DPM & Bounty)
|
||||
|
||||
export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||
id: string
|
||||
|
@ -64,10 +66,15 @@ export type PseudoNumericContract = Contract & PseudoNumeric
|
|||
export type NumericContract = Contract & Numeric
|
||||
export type FreeResponseContract = Contract & FreeResponse
|
||||
export type MultipleChoiceContract = Contract & MultipleChoice
|
||||
export type BountyContract = Contract & Bounty
|
||||
export type DPMContract = Contract & DPM
|
||||
export type CPMMContract = Contract & CPMM
|
||||
export type DPMBinaryContract = BinaryContract & DPM
|
||||
export type CPMMBinaryContract = BinaryContract & CPMM
|
||||
export type AnswerContract =
|
||||
| FreeResponseContract
|
||||
| MultipleChoiceContract
|
||||
| BountyContract
|
||||
|
||||
export type DPM = {
|
||||
mechanism: 'dpm-2'
|
||||
|
@ -127,6 +134,21 @@ export type Numeric = {
|
|||
resolutionValue?: number
|
||||
}
|
||||
|
||||
export type Bounty = {
|
||||
outcomeType: 'BOUNTY'
|
||||
// One answer for each submission
|
||||
answers: Answer[]
|
||||
// Resolution = which answer id the bounty went to
|
||||
resolution?: string | 'MKT' | 'CANCEL'
|
||||
resolutions?: { [outcome: string]: number } // Used for MKT resolution
|
||||
|
||||
// Amount contributed to each bounty, and by who
|
||||
prizes: {
|
||||
[giverId: string]: number
|
||||
}
|
||||
totalPrizes: number
|
||||
}
|
||||
|
||||
export type outcomeType = AnyOutcomeType['outcomeType']
|
||||
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
||||
|
@ -136,6 +158,7 @@ export const OUTCOME_TYPES = [
|
|||
'FREE_RESPONSE',
|
||||
'PSEUDO_NUMERIC',
|
||||
'NUMERIC',
|
||||
'BOUNTY',
|
||||
] as const
|
||||
|
||||
export const MAX_QUESTION_LENGTH = 480
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
getCpmmProbability,
|
||||
} from './calculate-cpmm'
|
||||
import {
|
||||
AnswerContract,
|
||||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
FreeResponseContract,
|
||||
|
@ -323,7 +324,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
export const getNewMultiBetInfo = (
|
||||
outcome: string,
|
||||
amount: number,
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
contract: AnswerContract,
|
||||
loanAmount: number
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { range } from 'lodash'
|
||||
import {
|
||||
Binary,
|
||||
Bounty,
|
||||
Contract,
|
||||
CPMM,
|
||||
DPM,
|
||||
|
@ -45,16 +46,22 @@ export function getNewContract(
|
|||
)
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||
|
||||
const propsByOutcomeType =
|
||||
outcomeType === 'BINARY'
|
||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||
: outcomeType === 'PSEUDO_NUMERIC'
|
||||
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
|
||||
: outcomeType === 'NUMERIC'
|
||||
? getNumericProps(ante, bucketCount, min, max)
|
||||
: outcomeType === 'MULTIPLE_CHOICE'
|
||||
? getMultipleChoiceProps(ante, answers)
|
||||
: getFreeAnswerProps(ante)
|
||||
const PROPS = {
|
||||
BINARY: getBinaryCpmmProps(initialProb, ante), // getBinaryDpmProps(initialProb, ante)
|
||||
PSEUDO_NUMERIC: getPseudoNumericCpmmProps(
|
||||
initialProb,
|
||||
ante,
|
||||
min,
|
||||
max,
|
||||
isLogScale
|
||||
),
|
||||
FREE_RESPONSE: getFreeAnswerProps(ante),
|
||||
MULTIPLE_CHOICE: getMultipleChoiceProps(ante, answers),
|
||||
NUMERIC: getNumericProps(ante, bucketCount, min, max),
|
||||
BOUNTY: getBountyProps(ante, creator),
|
||||
}
|
||||
|
||||
const propsByOutcomeType = PROPS[outcomeType] || PROPS['FREE_RESPONSE']
|
||||
|
||||
const contract: Contract = removeUndefinedProps({
|
||||
id,
|
||||
|
@ -157,6 +164,18 @@ const getFreeAnswerProps = (ante: number) => {
|
|||
return system
|
||||
}
|
||||
|
||||
function getBountyProps(ante: number, creator: User) {
|
||||
const system: DPM & Bounty = {
|
||||
...getFreeAnswerProps(ante),
|
||||
outcomeType: 'BOUNTY',
|
||||
prizes: {
|
||||
[creator.id]: ante,
|
||||
},
|
||||
totalPrizes: ante,
|
||||
}
|
||||
return system
|
||||
}
|
||||
|
||||
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
|
||||
const numAnswers = answers.length
|
||||
const betAnte = ante / numAnswers
|
||||
|
|
|
@ -3,6 +3,7 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
|
|||
import { Bet, NumericBet } from './bet'
|
||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
||||
import {
|
||||
AnswerContract,
|
||||
DPMContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
|
@ -184,7 +185,7 @@ export const getDpmMktPayouts = (
|
|||
|
||||
export const getPayoutsMultiOutcome = (
|
||||
resolutions: { [outcome: string]: number },
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
contract: AnswerContract,
|
||||
bets: Bet[]
|
||||
) => {
|
||||
const poolTotal = sum(Object.values(contract.pool))
|
||||
|
|
|
@ -7,5 +7,11 @@
|
|||
"firestore": {
|
||||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json"
|
||||
},
|
||||
"emulators": {
|
||||
"firestore": {
|
||||
"port": 8080,
|
||||
"host": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
55
functions/src/add-bounty.ts
Normal file
55
functions/src/add-bounty.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Contract } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
const bodySchema = z.object({
|
||||
contractId: z.string(),
|
||||
amount: z.number().gt(0),
|
||||
})
|
||||
|
||||
export const addbounty = newEndpoint({}, async (req, auth) => {
|
||||
const { amount, contractId } = validate(bodySchema, req.body)
|
||||
|
||||
return await firestore.runTransaction(async (transaction) => {
|
||||
const userDoc = firestore.doc(`users/${auth.uid}`)
|
||||
const userSnap = await transaction.get(userDoc)
|
||||
if (!userSnap.exists) throw new APIError(400, 'User not found')
|
||||
const user = userSnap.data() as User
|
||||
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
const contractSnap = await transaction.get(contractDoc)
|
||||
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
||||
const contract = contractSnap.data() as Contract
|
||||
if (contract.outcomeType !== 'BOUNTY')
|
||||
throw new APIError(400, "Can't add bounties to non-BOUNTY contracts")
|
||||
|
||||
const { closeTime } = contract
|
||||
if (closeTime && Date.now() > closeTime)
|
||||
throw new APIError(400, 'Contract is closed')
|
||||
|
||||
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
|
||||
|
||||
// TODO: Capture with txns?
|
||||
transaction.update(userDoc, {
|
||||
balance: user.balance - amount,
|
||||
totalDeposits: user.totalDeposits - amount,
|
||||
})
|
||||
|
||||
const existingPrize = contract.prizes[user.id] ?? 0
|
||||
|
||||
transaction.update(contractDoc, {
|
||||
prizes: {
|
||||
...contract.prizes,
|
||||
[user.id]: existingPrize + amount,
|
||||
},
|
||||
totalPrizes: contract.totalPrizes + amount,
|
||||
})
|
||||
|
||||
return { status: 'success' }
|
||||
})
|
||||
})
|
|
@ -36,7 +36,10 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
|
|||
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
||||
const contract = contractSnap.data() as Contract
|
||||
|
||||
if (contract.outcomeType !== 'FREE_RESPONSE')
|
||||
if (
|
||||
contract.outcomeType !== 'FREE_RESPONSE' &&
|
||||
contract.outcomeType !== 'BOUNTY'
|
||||
)
|
||||
throw new APIError(400, 'Requires a free response contract')
|
||||
|
||||
const { closeTime, volume } = contract
|
||||
|
|
|
@ -234,7 +234,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
)
|
||||
)
|
||||
await contractRef.update({ answers: answerObjects })
|
||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||
} else if (outcomeType === 'FREE_RESPONSE' || outcomeType === 'BOUNTY') {
|
||||
const noneAnswerDoc = firestore
|
||||
.collection(`contracts/${contract.id}/answers`)
|
||||
.doc('0')
|
||||
|
|
|
@ -66,6 +66,7 @@ import { stripewebhook, createcheckoutsession } from './stripe'
|
|||
import { getcurrentuser } from './get-current-user'
|
||||
import { acceptchallenge } from './accept-challenge'
|
||||
import { getcustomtoken } from './get-custom-token'
|
||||
import { addbounty } from './add-bounty'
|
||||
|
||||
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
||||
return onRequest(opts, handler as any)
|
||||
|
@ -91,6 +92,7 @@ const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
|
|||
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
||||
const acceptChallenge = toCloudFunction(acceptchallenge)
|
||||
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
||||
const addBountyFunction = toCloudFunction(addbounty)
|
||||
|
||||
export {
|
||||
healthFunction as health,
|
||||
|
@ -114,4 +116,5 @@ export {
|
|||
getCurrentUserFunction as getcurrentuser,
|
||||
acceptChallenge as acceptchallenge,
|
||||
getCustomTokenFunction as getcustomtoken,
|
||||
addBountyFunction as addbounty,
|
||||
}
|
||||
|
|
|
@ -2,12 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import { z } from 'zod'
|
||||
import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
||||
|
||||
import {
|
||||
Contract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
RESOLUTIONS,
|
||||
} from '../../common/contract'
|
||||
import { AnswerContract, Contract, RESOLUTIONS } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { getUser, isProd, payUser } from './utils'
|
||||
|
@ -295,10 +290,7 @@ function getResolutionParams(contract: Contract, body: string) {
|
|||
throw new APIError(500, `Invalid outcome type: ${outcomeType}`)
|
||||
}
|
||||
|
||||
function validateAnswer(
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
answer: number
|
||||
) {
|
||||
function validateAnswer(contract: AnswerContract, answer: number) {
|
||||
const validIds = contract.answers.map((a) => a.id)
|
||||
if (!validIds.includes(answer.toString())) {
|
||||
throw new APIError(400, `${answer} is not a valid answer ID`)
|
||||
|
|
|
@ -27,6 +27,7 @@ import { unsubscribe } from './unsubscribe'
|
|||
import { stripewebhook, createcheckoutsession } from './stripe'
|
||||
import { getcurrentuser } from './get-current-user'
|
||||
import { getcustomtoken } from './get-custom-token'
|
||||
import { addbounty } from './add-bounty'
|
||||
|
||||
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
||||
const app = express()
|
||||
|
@ -65,6 +66,7 @@ addJsonEndpointRoute('/resolvemarket', resolvemarket)
|
|||
addJsonEndpointRoute('/unsubscribe', unsubscribe)
|
||||
addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession)
|
||||
addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
|
||||
addJsonEndpointRoute('/addbounty', addbounty)
|
||||
addEndpointRoute('/getcustomtoken', getcustomtoken)
|
||||
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ To run the development server, install [Yarn 1.x][yarn], and then in this direct
|
|||
3. Your site will be available on http://localhost:3000
|
||||
|
||||
Check package.json for other command-line tasks. (e.g. `yarn dev` will point the development server at the prod
|
||||
database. `yarn emulate` will run against a local emulated database, if you are serving it via `yarn serve` from the
|
||||
database. `yarn dev:emulate` will run against a local emulated database, if you are serving it via `yarn serve` from the
|
||||
[`functions/` package][functions-readme].)
|
||||
|
||||
## Tech stack
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||
import { XIcon } from '@heroicons/react/solid'
|
||||
|
||||
import { Answer } from 'common/answer'
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import { AnswerContract } from 'common/contract'
|
||||
import { BuyAmountInput } from '../amount-input'
|
||||
import { Col } from '../layout/col'
|
||||
import { APIError, placeBet } from 'web/lib/firebase/api'
|
||||
|
@ -30,7 +30,7 @@ import { AlertBox } from '../alert-box'
|
|||
|
||||
export function AnswerBetPanel(props: {
|
||||
answer: Answer
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
contract: AnswerContract
|
||||
closePanel: () => void
|
||||
className?: string
|
||||
isModal?: boolean
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import clsx from 'clsx'
|
||||
|
||||
import { Answer } from 'common/answer'
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import { AnswerContract } from 'common/contract'
|
||||
import { Col } from '../layout/col'
|
||||
import { Row } from '../layout/row'
|
||||
import { Avatar } from '../avatar'
|
||||
|
@ -13,7 +13,7 @@ import { Linkify } from '../linkify'
|
|||
|
||||
export function AnswerItem(props: {
|
||||
answer: Answer
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
contract: AnswerContract
|
||||
showChoice: 'radio' | 'checkbox' | undefined
|
||||
chosenProb: number | undefined
|
||||
totalChosenProb?: number
|
||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx'
|
|||
import { sum } from 'lodash'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import { AnswerContract } from 'common/contract'
|
||||
import { Col } from '../layout/col'
|
||||
import { APIError, resolveMarket } from 'web/lib/firebase/api'
|
||||
import { Row } from '../layout/row'
|
||||
|
@ -11,7 +11,7 @@ import { ResolveConfirmationButton } from '../confirmation-button'
|
|||
import { removeUndefinedProps } from 'common/util/object'
|
||||
|
||||
export function AnswerResolvePanel(props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
contract: AnswerContract
|
||||
resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||
setResolveOption: (
|
||||
option: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||
|
|
|
@ -5,14 +5,18 @@ import { groupBy, sortBy, sumBy } from 'lodash'
|
|||
import { memo } from 'react'
|
||||
|
||||
import { Bet } from 'common/bet'
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import {
|
||||
AnswerContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
} from 'common/contract'
|
||||
import { getOutcomeProbability } from 'common/calculate'
|
||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||
|
||||
const NUM_LINES = 6
|
||||
|
||||
export const AnswersGraph = memo(function AnswersGraph(props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
contract: AnswerContract
|
||||
bets: Bet[]
|
||||
height?: number
|
||||
}) {
|
||||
|
@ -178,10 +182,7 @@ function formatTime(
|
|||
return d.format(format)
|
||||
}
|
||||
|
||||
const computeProbsByOutcome = (
|
||||
bets: Bet[],
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
) => {
|
||||
const computeProbsByOutcome = (bets: Bet[], contract: AnswerContract) => {
|
||||
const { totalBets, outcomeType } = contract
|
||||
|
||||
const betsByOutcome = groupBy(bets, (bet) => bet.outcome)
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { sortBy, partition, sum, uniq } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import {
|
||||
AnswerContract,
|
||||
BountyContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
} from 'common/contract'
|
||||
import { Col } from '../layout/col'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
||||
|
@ -25,9 +30,7 @@ import { UserLink } from 'web/components/user-page'
|
|||
import { Linkify } from 'web/components/linkify'
|
||||
import { BuyButton } from 'web/components/yes-no-selector'
|
||||
|
||||
export function AnswersPanel(props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
}) {
|
||||
export function AnswersPanel(props: { contract: AnswerContract }) {
|
||||
const { contract } = props
|
||||
const { creatorId, resolution, resolutions, totalBets, outcomeType } =
|
||||
contract
|
||||
|
@ -136,7 +139,7 @@ export function AnswersPanel(props: {
|
|||
<div className="pb-4 text-gray-500">No answers yet...</div>
|
||||
)}
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' &&
|
||||
{(outcomeType === 'FREE_RESPONSE' || outcomeType === 'BOUNTY') &&
|
||||
tradingAllowed(contract) &&
|
||||
(!resolveOption || resolveOption === 'CANCEL') && (
|
||||
<CreateAnswerPanel contract={contract} />
|
||||
|
@ -158,7 +161,7 @@ export function AnswersPanel(props: {
|
|||
}
|
||||
|
||||
function getAnswerItems(
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
contract: AnswerContract,
|
||||
answers: Answer[],
|
||||
user: User | undefined | null
|
||||
) {
|
||||
|
@ -184,7 +187,7 @@ function getAnswerItems(
|
|||
}
|
||||
|
||||
function OpenAnswer(props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
contract: AnswerContract
|
||||
answer: Answer
|
||||
items: ActivityItem[]
|
||||
type: string
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useState } from 'react'
|
|||
import Textarea from 'react-expanding-textarea'
|
||||
import { findBestMatch } from 'string-similarity'
|
||||
|
||||
import { FreeResponseContract } from 'common/contract'
|
||||
import { BountyContract, FreeResponseContract } from 'common/contract'
|
||||
import { BuyAmountInput } from '../amount-input'
|
||||
import { Col } from '../layout/col'
|
||||
import { APIError, createAnswer } from 'web/lib/firebase/api'
|
||||
|
@ -26,7 +26,9 @@ import { MAX_ANSWER_LENGTH } from 'common/answer'
|
|||
import { withTracking } from 'web/lib/service/analytics'
|
||||
import { lowerCase } from 'lodash'
|
||||
|
||||
export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
||||
export function CreateAnswerPanel(props: {
|
||||
contract: FreeResponseContract | BountyContract
|
||||
}) {
|
||||
const { contract } = props
|
||||
const user = useUser()
|
||||
const [text, setText] = useState('')
|
||||
|
|
73
web/components/bounty/bounty-box.tsx
Normal file
73
web/components/bounty/bounty-box.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import clsx from 'clsx'
|
||||
import { BountyContract } from 'common/contract'
|
||||
import { User } from 'common/user'
|
||||
import { useState } from 'react'
|
||||
import { addBounty } from 'web/lib/firebase/api'
|
||||
import { BuyAmountInput } from '../amount-input'
|
||||
import { Spacer } from '../layout/spacer'
|
||||
import { Title } from '../title'
|
||||
|
||||
export function BountyBox(props: {
|
||||
className?: string
|
||||
user?: User | null
|
||||
contract: BountyContract
|
||||
}) {
|
||||
const { className, user, contract } = props
|
||||
const [amount, setAmount] = useState<number | undefined>()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
|
||||
const donateDisabled = isSubmitting || !amount || error
|
||||
|
||||
const onSubmit: React.FormEventHandler = async (e) => {
|
||||
if (!user || donateDisabled) return
|
||||
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
setError(undefined)
|
||||
|
||||
await addBounty({
|
||||
amount,
|
||||
contractId: contract.id,
|
||||
})
|
||||
|
||||
setIsSubmitting(false)
|
||||
setAmount(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(className, 'rounded-lg bg-white py-6 px-8 shadow-lg')}>
|
||||
<Title text="Add to bounty" className="!mt-0" />
|
||||
<form onSubmit={onSubmit}>
|
||||
<label
|
||||
className="mb-2 block text-sm text-gray-500"
|
||||
htmlFor="donate-input"
|
||||
>
|
||||
Contribute
|
||||
</label>
|
||||
<BuyAmountInput
|
||||
inputClassName="w-full max-w-none donate-input"
|
||||
amount={amount}
|
||||
onChange={setAmount}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
|
||||
<Spacer h={8} />
|
||||
|
||||
{user && (
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
'btn w-full',
|
||||
donateDisabled ? 'btn-disabled' : 'btn-primary',
|
||||
isSubmitting && 'loading'
|
||||
)}
|
||||
>
|
||||
Add to bounty
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,11 +1,17 @@
|
|||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import { Row } from '../layout/row'
|
||||
import { formatLargeNumber, formatPercent } from 'common/util/format'
|
||||
import {
|
||||
formatLargeNumber,
|
||||
formatMoney,
|
||||
formatPercent,
|
||||
} from 'common/util/format'
|
||||
import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
||||
import { Col } from '../layout/col'
|
||||
import {
|
||||
AnswerContract,
|
||||
BinaryContract,
|
||||
BountyContract,
|
||||
Contract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
|
@ -169,6 +175,13 @@ export function ContractCard(props: {
|
|||
truncate="long"
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'BOUNTY' && (
|
||||
<BountyValue
|
||||
className="items-center self-center pr-5"
|
||||
contract={contract}
|
||||
/>
|
||||
)}
|
||||
<ProbBar contract={contract} />
|
||||
</>
|
||||
)}
|
||||
|
@ -211,8 +224,25 @@ export function BinaryResolutionOrChance(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function BountyValue(props: {
|
||||
contract: BountyContract
|
||||
large?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, large, className } = props
|
||||
const textColor = `text-${getColor(contract)}`
|
||||
return (
|
||||
<Col className={clsx(large ? 'text-3xl' : 'text-2xl', className)}>
|
||||
<div className={textColor}>{formatMoney(contract.totalPrizes)}</div>
|
||||
<div className={clsx(textColor, large ? 'text-xl' : 'text-base')}>
|
||||
bounty
|
||||
</div>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
function FreeResponseTopAnswer(props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
contract: AnswerContract
|
||||
truncate: 'short' | 'long' | 'none'
|
||||
className?: string
|
||||
}) {
|
||||
|
|
|
@ -32,14 +32,15 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
'userId'
|
||||
).length
|
||||
|
||||
const typeDisplay =
|
||||
outcomeType === 'BINARY'
|
||||
? 'YES / NO'
|
||||
: outcomeType === 'FREE_RESPONSE'
|
||||
? 'Free response'
|
||||
: outcomeType === 'MULTIPLE_CHOICE'
|
||||
? 'Multiple choice'
|
||||
: 'Numeric'
|
||||
const TYPES = {
|
||||
BINARY: 'YES / NO',
|
||||
FREE_RESPONSE: 'Free Response',
|
||||
MULTIPLE_CHOICE: 'Multiple Choice',
|
||||
NUMERIC: 'Numeric (deprecated)',
|
||||
PSEUDO_NUMERIC: 'Numeric',
|
||||
BOUNTY: 'Bounty',
|
||||
}
|
||||
const typeDisplay = TYPES[outcomeType] || 'Unknown'
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Row } from '../layout/row'
|
|||
import { Linkify } from '../linkify'
|
||||
import {
|
||||
BinaryResolutionOrChance,
|
||||
BountyValue,
|
||||
FreeResponseResolutionOrChance,
|
||||
NumericResolutionOrExpectation,
|
||||
PseudoNumericResolutionOrExpectation,
|
||||
|
@ -44,7 +45,6 @@ export const ContractOverview = (props: {
|
|||
<div className="text-2xl text-indigo-700 md:text-3xl">
|
||||
<Linkify text={question} />
|
||||
</div>
|
||||
|
||||
{isBinary && (
|
||||
<BinaryResolutionOrChance
|
||||
className="hidden items-end xl:flex"
|
||||
|
@ -52,20 +52,24 @@ export const ContractOverview = (props: {
|
|||
large
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPseudoNumeric && (
|
||||
<PseudoNumericResolutionOrExpectation
|
||||
contract={contract}
|
||||
className="hidden items-end xl:flex"
|
||||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'NUMERIC' && (
|
||||
<NumericResolutionOrExpectation
|
||||
contract={contract}
|
||||
className="hidden items-end xl:flex"
|
||||
/>
|
||||
)}
|
||||
{outcomeType === 'BOUNTY' && (
|
||||
<BountyValue
|
||||
contract={contract}
|
||||
className="hidden items-end xl:flex"
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
{isBinary ? (
|
||||
|
|
|
@ -56,7 +56,8 @@ export function ContractTabs(props: {
|
|||
tips={tips}
|
||||
user={user}
|
||||
mode={
|
||||
contract.outcomeType === 'FREE_RESPONSE'
|
||||
contract.outcomeType === 'FREE_RESPONSE' ||
|
||||
contract.outcomeType === 'BOUNTY'
|
||||
? 'free-response-comment-answer-groups'
|
||||
: 'comments'
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ import { Answer } from 'common/answer'
|
|||
import { getProbability } from 'common/calculate'
|
||||
import { getValueFromBucket } from 'common/calculate-dpm'
|
||||
import {
|
||||
AnswerContract,
|
||||
BinaryContract,
|
||||
BountyContract,
|
||||
Contract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
|
@ -77,7 +79,7 @@ export function BinaryContractOutcomeLabel(props: {
|
|||
}
|
||||
|
||||
export function FreeResponseOutcomeLabel(props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
contract: AnswerContract
|
||||
resolution: string | 'CANCEL' | 'MKT'
|
||||
truncate: 'short' | 'long' | 'none'
|
||||
answerClassName?: string
|
||||
|
|
|
@ -88,3 +88,7 @@ export function acceptChallenge(params: any) {
|
|||
export function getCurrentUser(params: any) {
|
||||
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
|
||||
}
|
||||
|
||||
export function addBounty(params: any) {
|
||||
return call(getFunctionUrl('addbounty'), 'POST', params)
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ import { listUsers } from 'web/lib/firebase/users'
|
|||
import { FeedComment } from 'web/components/feed/feed-comments'
|
||||
import { Title } from 'web/components/title'
|
||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
||||
import { BountyBox } from 'web/components/bounty/bounty-box'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: {
|
||||
|
@ -116,30 +117,38 @@ export function ContractPageSidebar(props: {
|
|||
const isBinary = outcomeType === 'BINARY'
|
||||
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
|
||||
const isNumeric = outcomeType === 'NUMERIC'
|
||||
const isBounty = outcomeType === 'BOUNTY'
|
||||
const allowTrade = tradingAllowed(contract)
|
||||
const allowResolve = !isResolved && isCreator && !!user
|
||||
const hasSidePanel =
|
||||
(isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve)
|
||||
(isBinary || isNumeric || isPseudoNumeric || isBounty) &&
|
||||
(allowTrade || allowResolve)
|
||||
if (!hasSidePanel) {
|
||||
return null
|
||||
}
|
||||
|
||||
return hasSidePanel ? (
|
||||
return (
|
||||
<Col className="gap-4">
|
||||
{allowTrade &&
|
||||
(isNumeric ? (
|
||||
<NumericBetPanel className="hidden xl:flex" contract={contract} />
|
||||
) : isBounty ? (
|
||||
<BountyBox contract={contract} user={user ?? null} />
|
||||
) : (
|
||||
<BetPanel
|
||||
className="hidden xl:flex"
|
||||
contract={contract as CPMMBinaryContract}
|
||||
/>
|
||||
))}
|
||||
|
||||
{allowResolve &&
|
||||
(isNumeric || isPseudoNumeric ? (
|
||||
<NumericResolutionPanel creator={user} contract={contract} />
|
||||
) : (
|
||||
) : isBinary ? (
|
||||
<ResolutionPanel creator={user} contract={contract} />
|
||||
))}
|
||||
) : null)}
|
||||
</Col>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
export function ContractPageContent(
|
||||
|
@ -227,7 +236,8 @@ export function ContractPageContent(
|
|||
)}
|
||||
|
||||
{(outcomeType === 'FREE_RESPONSE' ||
|
||||
outcomeType === 'MULTIPLE_CHOICE') && (
|
||||
outcomeType === 'MULTIPLE_CHOICE' ||
|
||||
outcomeType === 'BOUNTY') && (
|
||||
<>
|
||||
<Spacer h={4} />
|
||||
<AnswersPanel contract={contract} />
|
||||
|
|
|
@ -56,12 +56,6 @@ export default function Create(props: { auth: { user: User } }) {
|
|||
const { user } = props.auth
|
||||
const router = useRouter()
|
||||
const params = router.query as NewQuestionParams
|
||||
// TODO: Not sure why Question is pulled out as its own component;
|
||||
// Maybe merge into newContract and then we don't need useEffect here.
|
||||
const [question, setQuestion] = useState('')
|
||||
useEffect(() => {
|
||||
setQuestion(params.q ?? '')
|
||||
}, [params.q])
|
||||
|
||||
if (!router.isReady) return <div />
|
||||
|
||||
|
@ -76,26 +70,7 @@ export default function Create(props: { auth: { user: User } }) {
|
|||
<div className="rounded-lg px-6 py-4 sm:py-0">
|
||||
<Title className="!mt-0" text="Create a market" />
|
||||
|
||||
<form>
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="mb-1">
|
||||
Question<span className={'text-red-700'}>*</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<Textarea
|
||||
placeholder="e.g. Will the Democrats win the 2024 US presidential election?"
|
||||
className="input input-bordered resize-none"
|
||||
autoFocus
|
||||
maxLength={MAX_QUESTION_LENGTH}
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value || '')}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<Spacer h={6} />
|
||||
<NewContract question={question} params={params} creator={user} />
|
||||
<NewContract params={params} creator={user} />
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
|
@ -103,12 +78,9 @@ export default function Create(props: { auth: { user: User } }) {
|
|||
}
|
||||
|
||||
// Allow user to create a new contract
|
||||
export function NewContract(props: {
|
||||
creator: User
|
||||
question: string
|
||||
params?: NewQuestionParams
|
||||
}) {
|
||||
const { creator, question, params } = props
|
||||
function NewContract(props: { creator: User; params?: NewQuestionParams }) {
|
||||
const { creator, params } = props
|
||||
const [question, setQuestion] = useState('')
|
||||
const { groupId, initValue } = params ?? {}
|
||||
const [outcomeType, setOutcomeType] = useState<outcomeType>(
|
||||
(params?.outcomeType as outcomeType) ?? 'BINARY'
|
||||
|
@ -131,6 +103,24 @@ export function NewContract(props: {
|
|||
}
|
||||
})
|
||||
}, [creator.id, groupId])
|
||||
|
||||
useEffect(() => {
|
||||
setQuestion(params?.q ?? '')
|
||||
}, [params?.q])
|
||||
|
||||
const questionLabel = outcomeType === 'BOUNTY' ? 'Bounty' : 'Question'
|
||||
const placeholders = {
|
||||
BINARY: 'e.g. Will the Democrats win the 2024 US presidential election?',
|
||||
MULTIPLE_CHOICE:
|
||||
'e.g. Which basketball team will be the March Madness champion?',
|
||||
FREE_RESPONSE: 'e.g. What programming language should I learn next?',
|
||||
NUMERIC: '', // Numeric type is deprecated
|
||||
PSEUDO_NUMERIC:
|
||||
'e.g. How many people will show up to Taco Tuesday this week?',
|
||||
BOUNTY: 'e.g. Add Dark Mode to Manifold Markets',
|
||||
}
|
||||
const placeholder = placeholders[outcomeType] ?? placeholders['BINARY']
|
||||
|
||||
const [ante, _setAnte] = useState(FIXED_ANTE)
|
||||
|
||||
// If params.closeTime is set, extract out the specified date and time
|
||||
|
@ -255,7 +245,7 @@ export function NewContract(props: {
|
|||
return (
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="mb-1">Answer type</span>
|
||||
<span className="mb-1">Market type</span>
|
||||
</label>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={outcomeType}
|
||||
|
@ -272,6 +262,7 @@ export function NewContract(props: {
|
|||
'Multiple choice': 'MULTIPLE_CHOICE',
|
||||
'Free response': 'FREE_RESPONSE',
|
||||
Numeric: 'PSEUDO_NUMERIC',
|
||||
Bounty: 'BOUNTY',
|
||||
}}
|
||||
isSubmitting={isSubmitting}
|
||||
className={'col-span-4'}
|
||||
|
@ -284,6 +275,27 @@ export function NewContract(props: {
|
|||
|
||||
<Spacer h={6} />
|
||||
|
||||
<form>
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="mb-1">
|
||||
{questionLabel}
|
||||
<span className={'text-red-700'}>*</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<Textarea
|
||||
placeholder={placeholder}
|
||||
className="input input-bordered resize-none"
|
||||
autoFocus
|
||||
maxLength={MAX_QUESTION_LENGTH}
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value || '')}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<Spacer h={6} />
|
||||
|
||||
{outcomeType === 'MULTIPLE_CHOICE' && (
|
||||
<MultipleChoiceAnswers answers={answers} setAnswers={setAnswers} />
|
||||
)}
|
||||
|
@ -380,7 +392,7 @@ export function NewContract(props: {
|
|||
|
||||
<div className="form-control mb-1 items-start">
|
||||
<label className="label mb-1 gap-2">
|
||||
<span>Question closes in</span>
|
||||
<span>{questionLabel} closes in</span>
|
||||
<InfoTooltip text="Betting will be halted after this time (local timezone)." />
|
||||
</label>
|
||||
<Row className={'w-full items-center gap-2'}>
|
||||
|
@ -436,7 +448,7 @@ export function NewContract(props: {
|
|||
<Row className="items-end justify-between">
|
||||
<div className="form-control mb-1 items-start">
|
||||
<label className="label mb-1 gap-2">
|
||||
<span>Cost</span>
|
||||
<span>{outcomeType === 'BOUNTY' ? 'Initial Bounty' : 'Cost'}</span>
|
||||
<InfoTooltip
|
||||
text={`Cost to create your question. This amount is used to subsidize betting.`}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue
Block a user