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 commit22f59adc9c
. * 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 commit4de86d5b08
) * Added comments for leading semicolons (cherry picked from commit60739c7853
) * 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 commit944de9398a
) * 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 commit09aea5c207
. * 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 commit8380bf4f72
. * 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
This commit is contained in:
parent
cb64703905
commit
a3663d03e8
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,4 +2,4 @@
|
||||||
.idea/
|
.idea/
|
||||||
.vercel
|
.vercel
|
||||||
node_modules
|
node_modules
|
||||||
yarn-error.log
|
yarn-error.log
|
|
@ -31,9 +31,9 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
closeTime?: number // When no more trading is allowed
|
closeTime?: number // When no more trading is allowed
|
||||||
|
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
resolutionTime?: number // When the contract creator resolved the market
|
resolutionTime?: number // When the market is resolved
|
||||||
resolution?: string
|
resolution?: string
|
||||||
|
autoResolutionTime?: number // When the market will be resolved automatically
|
||||||
closeEmailsSent?: number
|
closeEmailsSent?: number
|
||||||
|
|
||||||
volume: number
|
volume: number
|
||||||
|
@ -90,10 +90,12 @@ export type Numeric = {
|
||||||
resolutionValue?: number
|
resolutionValue?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type contractField = keyof Contract
|
||||||
export type outcomeType = AnyOutcomeType['outcomeType']
|
export type outcomeType = AnyOutcomeType['outcomeType']
|
||||||
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||||
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
||||||
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'NUMERIC'] 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_QUESTION_LENGTH = 480
|
||||||
export const MAX_DESCRIPTION_LENGTH = 10000
|
export const MAX_DESCRIPTION_LENGTH = 10000
|
||||||
|
|
|
@ -27,7 +27,8 @@ export function getNewContract(
|
||||||
// used for numeric markets
|
// used for numeric markets
|
||||||
bucketCount: number,
|
bucketCount: number,
|
||||||
min: number,
|
min: number,
|
||||||
max: number
|
max: number,
|
||||||
|
autoResolutionTime?: number
|
||||||
) {
|
) {
|
||||||
const tags = parseTags(
|
const tags = parseTags(
|
||||||
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
|
||||||
|
@ -59,6 +60,7 @@ export function getNewContract(
|
||||||
isResolved: false,
|
isResolved: false,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
closeTime,
|
closeTime,
|
||||||
|
autoResolutionTime,
|
||||||
|
|
||||||
volume: 0,
|
volume: 0,
|
||||||
volume24Hours: 0,
|
volume24Hours: 0,
|
||||||
|
|
|
@ -56,7 +56,7 @@ service cloud.firestore {
|
||||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['tags', 'lowercaseTags']);
|
.hasOnly(['tags', 'lowercaseTags']);
|
||||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['description', 'closeTime'])
|
.hasOnly(['description', 'closeTime', 'autoResolutionTime'])
|
||||||
&& resource.data.creatorId == request.auth.uid;
|
&& resource.data.creatorId == request.auth.uid;
|
||||||
allow update: if isAdmin();
|
allow update: if isAdmin();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
import { getNoneAnswer } from '../../common/answer'
|
import { getNoneAnswer } from '../../common/answer'
|
||||||
import { getNewContract } from '../../common/new-contract'
|
import { getNewContract } from '../../common/new-contract'
|
||||||
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
||||||
|
import { DAY_MS } from '../../common/util/time'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
|
@ -64,6 +65,8 @@ export const createmarket = newEndpoint(['POST'], async (req, auth) => {
|
||||||
;({ initialProb } = validate(binarySchema, req.body))
|
;({ initialProb } = validate(binarySchema, req.body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autoResolutionTime = closeTime.getTime() + 7 * DAY_MS
|
||||||
|
|
||||||
// Uses utc time on server:
|
// Uses utc time on server:
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
let freeMarketResetTime = new Date().setUTCHours(16, 0, 0, 0)
|
let freeMarketResetTime = new Date().setUTCHours(16, 0, 0, 0)
|
||||||
|
@ -113,6 +116,7 @@ export const createmarket = newEndpoint(['POST'], async (req, auth) => {
|
||||||
ante,
|
ante,
|
||||||
closeTime.getTime(),
|
closeTime.getTime(),
|
||||||
tags ?? [],
|
tags ?? [],
|
||||||
|
autoResolutionTime,
|
||||||
NUMERIC_BUCKET_COUNT,
|
NUMERIC_BUCKET_COUNT,
|
||||||
min ?? 0,
|
min ?? 0,
|
||||||
max ?? 0
|
max ?? 0
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { 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 { User } from '../../common/user'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getUser, isProd, payUser } from './utils'
|
import { getUser, isProd, payUser } from './utils'
|
||||||
|
@ -15,6 +21,10 @@ import {
|
||||||
} from '../../common/payouts'
|
} from '../../common/payouts'
|
||||||
import { removeUndefinedProps } from '../../common/util/object'
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
import { LiquidityProvision } from '../../common/liquidity-provision'
|
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
|
export const resolveMarket = functions
|
||||||
.runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] })
|
.runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] })
|
||||||
|
@ -31,140 +41,203 @@ export const resolveMarket = functions
|
||||||
) => {
|
) => {
|
||||||
const userId = context?.auth?.uid
|
const userId = context?.auth?.uid
|
||||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
const contractDoc = firestore.doc(`contracts/${data.contractId}`)
|
||||||
const { outcome, contractId, probabilityInt, resolutions, value } = data
|
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
|
||||||
const contractSnap = await contractDoc.get()
|
const contractSnap = await contractDoc.get()
|
||||||
if (!contractSnap.exists)
|
if (!contractSnap.exists)
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
const { creatorId, outcomeType, closeTime } = contract
|
if (contract.creatorId !== userId)
|
||||||
|
return { status: 'error', message: 'User not creator of contract' }
|
||||||
|
|
||||||
if (outcomeType === 'BINARY') {
|
return privateResolveMarket(contract, data)
|
||||||
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' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== undefined && !isFinite(value))
|
export const autoResolveMarkets = functions.pubsub
|
||||||
return { status: 'error', message: 'Invalid value' }
|
.schedule('every 1 minutes')
|
||||||
|
.onRun(async () => {
|
||||||
|
const contracts = await getValues<Contract>(
|
||||||
|
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 (
|
if (
|
||||||
outcomeType === 'BINARY' &&
|
|
||||||
probabilityInt !== undefined &&
|
probabilityInt !== undefined &&
|
||||||
(probabilityInt < 0 ||
|
(probabilityInt < 0 ||
|
||||||
probabilityInt > 100 ||
|
probabilityInt > 100 ||
|
||||||
!isFinite(probabilityInt))
|
!isFinite(probabilityInt))
|
||||||
)
|
)
|
||||||
return { status: 'error', message: 'Invalid probability' }
|
return { status: 'error', message: 'Invalid probability' }
|
||||||
|
}
|
||||||
|
|
||||||
if (creatorId !== userId)
|
if (value && !isFinite(value))
|
||||||
return { status: 'error', message: 'User not creator of contract' }
|
return { status: 'error', message: 'Invalid value' }
|
||||||
|
|
||||||
if (contract.resolution)
|
if (contract.resolution)
|
||||||
return { status: 'error', message: 'Contract already resolved' }
|
return { status: 'error', message: 'Contract already resolved' }
|
||||||
|
|
||||||
const creator = await getUser(creatorId)
|
const creator = await getUser(creatorId)
|
||||||
if (!creator) return { status: 'error', message: 'Creator not found' }
|
if (!creator) return { status: 'error', message: 'Creator not found' }
|
||||||
|
|
||||||
const resolutionProbability =
|
const resolutionProbability =
|
||||||
probabilityInt !== undefined ? probabilityInt / 100 : undefined
|
probabilityInt !== undefined ? probabilityInt / 100 : undefined
|
||||||
|
|
||||||
const resolutionTime = Date.now()
|
const resolutionTime = Date.now()
|
||||||
const newCloseTime = closeTime
|
const newCloseTime = closeTime
|
||||||
? Math.min(closeTime, resolutionTime)
|
? Math.min(closeTime, resolutionTime)
|
||||||
: closeTime
|
: closeTime
|
||||||
|
|
||||||
const betsSnap = await firestore
|
const betsSnap = await firestore.collection(`contracts/${id}/bets`).get()
|
||||||
.collection(`contracts/${contractId}/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
|
const liquiditiesSnap = await firestore
|
||||||
.collection(`contracts/${contractId}/liquidity`)
|
.collection(`contracts/${id}/liquidity`)
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
const liquidities = liquiditiesSnap.docs.map(
|
const liquidities = liquiditiesSnap.docs.map(
|
||||||
(doc) => doc.data() as LiquidityProvision
|
(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 { 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 processPayouts = async (payouts: Payout[], isDeposit = false) => {
|
||||||
const userPayouts = groupPayoutsByUser(payouts)
|
const userPayouts = groupPayoutsByUser(payouts)
|
||||||
|
|
||||||
|
|
53
functions/src/scripts/add-auto-resolution.ts
Normal file
53
functions/src/scripts/add-auto-resolution.ts
Normal file
|
@ -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<Contract>(
|
||||||
|
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<Contract>)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module)
|
||||||
|
addAutoResolutionToContracts().then(() => process.exit())
|
108
functions/src/scripts/add-close-time.ts
Normal file
108
functions/src/scripts/add-close-time.ts
Normal file
|
@ -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<Contract>(
|
||||||
|
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<Contract>)
|
||||||
|
console.log('Added close time', contract.slug, new Date(closeTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeTimes = new Map<string, number>([
|
||||||
|
['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())
|
|
@ -23,6 +23,7 @@ import { Bet } from 'common/bet'
|
||||||
import NewContractBadge from '../new-contract-badge'
|
import NewContractBadge from '../new-contract-badge'
|
||||||
import { CATEGORY_LIST } from 'common/categories'
|
import { CATEGORY_LIST } from 'common/categories'
|
||||||
import { TagsList } from '../tags-list'
|
import { TagsList } from '../tags-list'
|
||||||
|
import { DAY_MS } from 'common/util/time'
|
||||||
import { UserFollowButton } from '../follow-button'
|
import { UserFollowButton } from '../follow-button'
|
||||||
|
|
||||||
export function MiscDetails(props: {
|
export function MiscDetails(props: {
|
||||||
|
@ -160,14 +161,19 @@ export function ContractDetails(props: {
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Row className="items-center gap-1">
|
<Row className="items-center gap-1">
|
||||||
<DatabaseIcon className="h-5 w-5" />
|
<DatabaseIcon className="h-5 w-5" />
|
||||||
|
|
||||||
<div className="whitespace-nowrap">{volumeLabel}</div>
|
<div className="whitespace-nowrap">{volumeLabel}</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{!disabled && <ContractInfoDialog contract={contract} bets={bets} />}
|
{!disabled && (
|
||||||
|
<ContractInfoDialog
|
||||||
|
contract={contract}
|
||||||
|
bets={bets}
|
||||||
|
isCreator={isCreator ?? false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -210,15 +216,32 @@ function EditableCloseDate(props: {
|
||||||
const newCloseTime = dayjs(closeDate).valueOf()
|
const newCloseTime = dayjs(closeDate).valueOf()
|
||||||
if (newCloseTime === closeTime) setIsEditingCloseTime(false)
|
if (newCloseTime === closeTime) setIsEditingCloseTime(false)
|
||||||
else if (newCloseTime > Date.now()) {
|
else if (newCloseTime > Date.now()) {
|
||||||
const { description } = contract
|
const { description, autoResolutionTime } = contract
|
||||||
const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a')
|
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<Contract> = {
|
||||||
closeTime: newCloseTime,
|
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)
|
setIsEditingCloseTime(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { DotsHorizontalIcon } from '@heroicons/react/outline'
|
import {
|
||||||
|
DotsHorizontalIcon,
|
||||||
|
PencilIcon,
|
||||||
|
CheckIcon,
|
||||||
|
} from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { uniqBy } from 'lodash'
|
import { uniqBy } from 'lodash'
|
||||||
|
@ -11,6 +15,7 @@ import {
|
||||||
contractPath,
|
contractPath,
|
||||||
contractPool,
|
contractPool,
|
||||||
getBinaryProbPercent,
|
getBinaryProbPercent,
|
||||||
|
updateContract,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
import { LiquidityPanel } from '../liquidity-panel'
|
import { LiquidityPanel } from '../liquidity-panel'
|
||||||
import { CopyLinkButton } from '../copy-link-button'
|
import { CopyLinkButton } from '../copy-link-button'
|
||||||
|
@ -23,15 +28,25 @@ import { Title } from '../title'
|
||||||
import { TweetButton } from '../tweet-button'
|
import { TweetButton } from '../tweet-button'
|
||||||
import { InfoTooltip } from '../info-tooltip'
|
import { InfoTooltip } from '../info-tooltip'
|
||||||
|
|
||||||
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
const formatTime = (dt: number) => dayjs(dt).format('MMM DD, YYYY hh:mm a z')
|
||||||
const { contract, bets } = props
|
|
||||||
|
export function ContractInfoDialog(props: {
|
||||||
|
contract: Contract
|
||||||
|
bets: Bet[]
|
||||||
|
isCreator: boolean
|
||||||
|
}) {
|
||||||
|
const { contract, bets, isCreator } = props
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const formatTime = (dt: number) => dayjs(dt).format('MMM DD, YYYY hh:mm a z')
|
const {
|
||||||
|
createdTime,
|
||||||
const { createdTime, closeTime, resolutionTime, mechanism, outcomeType } =
|
closeTime,
|
||||||
contract
|
resolutionTime,
|
||||||
|
mechanism,
|
||||||
|
outcomeType,
|
||||||
|
autoResolutionTime,
|
||||||
|
} = contract
|
||||||
|
|
||||||
const tradersCount = uniqBy(
|
const tradersCount = uniqBy(
|
||||||
bets.filter((bet) => !bet.isAnte),
|
bets.filter((bet) => !bet.isAnte),
|
||||||
|
@ -116,6 +131,14 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{autoResolutionTime && !resolutionTime && (
|
||||||
|
<EditableResolutionTime
|
||||||
|
time={autoResolutionTime}
|
||||||
|
contract={contract}
|
||||||
|
isCreator={isCreator}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{resolutionTime && (
|
{resolutionTime && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Market resolved</td>
|
<td>Market resolved</td>
|
||||||
|
@ -180,3 +203,72 @@ const getTweetText = (contract: Contract, isCreator: boolean) => {
|
||||||
|
|
||||||
return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
|
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 (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Market autoresolves
|
||||||
|
{isCreator &&
|
||||||
|
(isEditing ? (
|
||||||
|
<button className="btn btn-xs btn-ghost" onClick={onSave}>
|
||||||
|
<CheckIcon className="inline h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-ghost"
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
>
|
||||||
|
<PencilIcon className="inline h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="form-control mr-1 items-start">
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="input input-xs"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setTimeString(e.target.value || '')}
|
||||||
|
min={contract.closeTime}
|
||||||
|
value={timeString}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="form-control mr-1 items-start">
|
||||||
|
{formatTime(time)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ export function Modal(props: {
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<div className="inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle">
|
<div className="inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
|
@ -32,6 +32,8 @@ export type LiteMarket = {
|
||||||
volume7Days: number
|
volume7Days: number
|
||||||
volume24Hours: number
|
volume24Hours: number
|
||||||
|
|
||||||
|
autoResolutionTime?: number
|
||||||
|
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
resolution?: string
|
resolution?: string
|
||||||
resolutionTime?: number
|
resolutionTime?: number
|
||||||
|
@ -64,6 +66,7 @@ export function toLiteMarket(contract: Contract): LiteMarket {
|
||||||
volume,
|
volume,
|
||||||
volume7Days,
|
volume7Days,
|
||||||
volume24Hours,
|
volume24Hours,
|
||||||
|
autoResolutionTime,
|
||||||
isResolved,
|
isResolved,
|
||||||
resolution,
|
resolution,
|
||||||
resolutionTime,
|
resolutionTime,
|
||||||
|
@ -97,6 +100,7 @@ export function toLiteMarket(contract: Contract): LiteMarket {
|
||||||
volume,
|
volume,
|
||||||
volume7Days,
|
volume7Days,
|
||||||
volume24Hours,
|
volume24Hours,
|
||||||
|
autoResolutionTime,
|
||||||
isResolved,
|
isResolved,
|
||||||
resolution,
|
resolution,
|
||||||
resolutionTime,
|
resolutionTime,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user