From a3663d03e856ef087adeab052182d907ac2700ba Mon Sep 17 00:00:00 2001 From: TrueMilli <61841994+TrueMilli@users.noreply.github.com> Date: Tue, 14 Jun 2022 22:01:32 +0200 Subject: [PATCH] Automated market resolution (#404) * Added radio buttons to market creation (non functional) * Ignoring vs code files Should this be done in the repo or should everyone using VS Code do that himself globally on his machine(s)? * Removed 'automatic' resolution * added union type for resolution * revert: resolution could be anything here (non binary markets) * Expanded ChoicesToggleGroup for string choices * Added combined resolution and required buttons to market creation * restricted automatic resolution to binary markets * added automatic resolution to contract * added automatic resolution to contract overview * string or number array to mixed array * created const for resolutions * Added comments for leading semicolons * configuration of auto resolution on market creation * v1.22.19 * v1.0.0 * v0.0.0 * v1.0.0 * v1.22.19 * Mock display automatic resolution * Revert changes to market creation * Revert "v1.22.19" This reverts commit 22f59adc9cd15eb5dc3a30c362b7d90eb3f5f230. * Removed resolutiontype from contract creation * Added auto resolution time to contract * Auto resolution date editable * refactoring * Editable interface for auto resolution * New edit interface for auto resolution * Setting of auto resolve date when changing close date * prohibited changing other peoples markets * removed unnecessary export * refactoring (cherry picked from commit 4de86d5b08f9bc1d960b51746260a5b8c5d9b1fd) * Added comments for leading semicolons (cherry picked from commit 60739c7853ce611a12227887f1a115f75e957d8e) * Ignoring vs code files Should this be done in the repo or should everyone using VS Code do that himself globally on his machine(s)? (cherry picked from commit 944de9398a03ff9e5b2fccefa69935b4d9fa4b32) * removed unused imports and variables * added type for binary resolution * Prettier * const for binary resolutions * using the type "resolution" * Prettier * Re-added comment * Update functions/src/create-contract.ts * Revert "Ignoring vs code files" This reverts commit 09aea5c207bcdc5ee2edb5db70223b5516abe415. * launch config for debugging with vs code WIP * "Launch Chrome" does not work since login via google is not possible in debugger-chrome * Breakpoints are unbound when attached to chrome * Revert "Added comments for leading semicolons" * prettier * linebreak crlf * vscode settings * correct linebreaks * search exclusion * automatic prettifier * vscode settings * correct linebreaks * search exclusion * automatic prettifier * Working debugger config * fix merge * Removed comments, default resolution MKT * removed vscode from gitignore * refactoring description update * Added auto resolution to LiteMarket * fix date, setDate mutates object * fixed firestore.rules * script to add auto resolution to all markets * regularely auto resolve markets * fix description error * moved calculate ts for access in firebase * Revert "moved calculate ts for access in firebase" This reverts commit 8380bf4f726cdfa0009cc40d6938ad5d65789d30. * fix reference to calculate for firebase * fixed references to time * renamed function * added description * added auto resolution to description * direct bool check instead of != null * direct bool check instead of != undefined * remove explicit type * Fix free response markets * removed contract from functionname * interval set to 1h * query instead of filter * folds ~> contracts * query instead of filter * promise.all instead of foreach * removed contractDoc from function header * removed autoResolution from function header * batchedWaitAll instead of promise.all * removed unused parameter * replaced auto resolution with constant * suggestions from PR * fix comment * removed unused imports * added scripts to add close dates on prod * optimization * removed test script * security: only auto resolve markets which are closed * consistency checks * re-added type check for binary markets * moved check of probability into switch case block * removed unused import * auto resolution every minute * auto resolution time optional * pr fixes --- .gitignore | 2 +- common/contract.ts | 6 +- common/new-contract.ts | 4 +- firestore.rules | 2 +- functions/src/create-contract.ts | 4 + functions/src/resolve-market.ts | 295 +++++++++++------- functions/src/scripts/add-auto-resolution.ts | 53 ++++ functions/src/scripts/add-close-time.ts | 108 +++++++ web/components/contract/contract-details.tsx | 37 ++- .../contract/contract-info-dialog.tsx | 106 ++++++- web/components/layout/modal.tsx | 2 +- web/pages/api/v0/_types.ts | 4 + 12 files changed, 492 insertions(+), 131 deletions(-) create mode 100644 functions/src/scripts/add-auto-resolution.ts create mode 100644 functions/src/scripts/add-close-time.ts diff --git a/.gitignore b/.gitignore index 10f5d982..80182258 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .idea/ .vercel node_modules -yarn-error.log +yarn-error.log \ No newline at end of file diff --git a/common/contract.ts b/common/contract.ts index 8427c84b..e889906e 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -31,9 +31,9 @@ export type Contract = { closeTime?: number // When no more trading is allowed isResolved: boolean - resolutionTime?: number // When the contract creator resolved the market + resolutionTime?: number // When the market is resolved resolution?: string - + autoResolutionTime?: number // When the market will be resolved automatically closeEmailsSent?: number volume: number @@ -90,10 +90,12 @@ export type Numeric = { resolutionValue?: number } +export type contractField = keyof Contract export type outcomeType = AnyOutcomeType['outcomeType'] export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL' export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'NUMERIC'] as const +export const AUTO_RESOLUTION = 'MKT' as resolution export const MAX_QUESTION_LENGTH = 480 export const MAX_DESCRIPTION_LENGTH = 10000 diff --git a/common/new-contract.ts b/common/new-contract.ts index 0b7d294a..9eb6cf5a 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -27,7 +27,8 @@ export function getNewContract( // used for numeric markets bucketCount: number, min: number, - max: number + max: number, + autoResolutionTime?: number ) { const tags = parseTags( `${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}` @@ -59,6 +60,7 @@ export function getNewContract( isResolved: false, createdTime: Date.now(), closeTime, + autoResolutionTime, volume: 0, volume24Hours: 0, diff --git a/firestore.rules b/firestore.rules index 3516de02..ab65e325 100644 --- a/firestore.rules +++ b/firestore.rules @@ -56,7 +56,7 @@ service cloud.firestore { allow update: if request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['tags', 'lowercaseTags']); allow update: if request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['description', 'closeTime']) + .hasOnly(['description', 'closeTime', 'autoResolutionTime']) && resource.data.creatorId == request.auth.uid; allow update: if isAdmin(); } diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 645b5544..97767ed7 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -27,6 +27,7 @@ import { import { getNoneAnswer } from '../../common/answer' import { getNewContract } from '../../common/new-contract' import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' +import { DAY_MS } from '../../common/util/time' import { User } from '../../common/user' const bodySchema = z.object({ @@ -64,6 +65,8 @@ export const createmarket = newEndpoint(['POST'], async (req, auth) => { ;({ initialProb } = validate(binarySchema, req.body)) } + const autoResolutionTime = closeTime.getTime() + 7 * DAY_MS + // Uses utc time on server: const today = new Date() let freeMarketResetTime = new Date().setUTCHours(16, 0, 0, 0) @@ -113,6 +116,7 @@ export const createmarket = newEndpoint(['POST'], async (req, auth) => { ante, closeTime.getTime(), tags ?? [], + autoResolutionTime, NUMERIC_BUCKET_COUNT, min ?? 0, max ?? 0 diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 894f1492..e1b7fbb3 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -1,8 +1,14 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash' +import { difference, uniq, mapValues, groupBy, sum, sumBy } from 'lodash' -import { Contract, resolution, RESOLUTIONS } from '../../common/contract' +import { + AUTO_RESOLUTION, + Contract, + FreeResponse, + resolution, + RESOLUTIONS, +} from '../../common/contract' import { User } from '../../common/user' import { Bet } from '../../common/bet' import { getUser, isProd, payUser } from './utils' @@ -15,6 +21,10 @@ import { } from '../../common/payouts' import { removeUndefinedProps } from '../../common/util/object' import { LiquidityProvision } from '../../common/liquidity-provision' +import { getDpmOutcomeProbability } from '../../common/calculate-dpm' +import { getValues } from './utils' +import { batchedWaitAll } from '../../common/util/promise' +import { getProbability } from '../../common/calculate' export const resolveMarket = functions .runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] }) @@ -31,140 +41,203 @@ export const resolveMarket = functions ) => { const userId = context?.auth?.uid if (!userId) return { status: 'error', message: 'Not authorized' } - - const { outcome, contractId, probabilityInt, resolutions, value } = data - - const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractDoc = firestore.doc(`contracts/${data.contractId}`) const contractSnap = await contractDoc.get() if (!contractSnap.exists) return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract - const { creatorId, outcomeType, closeTime } = contract + if (contract.creatorId !== userId) + return { status: 'error', message: 'User not creator of contract' } - if (outcomeType === 'BINARY') { - if (!RESOLUTIONS.includes(outcome)) - return { status: 'error', message: 'Invalid outcome' } - } else if (outcomeType === 'FREE_RESPONSE') { - if ( - isNaN(+outcome) && - !(outcome === 'MKT' && resolutions) && - outcome !== 'CANCEL' - ) - return { status: 'error', message: 'Invalid outcome' } - } else if (outcomeType === 'NUMERIC') { - if (isNaN(+outcome) && outcome !== 'CANCEL') - return { status: 'error', message: 'Invalid outcome' } - } else { - return { status: 'error', message: 'Invalid contract outcomeType' } - } + return privateResolveMarket(contract, data) + } + ) - if (value !== undefined && !isFinite(value)) - return { status: 'error', message: 'Invalid value' } +export const autoResolveMarkets = functions.pubsub + .schedule('every 1 minutes') + .onRun(async () => { + const contracts = await getValues( + firestore + .collection('contracts') + .where('isResolved', '==', false) + .where('closeTime', '>', Date.now()) + .where('autoResolutionTime', '<', Date.now()) + ) + await batchedWaitAll( + contracts.map((contract) => async () => { + const result = await autoResolve(contract) + + console.log('resolved', contract.slug, 'result:', result) + }) + ) + }) + +const autoResolve = async (contract: Contract) => { + const data = { + outcome: AUTO_RESOLUTION, + value: undefined, // numeric + probabilityInt: + contract.outcomeType == 'BINARY' + ? getProbability(contract) * 100 + : undefined, + resolutions: + contract.outcomeType == 'FREE_RESPONSE' + ? getFreeResponseResolutions(contract) + : undefined, + } + contract.description = contract.description.concat( + `\n\n\nContract resolved automatically.` + ) + + return await privateResolveMarket(contract, data) +} + +const getFreeResponseResolutions = (contract: Contract & FreeResponse) => { + const answersWithProbs = getAnswersWithProbs(contract) + const totalProb = sum(Object.values(answersWithProbs)) + return mapValues(answersWithProbs, (prob) => (100 * prob) / totalProb) +} + +const getAnswersWithProbs = (contract: Contract & FreeResponse) => { + const answers: { [id: string]: number } = {} + for (const answer of contract.answers) { + answers[answer.id] = getDpmOutcomeProbability( + contract.totalShares, + answer.id + ) + } + return answers +} + +const privateResolveMarket = async ( + contract: Contract, + data: { + outcome: resolution + value?: number + probabilityInt?: number + resolutions?: { [outcome: string]: number } + } +) => { + const { creatorId, id, outcomeType, closeTime } = contract + const { outcome, probabilityInt, resolutions, value } = data + switch (outcomeType) { + case 'FREE_RESPONSE': + if ( + isNaN(+outcome) && + !(outcome === 'MKT' && resolutions) && + outcome !== 'CANCEL' + ) + return { status: 'error', message: 'Invalid outcome' } + break + case 'NUMERIC': + if (isNaN(+outcome) && outcome !== 'CANCEL') + return { status: 'error', message: 'Invalid outcome' } + break + case 'BINARY': + if (!RESOLUTIONS.includes(outcome)) + return { status: 'error', message: 'Invalid outcome' } if ( - outcomeType === 'BINARY' && probabilityInt !== undefined && (probabilityInt < 0 || probabilityInt > 100 || !isFinite(probabilityInt)) ) return { status: 'error', message: 'Invalid probability' } + } - if (creatorId !== userId) - return { status: 'error', message: 'User not creator of contract' } + if (value && !isFinite(value)) + return { status: 'error', message: 'Invalid value' } - if (contract.resolution) - return { status: 'error', message: 'Contract already resolved' } + if (contract.resolution) + return { status: 'error', message: 'Contract already resolved' } - const creator = await getUser(creatorId) - if (!creator) return { status: 'error', message: 'Creator not found' } + const creator = await getUser(creatorId) + if (!creator) return { status: 'error', message: 'Creator not found' } - const resolutionProbability = - probabilityInt !== undefined ? probabilityInt / 100 : undefined + const resolutionProbability = + probabilityInt !== undefined ? probabilityInt / 100 : undefined - const resolutionTime = Date.now() - const newCloseTime = closeTime - ? Math.min(closeTime, resolutionTime) - : closeTime + const resolutionTime = Date.now() + const newCloseTime = closeTime + ? Math.min(closeTime, resolutionTime) + : closeTime - const betsSnap = await firestore - .collection(`contracts/${contractId}/bets`) - .get() + const betsSnap = await firestore.collection(`contracts/${id}/bets`).get() - const bets = betsSnap.docs.map((doc) => doc.data() as Bet) + const bets = betsSnap.docs.map((doc) => doc.data() as Bet) - const liquiditiesSnap = await firestore - .collection(`contracts/${contractId}/liquidity`) - .get() + const liquiditiesSnap = await firestore + .collection(`contracts/${id}/liquidity`) + .get() - const liquidities = liquiditiesSnap.docs.map( - (doc) => doc.data() as LiquidityProvision - ) - - const { payouts, creatorPayout, liquidityPayouts, collectedFees } = - getPayouts( - outcome, - resolutions ?? {}, - contract, - bets, - liquidities, - resolutionProbability - ) - - await contractDoc.update( - removeUndefinedProps({ - isResolved: true, - resolution: outcome, - resolutionValue: value, - resolutionTime, - closeTime: newCloseTime, - resolutionProbability, - resolutions, - collectedFees, - }) - ) - - console.log('contract ', contractId, 'resolved to:', outcome) - - const openBets = bets.filter((b) => !b.isSold && !b.sale) - const loanPayouts = getLoanPayouts(openBets) - - if (!isProd) - console.log( - 'payouts:', - payouts, - 'creator payout:', - creatorPayout, - 'liquidity payout:' - ) - - if (creatorPayout) - await processPayouts( - [{ userId: creatorId, payout: creatorPayout }], - true - ) - - await processPayouts(liquidityPayouts, true) - - const result = await processPayouts([...payouts, ...loanPayouts]) - - const userPayoutsWithoutLoans = groupPayoutsByUser(payouts) - - await sendResolutionEmails( - openBets, - userPayoutsWithoutLoans, - creator, - creatorPayout, - contract, - outcome, - resolutionProbability, - resolutions - ) - - return result - } + const liquidities = liquiditiesSnap.docs.map( + (doc) => doc.data() as LiquidityProvision ) + const { payouts, creatorPayout, liquidityPayouts, collectedFees } = + getPayouts( + outcome, + resolutions ?? {}, + contract, + bets, + liquidities, + resolutionProbability + ) + + const contractDoc = firestore.doc(`contracts/${contract.id}`) + + await contractDoc.update( + removeUndefinedProps({ + isResolved: true, + resolution: outcome, + resolutionValue: value, + resolutionTime, + description: contract.description, + closeTime: newCloseTime, + resolutionProbability, + resolutions, + collectedFees, + }) + ) + + console.log('contract ', id, 'resolved to:', outcome) + + const openBets = bets.filter((b) => !b.isSold && !b.sale) + const loanPayouts = getLoanPayouts(openBets) + + if (!isProd) + console.log( + 'payouts:', + payouts, + 'creator payout:', + creatorPayout, + 'liquidity payout:' + ) + + if (creatorPayout) + await processPayouts([{ userId: creatorId, payout: creatorPayout }], true) + + await processPayouts(liquidityPayouts, true) + + const result = await processPayouts([...payouts, ...loanPayouts]) + + const userPayoutsWithoutLoans = groupPayoutsByUser(payouts) + + await sendResolutionEmails( + openBets, + userPayoutsWithoutLoans, + creator, + creatorPayout, + contract, + outcome, + resolutionProbability, + resolutions + ) + + return result +} + const processPayouts = async (payouts: Payout[], isDeposit = false) => { const userPayouts = groupPayoutsByUser(payouts) diff --git a/functions/src/scripts/add-auto-resolution.ts b/functions/src/scripts/add-auto-resolution.ts new file mode 100644 index 00000000..45e4b66c --- /dev/null +++ b/functions/src/scripts/add-auto-resolution.ts @@ -0,0 +1,53 @@ +// Existing contracts don't have auto resolutions. Let's add it. + +import * as admin from 'firebase-admin' + +import { initAdmin } from './script-init' +initAdmin() + +import { getValues } from '../utils' +import { Contract } from '../../../common/contract' +import { DAY_MS } from '../../../common/util/time' +import { batchedWaitAll } from '../../../common/util/promise' + +const firestore = admin.firestore() + +async function addAutoResolutionToContracts() { + console.log('Adding auto resolution to existing contracts') + + const contracts = await getValues( + firestore.collection('contracts').where('isResolved', '==', false) + ) + + console.log('Loaded', contracts.length, 'contracts') + + await batchedWaitAll( + contracts.map((c) => () => addAutoResolutionToContract(c)) + ) +} + +async function addAutoResolutionToContract(contract: Contract) { + if (contract.autoResolutionTime) { + console.log('Skipping, already has auto resolution', contract.slug) + return + } + const contractRef = firestore.doc(`contracts/${contract.id}`) + if (!contract.closeTime) { + console.error('Has no close time, please check manually', contract.slug) + return + } + + const autoResolutionTime = + contract.closeTime > Date.now() + ? contract.closeTime + 7 * DAY_MS + : Date.now() + 14 * DAY_MS + + console.log('Adding auto resolution', contract.slug) + + await contractRef.update({ + autoResolutionTime: autoResolutionTime, + } as Partial) +} + +if (require.main === module) + addAutoResolutionToContracts().then(() => process.exit()) diff --git a/functions/src/scripts/add-close-time.ts b/functions/src/scripts/add-close-time.ts new file mode 100644 index 00000000..ecae4063 --- /dev/null +++ b/functions/src/scripts/add-close-time.ts @@ -0,0 +1,108 @@ +// Some markets don't have a close time. Let's add it. + +import * as admin from 'firebase-admin' + +import { initAdmin } from './script-init' +initAdmin() + +import { getValues } from '../utils' +import { Contract } from '../../../common/contract' +import { batchedWaitAll } from '../../../common/util/promise' + +const firestore = admin.firestore() + +async function addCloseTimeToContracts() { + console.log('Adding close times to existing contracts') + + const contracts = await getValues( + firestore.collection('contracts').where('isResolved', '==', false) + ) + + console.log('Loaded', contracts.length, 'contracts') + + await batchedWaitAll(contracts.map((c) => () => addCloseTimeToContract(c))) +} + +async function addCloseTimeToContract(contract: Contract) { + if (contract.closeTime) { + return + } + const closeTime = closeTimes.get(contract.slug) + if (!closeTime) { + console.error('No close time found', contract.slug) + return + } + const contractRef = firestore.doc(`contracts/${contract.id}`) + await contractRef.update({ + closeTime, + } as Partial) + console.log('Added close time', contract.slug, new Date(closeTime)) +} + +const closeTimes = new Map([ + ['will-apple-ship-its-ar-glasses-by-e', 1672531200000], + ['will-ethereum-switch-to-proof-of-st', 1672531200000], + ['will-mantic-markets-have-over-1m', 1672531200000], + ['will-aoc-challenge-chuck-schumer-in', 1672531200000], + ['nancy-pelosi-announces-retirement-p', 1672531200000], + ['will-activisionblizzard-solve-its-r', 1672531200000], + ['test', 1656547200000], + ['will-spacex-become-a-publicly-trade', 1672531200000], + ['mantic-will-airdrop-crypto-to-early', 1656633600000], + ['will-the-homicide-rate-in-2022-rema', 1704067200000], + ['in-us-pandemic-fades-away-to-predel', 1672531200000], + ['will-we-discover-life-on-mars-befor', 1704067200000], + ['november-2022-yearonyear-cpi-growth', 1672531200000], + ['will-2060-globally-be-warmer-than-2', 2871763200000], + ['will-starship-reach-orbit-by-the-en', 1672531200000], + ['will-the-runnerup-in-the-2024-us-pr', 1735689600000], + ['will-joe-rogan-interview-a-guest-ab', 1672531200000], + ['the-unemployment-rate-stays-between', 1672531200000], + ['restaurant-and-retail-spending-cont', 1672531200000], + ['will-at-the-end-of-2022-western-tee', 1672531200000], + ['will-chinese-economic-growth-drop-b', 1924992000000], + ['us-authorizes-another-covid-booster', 1672531200000], + ['will-fbi-statistics-show-homicides-', 1672531200000], + ['will-dwayne-johnson-win-the-2024-us', 1737331200000], + ['democrats-go-down-at-least-one-gove', 1672531200000], + ['will-congress-hold-any-hearings-abo', 1672531200000], + ['will-there-be-a-2022-sarscov2-varia', 1672531200000], + ['will-there-be-a-federal-mask-requir', 1667865600000], + ['no-military-conflict-between-the-pr', 1672531200000], + ['will-redditcomrslatestarcodex-have-', 1656633600000], + ['we-will-be-getting-boosters-modifie', 1661990400000], + ['will-pete-buttigieg-be-the-2024-dem', 1735689600000], + ['omicron-has-a-100-or-bigger-transmi', 1672531200000], + ['will-apple-reach-a-market-capitaliz', 1672531200000], + ['will-the-median-rent-for-a-1bedroom', 1672531200000], + ['hillary-clinton-signals-in-any-way-', 1735689600000], + ['will-james-webb-space-telescope-dep', 1659312000000], + ['fullselfdriving-robotaxis-generally', 1704067200000], + ['will-circular-economy-become-mainst', 2272147200000], + ['joe-biden-is-still-president-at-the', 1672531200000], + ['will-bit-coin-hit-100k-this-year', 1672531200000], + ['democrats-lose-both-houses-of-congr', 1672531200000], + ['will-teslas-cybertruck-go-into-full', 1672531200000], + ['will-the-sp-500-trade-below-3800-in', 1672531200000], + ['will-chicago-have-more-than-12-inch', 1656547200000], + ['will-a-major-norwegian-political-pa-58167546884aa', 1672531200000], + ['will-i-be-a-regular-user-of-this-we', 1672531200000], + ['will-apple-sell-an-apple-branded-ar', 1669852800000], + ['at-the-end-of-its-ipo-day-will-redd', 1672531200000], + ['will-any-major-known-associates-of-', 1672531200000], + ['will-donald-trump-be-the-republican', 1735689600000], + ['will-solana-have-a-higher-market-ca', 1672531200000], + ['will-congress-hold-any-hearings-abo-e21f987033b3', 1672531200000], + ['will-ethereum-overtake-bitcoin-in-t', 1672531200000], + ['china-officially-abandons-covid-zer', 1672531200000], + ['privacy-tokens-will-outgrow-status-', 1672531200000], + ['republicans-will-win-the-2022-texas', 1669852800000], + ['will-at-least-75-of-the-usa-covid19', 1677542400000], + ['liz-cheney-loses-primary-in-2022', 1672531200000], + ['will-the-us-inflation-rate-for-2022', 1688169600000], + ['will-republicans-win-enough-seats-i', 1669852800000], + ['will-the-world-experience-a-solar-s', 1672531200000], +]) + +if (require.main === module) + addCloseTimeToContracts().then(() => process.exit()) diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 9f9c0936..948ce5ce 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -23,6 +23,7 @@ import { Bet } from 'common/bet' import NewContractBadge from '../new-contract-badge' import { CATEGORY_LIST } from 'common/categories' import { TagsList } from '../tags-list' +import { DAY_MS } from 'common/util/time' import { UserFollowButton } from '../follow-button' export function MiscDetails(props: { @@ -160,14 +161,19 @@ export function ContractDetails(props: { )} )} -
{volumeLabel}
- {!disabled && } + {!disabled && ( + + )} ) } @@ -210,15 +216,32 @@ function EditableCloseDate(props: { const newCloseTime = dayjs(closeDate).valueOf() if (newCloseTime === closeTime) setIsEditingCloseTime(false) else if (newCloseTime > Date.now()) { - const { description } = contract + const { description, autoResolutionTime } = contract const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a') - const newDescription = `${description}\n\nClose date updated to ${formattedCloseDate}` + let newDescription = description.concat( + `\n\nClose date updated to ${formattedCloseDate}` + ) - updateContract(contract.id, { + const update: Partial = { closeTime: newCloseTime, - description: newDescription, - }) + } + if (autoResolutionTime) { + const newAutoResolutionTime = newCloseTime + 7 * DAY_MS + if (newAutoResolutionTime >= autoResolutionTime) { + update.autoResolutionTime = newAutoResolutionTime + const formattedNewAutoResolutionTime = dayjs( + newAutoResolutionTime + ).format('YYYY-MM-DD h:mm a') + newDescription = newDescription.concat( + `\nAuto resolution date updated to ${formattedNewAutoResolutionTime}` + ) + } + } + + update.description = newDescription + + updateContract(contract.id, update) setIsEditingCloseTime(false) } } diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 1b102ab4..81810d2d 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -1,4 +1,8 @@ -import { DotsHorizontalIcon } from '@heroicons/react/outline' +import { + DotsHorizontalIcon, + PencilIcon, + CheckIcon, +} from '@heroicons/react/outline' import clsx from 'clsx' import dayjs from 'dayjs' import { uniqBy } from 'lodash' @@ -11,6 +15,7 @@ import { contractPath, contractPool, getBinaryProbPercent, + updateContract, } from 'web/lib/firebase/contracts' import { LiquidityPanel } from '../liquidity-panel' import { CopyLinkButton } from '../copy-link-button' @@ -23,15 +28,25 @@ import { Title } from '../title' import { TweetButton } from '../tweet-button' import { InfoTooltip } from '../info-tooltip' -export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { - const { contract, bets } = props +const formatTime = (dt: number) => dayjs(dt).format('MMM DD, YYYY hh:mm a z') + +export function ContractInfoDialog(props: { + contract: Contract + bets: Bet[] + isCreator: boolean +}) { + const { contract, bets, isCreator } = props const [open, setOpen] = useState(false) - const formatTime = (dt: number) => dayjs(dt).format('MMM DD, YYYY hh:mm a z') - - const { createdTime, closeTime, resolutionTime, mechanism, outcomeType } = - contract + const { + createdTime, + closeTime, + resolutionTime, + mechanism, + outcomeType, + autoResolutionTime, + } = contract const tradersCount = uniqBy( bets.filter((bet) => !bet.isAnte), @@ -116,6 +131,14 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { )} + {autoResolutionTime && !resolutionTime && ( + + )} + {resolutionTime && ( Market resolved @@ -180,3 +203,72 @@ const getTweetText = (contract: Contract, isCreator: boolean) => { return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}` } + +export function EditableResolutionTime(props: { + time: number + contract: Contract + isCreator: boolean +}) { + const { time, contract, isCreator } = props + + const [isEditing, setIsEditing] = useState(false) + const [timeString, setTimeString] = useState(time && formatTime(time)) + + const onSave = () => { + const newTime = dayjs(timeString).valueOf() + if (newTime === time) setIsEditing(false) + else if ( + contract.closeTime && + newTime > (contract.closeTime ?? Date.now()) + ) { + const formattedTime = dayjs(newTime).format('YYYY-MM-DD h:mm a') + const newDescription = `${contract.description}\n\nAuto resolution date updated to ${formattedTime}` + + updateContract(contract.id, { + autoResolutionTime: newTime, + description: newDescription, + }) + + setIsEditing(false) + } + } + + return ( + + + Market autoresolves + {isCreator && + (isEditing ? ( + + ) : ( + + ))} + + + {isEditing ? ( +
+ e.stopPropagation()} + onChange={(e) => setTimeString(e.target.value || '')} + min={contract.closeTime} + value={timeString} + /> +
+ ) : ( +
+ {formatTime(time)} +
+ )} + + + ) +} diff --git a/web/components/layout/modal.tsx b/web/components/layout/modal.tsx index d61a38dd..4817d072 100644 --- a/web/components/layout/modal.tsx +++ b/web/components/layout/modal.tsx @@ -45,7 +45,7 @@ export function Modal(props: { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
{children}
diff --git a/web/pages/api/v0/_types.ts b/web/pages/api/v0/_types.ts index 78fe3708..57903f44 100644 --- a/web/pages/api/v0/_types.ts +++ b/web/pages/api/v0/_types.ts @@ -32,6 +32,8 @@ export type LiteMarket = { volume7Days: number volume24Hours: number + autoResolutionTime?: number + isResolved: boolean resolution?: string resolutionTime?: number @@ -64,6 +66,7 @@ export function toLiteMarket(contract: Contract): LiteMarket { volume, volume7Days, volume24Hours, + autoResolutionTime, isResolved, resolution, resolutionTime, @@ -97,6 +100,7 @@ export function toLiteMarket(contract: Contract): LiteMarket { volume, volume7Days, volume24Hours, + autoResolutionTime, isResolved, resolution, resolutionTime,