Merge branch 'main' into user-profile

This commit is contained in:
mantikoros 2022-01-31 14:25:31 -06:00
commit e0cfb71a8c
40 changed files with 333 additions and 230 deletions

View File

@ -174,7 +174,10 @@ export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
}
function calculateMktPayout(contract: Contract, bet: Bet) {
const p = getProbability(contract.totalShares)
const p =
contract.resolutionProbability !== undefined
? contract.resolutionProbability
: getProbability(contract.totalShares)
const weightedTotal =
p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO

View File

@ -27,6 +27,7 @@ export type Contract = {
isResolved: boolean
resolutionTime?: number // When the contract creator resolved the market
resolution?: outcome // Chosen by creator; must be one of outcomes
resolutionProbability?: number
volume24Hours: number
volume7Days: number

View File

@ -19,7 +19,7 @@ export function getNewContract(
calcStartPool(initialProb, ante)
const tags = parseTags(
`${extraTags.map((tag) => `#${tag}`).join(' ')} ${question} ${description}`
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
)
const lowercaseTags = tags.map((tag) => tag.toLowerCase())

View File

@ -59,8 +59,16 @@ export const getStandardPayouts = (
]) // add creator fee
}
export const getMktPayouts = (contract: Contract, bets: Bet[]) => {
const p = getProbability(contract.totalShares)
export const getMktPayouts = (
contract: Contract,
bets: Bet[],
resolutionProbability?: number
) => {
const p =
resolutionProbability === undefined
? getProbability(contract.totalShares)
: resolutionProbability
const poolTotal = contract.pool.YES + contract.pool.NO
console.log('Resolved MKT at p=', p, 'pool: $M', poolTotal)
@ -116,14 +124,15 @@ export const getMktPayouts = (contract: Contract, bets: Bet[]) => {
export const getPayouts = (
outcome: outcome,
contract: Contract,
bets: Bet[]
bets: Bet[],
resolutionProbability?: number
) => {
switch (outcome) {
case 'YES':
case 'NO':
return getStandardPayouts(outcome, contract, bets)
case 'MKT':
return getMktPayouts(contract, bets)
return getMktPayouts(contract, bets, resolutionProbability)
case 'CANCEL':
return getCancelPayouts(contract, bets)
}

View File

@ -3,19 +3,25 @@ export function parseTags(text: string) {
const matches = (text.match(regex) || []).map((match) =>
match.trim().substring(1)
)
const tagSet = new Set(matches)
const tagSet = new Set()
const uniqueTags: string[] = []
tagSet.forEach((tag) => uniqueTags.push(tag))
// Keep casing of last tag.
matches.reverse()
for (const tag of matches) {
const lowercase = tag.toLowerCase()
if (!tagSet.has(lowercase)) {
tagSet.add(lowercase)
uniqueTags.push(tag)
}
}
uniqueTags.reverse()
return uniqueTags
}
export function parseWordsAsTags(text: string) {
const regex = /(?:^|\s)(?:#?[a-z0-9_]+)/gi
const matches = (text.match(regex) || [])
.map((match) => match.replace('#', '').trim())
.filter((tag) => tag)
const tagSet = new Set(matches)
const uniqueTags: string[] = []
tagSet.forEach((tag) => uniqueTags.push(tag))
return uniqueTags
const taggedText = text
.split(/\s+/)
.map((tag) => `#${tag}`)
.join(' ')
return parseTags(taggedText)
}

View File

@ -1,5 +1,7 @@
import { getProbability } from '../../common/calculate'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { formatPercent } from '../../common/util/format'
import { sendTemplateEmail } from './send-email'
import { getPrivateUser, getUser } from './utils'
@ -18,7 +20,8 @@ export const sendMarketResolutionEmail = async (
payout: number,
creator: User,
contract: Contract,
resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT'
resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT',
resolutionProbability?: number
) => {
const privateUser = await getPrivateUser(userId)
if (
@ -31,6 +34,14 @@ export const sendMarketResolutionEmail = async (
const user = await getUser(userId)
if (!user) return
const prob = resolutionProbability ?? getProbability(contract.totalShares)
const toDisplayResolution = {
YES: 'YES',
NO: 'NO',
CANCEL: 'N/A',
MKT: formatPercent(prob),
}
const outcome = toDisplayResolution[resolution]
const subject = `Resolved ${outcome}: ${contract.question}`
@ -56,5 +67,3 @@ export const sendMarketResolutionEmail = async (
templateData
)
}
const toDisplayResolution = { YES: 'YES', NO: 'NO', CANCEL: 'N/A', MKT: 'MKT' }

View File

@ -16,17 +16,24 @@ export const resolveMarket = functions
data: {
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
contractId: string
probabilityInt?: number
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const { outcome, contractId } = data
const { outcome, contractId, probabilityInt } = data
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
return { status: 'error', message: 'Invalid outcome' }
if (
probabilityInt !== undefined &&
(probabilityInt < 1 || probabilityInt > 99 || !isFinite(probabilityInt))
)
return { status: 'error', message: 'Invalid probability' }
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await contractDoc.get()
if (!contractSnap.exists)
@ -42,10 +49,16 @@ export const resolveMarket = functions
const creator = await getUser(contract.creatorId)
if (!creator) return { status: 'error', message: 'Creator not found' }
const resolutionProbability =
probabilityInt !== undefined ? probabilityInt / 100 : undefined
await contractDoc.update({
isResolved: true,
resolution: outcome,
resolutionTime: Date.now(),
...(resolutionProbability === undefined
? {}
: { resolutionProbability }),
})
console.log('contract ', contractId, 'resolved to:', outcome)
@ -57,7 +70,12 @@ export const resolveMarket = functions
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
const openBets = bets.filter((b) => !b.isSold && !b.sale)
const payouts = getPayouts(outcome, contract, openBets)
const payouts = getPayouts(
outcome,
contract,
openBets,
resolutionProbability
)
console.log('payouts:', payouts)
@ -79,7 +97,8 @@ export const resolveMarket = functions
userPayouts,
creator,
contract,
outcome
outcome,
resolutionProbability
)
return result
@ -91,7 +110,8 @@ const sendResolutionEmails = async (
userPayouts: { [userId: string]: number },
creator: User,
contract: Contract,
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT',
resolutionProbability?: number
) => {
const nonWinners = _.difference(
_.uniq(openBets.map(({ userId }) => userId)),
@ -103,7 +123,14 @@ const sendResolutionEmails = async (
]
await Promise.all(
emailPayouts.map(([userId, payout]) =>
sendMarketResolutionEmail(userId, payout, creator, contract, outcome)
sendMarketResolutionEmail(
userId,
payout,
creator,
contract,
outcome,
resolutionProbability
)
)
)
}

View File

@ -1,20 +1,14 @@
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { initAdmin } from './script-init'
initAdmin('stephen')
import { Bet } from '../../../common/bet'
import { getProbability } from '../../../common/calculate'
import { Contract } from '../../../common/contract'
type DocRef = admin.firestore.DocumentReference
// Generate your own private key, and set the path below:
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
// const serviceAccount = require('../../../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
const serviceAccount = require('../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-351a65eca3.json')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
const firestore = admin.firestore()
async function migrateContract(contractRef: DocRef, contract: Contract) {

View File

@ -1,17 +1,11 @@
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { initAdmin } from './script-init'
initAdmin('stephen')
import { PrivateUser, STARTING_BALANCE, User } from '../../../common/user'
// Generate your own private key, and set the path below:
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
const serviceAccount = require('../../../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
const firestore = admin.firestore()
async function main() {

View File

@ -1,16 +1,11 @@
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { initAdmin } from './script-init'
initAdmin('james')
import { Contract } from '../../../common/contract'
// Generate your own private key, and set the path below:
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
// const serviceAccount = require('../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
const serviceAccount = require('../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
const firestore = admin.firestore()
async function makeContractsPublic() {

View File

@ -1,18 +1,14 @@
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { initAdmin } from './script-init'
initAdmin('james')
import { Bet } from '../../../common/bet'
import { Contract } from '../../../common/contract'
type DocRef = admin.firestore.DocumentReference
// Generate your own private key, and set the path below:
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
const serviceAccount = require('../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
const firestore = admin.firestore()
async function migrateBet(contractRef: DocRef, bet: Bet) {

View File

@ -1,6 +1,9 @@
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { initAdmin } from './script-init'
initAdmin('stephenDev')
import { Contract } from '../../../common/contract'
import { Bet } from '../../../common/bet'
import { calculateShares, getProbability } from '../../../common/calculate'
@ -9,15 +12,6 @@ import { User } from '../../../common/user'
type DocRef = admin.firestore.DocumentReference
// Generate your own private key, and set the path below:
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
// const serviceAccount = require('../../../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
const serviceAccount = require('../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-351a65eca3.json')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
const firestore = admin.firestore()
async function recalculateContract(

View File

@ -1,19 +1,14 @@
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { initAdmin } from './script-init'
initAdmin('james')
import { Bet } from '../../../common/bet'
import { Contract } from '../../../common/contract'
type DocRef = admin.firestore.DocumentReference
// Generate your own private key, and set the path below:
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
// const serviceAccount = require('../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
const serviceAccount = require('../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
const firestore = admin.firestore()
async function recalculateContract(contractRef: DocRef, contract: Contract) {

View File

@ -1,19 +1,12 @@
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { initAdmin } from './script-init'
initAdmin('stephenDev')
import { Contract } from '../../../common/contract'
import { getValues } from '../utils'
// Generate your own private key, and set the path below:
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
// James:
const serviceAccount = require('../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json')
// Stephen:
// const serviceAccount = require('../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
const firestore = admin.firestore()
async function renameUserContracts(

View File

@ -0,0 +1,32 @@
import * as admin from 'firebase-admin'
// Generate your own private key, and set the path below:
// Prod:
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
// Dev:
// https://console.firebase.google.com/u/0/project/dev-mantic-markets/settings/serviceaccounts/adminsdk
const pathsToPrivateKey = {
james:
'/Users/jahooma/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json',
jamesDev:
'/Users/jahooma/dev-mantic-markets-firebase-adminsdk-sir5m-f38cdbee37.json',
stephen:
'../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-351a65eca3.json',
stephenDev:
'../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json',
}
export const initAdmin = (who: keyof typeof pathsToPrivateKey) => {
const serviceAccount = require(pathsToPrivateKey[who])
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
}
// Then:
// yarn watch (or yarn build)
// firebase use dev (or firebase use prod)
// Run script:
// node lib/functions/src/scripts/update-contract-tags.js

View File

@ -1,22 +1,15 @@
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
// Generate your own private key, and set the path below:
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
// James:
const serviceAccount = require('/Users/jahooma/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json')
// Stephen:
// const serviceAccount = require('../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
const firestore = admin.firestore()
import { initAdmin } from './script-init'
initAdmin('jamesDev')
import { Contract } from '../../../common/contract'
import { parseTags } from '../../../common/util/parse'
import { getValues } from '../utils'
async function updateContractTags() {
const firestore = admin.firestore()
console.log('Updating contracts tags')
const contracts = await getValues<Contract>(firestore.collection('contracts'))
@ -48,4 +41,6 @@ async function updateContractTags() {
}
}
if (require.main === module) updateContractTags().then(() => process.exit())
if (require.main === module) {
updateContractTags().then(() => process.exit())
}

View File

@ -1,6 +1,6 @@
import clsx from 'clsx'
import { useUser } from '../hooks/use-user'
import { formatMoney } from '../lib/util/format'
import { formatMoney } from '../../common/util/format'
import { AddFundsButton } from './add-funds-button'
import { Col } from './layout/col'
import { Row } from './layout/row'

View File

@ -11,7 +11,7 @@ import {
formatMoney,
formatPercent,
formatWithCommas,
} from '../lib/util/format'
} from '../../common/util/format'
import { Title } from './title'
import {
getProbability,

View File

@ -11,7 +11,7 @@ import {
formatMoney,
formatPercent,
formatWithCommas,
} from '../lib/util/format'
} from '../../common/util/format'
import { Col } from './layout/col'
import { Spacer } from './layout/spacer'
import {

View File

@ -1,7 +1,7 @@
import clsx from 'clsx'
import Link from 'next/link'
import { Row } from '../components/layout/row'
import { formatMoney } from '../lib/util/format'
import { formatMoney } from '../../common/util/format'
import { UserLink } from './user-page'
import {
Contract,
@ -74,7 +74,7 @@ export function ResolutionOrChance(props: {
const resolutionText = {
YES: 'YES',
NO: 'NO',
MKT: 'MKT',
MKT: probPercent,
CANCEL: 'N/A',
'': '',
}[resolution || '']
@ -142,12 +142,9 @@ export function AbbrContractDetails(props: {
export function ContractDetails(props: { contract: Contract }) {
const { contract } = props
const { question, description, closeTime, creatorName, creatorUsername } =
contract
const { closeTime, creatorName, creatorUsername } = contract
const { truePool, createdDate, resolvedDate } = contractMetrics(contract)
const tags = parseTags(`${question} ${description}`).map((tag) => `#${tag}`)
return (
<Col className="text-sm text-gray-500 gap-2 sm:flex-row sm:flex-wrap">
<Row className="gap-2 flex-wrap">
@ -199,10 +196,10 @@ export function ContractDetails(props: { contract: Contract }) {
// String version of the above, to send to the OpenGraph image generator
export function contractTextDetails(contract: Contract) {
const { question, description, closeTime } = contract
const { closeTime, tags } = contract
const { truePool, createdDate, resolvedDate } = contractMetrics(contract)
const tags = parseTags(`${question} ${description}`).map((tag) => `#${tag}`)
const hashtags = tags.map((tag) => `#${tag}`)
return (
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
@ -212,6 +209,6 @@ export function contractTextDetails(contract: Contract) {
).format('MMM D, h:mma')}`
: '') +
`${formatMoney(truePool)} pool` +
(tags.length > 0 ? `${tags.join(' ')}` : '')
(hashtags.length > 0 ? `${hashtags.join(' ')}` : '')
)
}

View File

@ -27,7 +27,7 @@ import { Linkify } from './linkify'
import { Row } from './layout/row'
import { createComment } from '../lib/firebase/comments'
import { useComments } from '../hooks/use-comments'
import { formatMoney } from '../lib/util/format'
import { formatMoney } from '../../common/util/format'
import { ResolutionOrChance } from './contract-card'
import { SiteLink } from './site-link'
import { Col } from './layout/col'
@ -160,7 +160,9 @@ export function ContractDescription(props: {
setEditing(false)
const newDescription = `${contract.description}\n\n${description}`.trim()
const tags = parseTags(`${contract.tags.join(' ')} ${newDescription}`)
const tags = parseTags(
`${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}`
)
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
await updateContract(contract.id, {
description: newDescription,
@ -685,10 +687,7 @@ export function ContractFeed(props: {
))}
</ul>
{tradingAllowed(contract) && (
<BetRow
contract={contract}
className={clsx('-mt-4', betRowClassName)}
/>
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
)}
</div>
)

View File

@ -3,6 +3,7 @@ import {
Contract,
deleteContract,
contractPath,
tradingAllowed,
} from '../lib/firebase/contracts'
import { Col } from './layout/col'
import { Spacer } from './layout/spacer'
@ -58,11 +59,13 @@ export const ContractOverview = (props: {
large
/>
<BetRow
contract={contract}
className="md:hidden"
labelClassName="hidden"
/>
{tradingAllowed(contract) && (
<BetRow
contract={contract}
className="md:hidden"
labelClassName="hidden"
/>
)}
</Row>
<ContractDetails contract={contract} />

View File

@ -11,7 +11,6 @@ import {
import { User } from '../lib/firebase/users'
import { Col } from './layout/col'
import { SiteLink } from './site-link'
import { parseTags } from '../../common/util/parse'
import { ContractCard } from './contract-card'
import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
@ -122,12 +121,13 @@ function CreatorContractsGrid(props: { contracts: Contract[] }) {
function TagContractsGrid(props: { contracts: Contract[] }) {
const { contracts } = props
const contractTags = _.flatMap(contracts, (contract) =>
parseTags(contract.question + ' ' + contract.description).map((tag) => ({
const contractTags = _.flatMap(contracts, (contract) => {
const { tags } = contract
return tags.map((tag) => ({
tag,
contract,
}))
)
})
const groupedByTag = _.groupBy(contractTags, ({ tag }) => tag)
const byTag = _.mapValues(groupedByTag, (contractTags) =>
contractTags.map(({ contract }) => contract)
@ -210,7 +210,8 @@ export function SearchableGrid(props: {
check(c.question) ||
check(c.description) ||
check(c.creatorName) ||
check(c.creatorUsername)
check(c.creatorUsername) ||
check(c.lowercaseTags.map((tag) => `#${tag}`).join(' '))
)
if (sort === 'newest' || sort === 'all') {

View File

@ -4,7 +4,7 @@ import { useState } from 'react'
import { parseWordsAsTags } from '../../common/util/parse'
import { createFold } from '../lib/firebase/api-call'
import { foldPath } from '../lib/firebase/folds'
import { toCamelCase } from '../lib/util/format'
import { toCamelCase } from '../../common/util/format'
import { ConfirmationButton } from './confirmation-button'
import { Col } from './layout/col'
import { Spacer } from './layout/spacer'

View File

@ -6,7 +6,7 @@ import { PencilIcon } from '@heroicons/react/outline'
import { Fold } from '../../common/fold'
import { parseWordsAsTags } from '../../common/util/parse'
import { deleteFold, updateFold } from '../lib/firebase/folds'
import { toCamelCase } from '../lib/util/format'
import { toCamelCase } from '../../common/util/format'
import { Spacer } from './layout/spacer'
import { TagsList } from './tags-list'
import { useRouter } from 'next/router'

View File

@ -5,7 +5,7 @@ export function OutcomeLabel(props: {
if (outcome === 'YES') return <YesLabel />
if (outcome === 'NO') return <NoLabel />
if (outcome === 'MKT') return <MarketLabel />
if (outcome === 'MKT') return <ProbLabel />
return <CancelLabel />
}
@ -24,3 +24,7 @@ export function CancelLabel() {
export function MarketLabel() {
return <span className="text-blue-400">MKT</span>
}
export function ProbLabel() {
return <span className="text-blue-400">PROB</span>
}

View File

@ -0,0 +1,36 @@
import { Row } from './layout/row'
export function ProbabilitySelector(props: {
probabilityInt: number
setProbabilityInt: (p: number) => void
isSubmitting?: boolean
}) {
const { probabilityInt, setProbabilityInt, isSubmitting } = props
return (
<Row className="items-center gap-2">
<label className="input-group input-group-lg w-fit text-lg">
<input
type="number"
value={probabilityInt}
className="input input-bordered input-md text-lg"
disabled={isSubmitting}
min={1}
max={99}
onChange={(e) =>
setProbabilityInt(parseInt(e.target.value.substring(0, 2)))
}
/>
<span>%</span>
</label>
<input
type="range"
className="range range-primary"
min={1}
max={99}
value={probabilityInt}
onChange={(e) => setProbabilityInt(parseInt(e.target.value))}
/>
</Row>
)
}

View File

@ -1,5 +1,5 @@
import { firebaseLogout, User } from '../lib/firebase/users'
import { formatMoney } from '../lib/util/format'
import { formatMoney } from '../../common/util/format'
import { Avatar } from './avatar'
import { Col } from './layout/col'
import { MenuButton } from './menu'

View File

@ -9,6 +9,8 @@ import { YesNoCancelSelector } from './yes-no-selector'
import { Spacer } from './layout/spacer'
import { ConfirmationButton as ConfirmationButton } from './confirmation-button'
import { resolveMarket } from '../lib/firebase/api-call'
import { ProbabilitySelector } from './probability-selector'
import { getProbability } from '../../common/calculate'
export function ResolutionPanel(props: {
creator: User
@ -26,6 +28,8 @@ export function ResolutionPanel(props: {
'YES' | 'NO' | 'MKT' | 'CANCEL' | undefined
>()
const [prob, setProb] = useState(getProbability(contract.totalShares) * 100)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | undefined>(undefined)
@ -35,6 +39,7 @@ export function ResolutionPanel(props: {
const result = await resolveMarket({
outcome,
contractId: contract.id,
probabilityInt: prob,
}).then((r) => r.data as any)
console.log('resolved', outcome, 'result:', result)
@ -82,8 +87,8 @@ export function ResolutionPanel(props: {
<>The pool will be returned to traders with no fees.</>
) : outcome === 'MKT' ? (
<>
Traders will be paid out at the current implied probability. You
earn 1% of the pool.
Traders will be paid out at the probability you specify. You earn 1%
of the pool.
</>
) : (
<>Resolving this market will immediately pay out traders.</>
@ -113,7 +118,20 @@ export function ResolutionPanel(props: {
}}
onSubmit={resolve}
>
<p>Are you sure you want to resolve this market?</p>
{outcome === 'MKT' ? (
<>
<p className="mb-4">
What probability would you like to resolve the market to?
</p>
<ProbabilitySelector
probabilityInt={Math.round(prob)}
setProbabilityInt={setProb}
/>
</>
) : (
<p>Are you sure you want to resolve this market?</p>
)}
</ConfirmationButton>
</Col>
)

View File

@ -1,6 +1,6 @@
import clsx from 'clsx'
import React from 'react'
import { formatMoney } from '../lib/util/format'
import { formatMoney } from '../../common/util/format'
import { Col } from './layout/col'
import { Row } from './layout/row'
@ -78,7 +78,7 @@ export function YesNoCancelSelector(props: {
onClick={() => onSelect('MKT')}
className={clsx(btnClassName, 'btn-sm')}
>
MKT
PROB
</Button>
<Button

View File

@ -5,6 +5,7 @@ import {
listenForContracts,
listenForHotContracts,
} from '../lib/firebase/contracts'
import { listenForTaggedContracts } from '../lib/firebase/folds'
export const useContracts = () => {
const [contracts, setContracts] = useState<Contract[] | undefined>()
@ -16,6 +17,21 @@ export const useContracts = () => {
return contracts
}
export const useTaggedContracts = (tags: string[] | undefined) => {
const [contracts, setContracts] = useState<Contract[] | undefined>(
tags && tags.length === 0 ? [] : undefined
)
const tagsKey = tags?.map((tag) => tag.toLowerCase()).join(',') ?? ''
useEffect(() => {
if (!tags || tags.length === 0) return
return listenForTaggedContracts(tags, setContracts)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tagsKey])
return contracts
}
export const useHotContracts = () => {
const [hotContracts, setHotContracts] = useState<Contract[] | undefined>()

View File

@ -17,7 +17,7 @@ import {
import _ from 'lodash'
import { app } from './init'
import { getValues, listenForValues } from './utils'
import { getValues, listenForValue, listenForValues } from './utils'
import { Contract } from '../../../common/contract'
import { getProbability } from '../../../common/calculate'
import { createRNG, shuffle } from '../../../common/util/random'
@ -35,10 +35,11 @@ export function contractMetrics(contract: Contract) {
createdTime,
resolutionTime,
isResolved,
resolutionProbability,
} = contract
const truePool = pool.YES + pool.NO
const prob = getProbability(totalShares)
const prob = resolutionProbability ?? getProbability(totalShares)
const probPercent = Math.round(prob * 100) + '%'
const startProb = getProbability(phantomShares)
@ -125,9 +126,7 @@ export function listenForContract(
setContract: (contract: Contract | null) => void
) {
const contractRef = doc(contractCollection, contractId)
return onSnapshot(contractRef, (contractSnap) => {
setContract((contractSnap.data() ?? null) as Contract | null)
})
return listenForValue<Contract>(contractRef, setContract)
}
function chooseRandomSubset(contracts: Contract[], count: number) {

View File

@ -45,6 +45,17 @@ export async function getFoldBySlug(slug: string) {
return folds.length === 0 ? null : folds[0]
}
function contractsByTagsQuery(tags: string[]) {
return query(
contractCollection,
where(
'lowercaseTags',
'array-contains-any',
tags.map((tag) => tag.toLowerCase())
)
)
}
export async function getFoldContracts(fold: Fold) {
const {
tags,
@ -56,18 +67,7 @@ export async function getFoldContracts(fold: Fold) {
const [tagsContracts, includedContracts] = await Promise.all([
// TODO: if tags.length > 10, execute multiple parallel queries
tags.length > 0
? getValues<Contract>(
query(
contractCollection,
where(
'lowercaseTags',
'array-contains-any',
tags.map((tag) => tag.toLowerCase())
)
)
)
: [],
tags.length > 0 ? getValues<Contract>(contractsByTagsQuery(tags)) : [],
// TODO: if contractIds.length > 10, execute multiple parallel queries
contractIds.length > 0
@ -97,6 +97,13 @@ export async function getFoldContracts(fold: Fold) {
return [...approvedContracts, ...includedContracts]
}
export function listenForTaggedContracts(
tags: string[],
setContracts: (contracts: Contract[]) => void
) {
return listenForValues<Contract>(contractsByTagsQuery(tags), setContracts)
}
export function listenForFold(
foldId: string,
setFold: (fold: Fold | null) => void

View File

@ -27,13 +27,18 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
}
function scoreUsersByContract(contract: Contract, bets: Bet[]) {
const { resolution } = contract
const { resolution, resolutionProbability } = contract
const [closedBets, openBets] = _.partition(
bets,
(bet) => bet.isSold || bet.sale
)
const resolvePayouts = getPayouts(resolution ?? 'MKT', contract, openBets)
const resolvePayouts = getPayouts(
resolution ?? 'MKT',
contract,
openBets,
resolutionProbability
)
const salePayouts = closedBets.map((bet) => {
const { userId, sale } = bet

View File

@ -23,6 +23,8 @@ export function listenForValue<T>(
setValue: (value: T | null) => void
) {
return onSnapshot(docRef, (snapshot) => {
if (snapshot.metadata.fromCache) return
const value = snapshot.exists() ? (snapshot.data() as T) : null
setValue(value)
})

View File

@ -8,13 +8,13 @@ import { Spacer } from '../components/layout/spacer'
import { useUser } from '../hooks/use-user'
import { Contract, contractPath } from '../lib/firebase/contracts'
import { createContract } from '../lib/firebase/api-call'
import { Row } from '../components/layout/row'
import { AmountInput } from '../components/amount-input'
import { MINIMUM_ANTE } from '../../common/antes'
import { InfoTooltip } from '../components/info-tooltip'
import { CREATOR_FEE } from '../../common/fees'
import { Page } from '../components/page'
import { Title } from '../components/title'
import { ProbabilitySelector } from '../components/probability-selector'
export default function Create() {
const [question, setQuestion] = useState('')
@ -72,8 +72,8 @@ export function NewContract(props: { question: string; tag?: string }) {
const [anteError, setAnteError] = useState<string | undefined>()
// By default, close the market a week from today
// const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DD')
const [closeDate, setCloseDate] = useState<undefined | string>(undefined)
const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DDT23:59')
const [closeDate, setCloseDate] = useState<undefined | string>(weekFromToday)
const [isSubmitting, setIsSubmitting] = useState(false)
@ -127,30 +127,11 @@ export function NewContract(props: { question: string; tag?: string }) {
<label className="label">
<span className="mb-1">Initial probability</span>
</label>
<Row className="items-center gap-2">
<label className="input-group input-group-lg w-fit text-lg">
<input
type="number"
value={initialProb}
className="input input-bordered input-md text-lg"
disabled={isSubmitting}
min={1}
max={99}
onChange={(e) =>
setInitialProb(parseInt(e.target.value.substring(0, 2)))
}
/>
<span>%</span>
</label>
<input
type="range"
className="range range-primary"
min={1}
max={99}
value={initialProb}
onChange={(e) => setInitialProb(parseInt(e.target.value))}
/>
</Row>
<ProbabilitySelector
probabilityInt={initialProb}
setProbabilityInt={setInitialProb}
/>
</div>
<Spacer h={4} />
@ -175,15 +156,15 @@ export function NewContract(props: { question: string; tag?: string }) {
<div className="form-control items-start mb-1">
<label className="label gap-2 mb-1">
<span>Last trading day</span>
<InfoTooltip text="Trading allowed through 11:59 pm local time on this date." />
<span>Market close</span>
<InfoTooltip text="Trading will be halted after this time (local timezone)." />
</label>
<input
type="date"
type="datetime-local"
className="input input-bordered"
onClick={(e) => e.stopPropagation()}
onChange={(e) => setCloseDate(e.target.value || '')}
min={new Date().toISOString().split('T')[0]}
min={Date.now()}
disabled={isSubmitting}
value={closeDate}
/>

View File

@ -28,12 +28,13 @@ import { useRouter } from 'next/router'
import clsx from 'clsx'
import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring'
import { Leaderboard } from '../../../components/leaderboard'
import { formatMoney, toCamelCase } from '../../../lib/util/format'
import { formatMoney, toCamelCase } from '../../../../common/util/format'
import { EditFoldButton } from '../../../components/edit-fold-button'
import Custom404 from '../../404'
import { FollowFoldButton } from '../../../components/follow-fold-button'
import FeedCreate from '../../../components/feed-create'
import { SEO } from '../../../components/SEO'
import { useTaggedContracts } from '../../../hooks/use-contracts'
export async function getStaticProps(props: { params: { slugs: string[] } }) {
const { slugs } = props.params
@ -104,8 +105,14 @@ async function toUserScores(userScores: { [userId: string]: number }) {
const topUsers = await Promise.all(
topUserPairs.map(([userId]) => getUser(userId))
)
const topUserScores = topUserPairs.map(([_, score]) => score)
return [topUsers, topUserScores] as const
const existingPairs = topUserPairs.filter(([id, _]) =>
topUsers.find((user) => user?.id === id)
)
const topExistingUsers = existingPairs.map(
([id]) => topUsers.find((user) => user?.id === id) as User
)
const topUserScores = existingPairs.map(([_, score]) => score)
return [topExistingUsers, topUserScores] as const
}
export async function getStaticPaths() {
@ -127,8 +134,6 @@ export default function FoldPage(props: {
}) {
const {
curator,
contracts,
activeContracts,
activeContractBets,
activeContractComments,
topTraders,
@ -151,6 +156,16 @@ export default function FoldPage(props: {
const user = useUser()
const isCurator = user && fold && user.id === fold.curatorId
const taggedContracts = useTaggedContracts(fold?.tags) ?? props.contracts
const contractsMap = _.fromPairs(
taggedContracts.map((contract) => [contract.id, contract])
)
const contracts = props.contracts.map((contract) => contractsMap[contract.id])
const activeContracts = props.activeContracts.map(
(contract) => contractsMap[contract.id]
)
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
return <Custom404 />
}

View File

@ -1,12 +1,8 @@
import React from 'react'
import Router from 'next/router'
import _ from 'lodash'
import {
Contract,
getClosingSoonContracts,
getHotContracts,
listAllContracts,
} from '../lib/firebase/contracts'
import { Contract, listAllContracts } from '../lib/firebase/contracts'
import { Page } from '../components/page'
import { ActivityFeed, findActiveContracts } from './activity'
import {
@ -19,15 +15,13 @@ import FeedCreate from '../components/feed-create'
import { Spacer } from '../components/layout/spacer'
import { Col } from '../components/layout/col'
import { useUser } from '../hooks/use-user'
import { useContracts } from '../hooks/use-contracts'
export async function getStaticProps() {
const [contracts, recentComments, hotContracts, closingSoonContracts] =
await Promise.all([
listAllContracts().catch((_) => []),
getRecentComments().catch(() => []),
getHotContracts().catch(() => []),
getClosingSoonContracts().catch(() => []),
])
const [contracts, recentComments] = await Promise.all([
listAllContracts().catch((_) => []),
getRecentComments().catch(() => []),
])
const activeContracts = findActiveContracts(contracts, recentComments)
const activeContractBets = await Promise.all(
@ -44,8 +38,6 @@ export async function getStaticProps() {
activeContracts,
activeContractBets,
activeContractComments,
hotContracts,
closingSoonContracts,
},
revalidate: 60, // regenerate after a minute
@ -56,27 +48,18 @@ const Home = (props: {
activeContracts: Contract[]
activeContractBets: Bet[][]
activeContractComments: Comment[][]
hotContracts: Contract[]
closingSoonContracts: Contract[]
}) => {
const {
activeContracts,
activeContractBets,
activeContractComments,
// hotContracts,
// closingSoonContracts,
} = props
const { activeContracts, activeContractBets, activeContractComments } = props
const user = useUser()
// const initialActiveContracts = props.activeContracts ?? []
// const contracts = useContracts()
// const recentComments = useRecentComments()
// const activeContracts =
// recentComments && contracts
// ? findActiveContracts(contracts, recentComments)
// : initialActiveContracts
// TODO: get activeContractBets, activeContractComments associated with activeContracts
const contracts = useContracts() ?? activeContracts
const contractsMap = _.fromPairs(
contracts.map((contract) => [contract.id, contract])
)
const updatedContracts = activeContracts.map(
(contract) => contractsMap[contract.id]
)
if (user === null) {
Router.replace('/')
@ -89,17 +72,10 @@ const Home = (props: {
<Col className="max-w-3xl">
<FeedCreate user={user ?? undefined} />
<Spacer h={4} />
{/* <HotMarkets contracts={hotContracts?.slice(0, 4) ?? []} />
<Spacer h={4} />
<ClosingSoonMarkets contracts={closingSoonContracts ?? []} />
<Spacer h={10} /> */}
<ActivityFeed
contracts={activeContracts ?? []}
contractBets={activeContractBets ?? []}
contractComments={activeContractComments ?? []}
contracts={updatedContracts}
contractBets={activeContractBets}
contractComments={activeContractComments}
/>
</Col>
</Col>

View File

@ -1,9 +1,10 @@
import _ from 'lodash'
import { Col } from '../components/layout/col'
import { Leaderboard } from '../components/leaderboard'
import { Page } from '../components/page'
import { getTopCreators, getTopTraders, User } from '../lib/firebase/users'
import { formatMoney } from '../lib/util/format'
import { formatMoney } from '../../common/util/format'
export async function getStaticProps() {
const [topTraders, topCreators] = await Promise.all([