Inga/admin rules resolve (#880)

* Giving admin permission to resolve all markets that have closed after 7 days.
This commit is contained in:
ingawei 2022-09-14 22:28:40 -05:00 committed by GitHub
parent 9aa56dd193
commit ccf02bdba8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 99 additions and 29 deletions

View File

@ -21,7 +21,10 @@ export function isWhitelisted(email?: string) {
} }
// TODO: Before open sourcing, we should turn these into env vars // TODO: Before open sourcing, we should turn these into env vars
export function isAdmin(email: string) { export function isAdmin(email?: string) {
if (!email) {
return false
}
return ENV_CONFIG.adminEmails.includes(email) return ENV_CONFIG.adminEmails.includes(email)
} }

View File

@ -74,6 +74,7 @@ export const PROD_CONFIG: EnvConfig = {
'iansphilips@gmail.com', // Ian 'iansphilips@gmail.com', // Ian
'd4vidchee@gmail.com', // D4vid 'd4vidchee@gmail.com', // D4vid
'federicoruizcassarino@gmail.com', // Fede 'federicoruizcassarino@gmail.com', // Fede
'ingawei@gmail.com', //Inga
], ],
visibility: 'PUBLIC', visibility: 'PUBLIC',

View File

@ -14,7 +14,8 @@ service cloud.firestore {
'manticmarkets@gmail.com', 'manticmarkets@gmail.com',
'iansphilips@gmail.com', 'iansphilips@gmail.com',
'd4vidchee@gmail.com', 'd4vidchee@gmail.com',
'federicoruizcassarino@gmail.com' 'federicoruizcassarino@gmail.com',
'ingawei@gmail.com'
] ]
} }

View File

@ -16,7 +16,7 @@ import {
groupPayoutsByUser, groupPayoutsByUser,
Payout, Payout,
} from '../../common/payouts' } from '../../common/payouts'
import { isManifoldId } from '../../common/envs/constants' import { isAdmin, isManifoldId } from '../../common/envs/constants'
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 { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
@ -76,13 +76,18 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
throw new APIError(404, 'No contract exists with the provided ID') throw new APIError(404, 'No contract exists with the provided ID')
const contract = contractSnap.data() as Contract const contract = contractSnap.data() as Contract
const { creatorId, closeTime } = contract const { creatorId, closeTime } = contract
const firebaseUser = await admin.auth().getUser(auth.uid)
const { value, resolutions, probabilityInt, outcome } = getResolutionParams( const { value, resolutions, probabilityInt, outcome } = getResolutionParams(
contract, contract,
req.body req.body
) )
if (creatorId !== auth.uid && !isManifoldId(auth.uid)) if (
creatorId !== auth.uid &&
!isManifoldId(auth.uid) &&
!isAdmin(firebaseUser.email)
)
throw new APIError(403, 'User is not creator of contract') throw new APIError(403, 'User is not creator of contract')
if (contract.resolution) throw new APIError(400, 'Contract already resolved') if (contract.resolution) throw new APIError(400, 'Contract already resolved')

View File

@ -11,6 +11,8 @@ import { ResolveConfirmationButton } from '../confirmation-button'
import { removeUndefinedProps } from 'common/util/object' import { removeUndefinedProps } from 'common/util/object'
export function AnswerResolvePanel(props: { export function AnswerResolvePanel(props: {
isAdmin: boolean
isCreator: boolean
contract: FreeResponseContract | MultipleChoiceContract contract: FreeResponseContract | MultipleChoiceContract
resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
setResolveOption: ( setResolveOption: (
@ -18,7 +20,14 @@ export function AnswerResolvePanel(props: {
) => void ) => void
chosenAnswers: { [answerId: string]: number } chosenAnswers: { [answerId: string]: number }
}) { }) {
const { contract, resolveOption, setResolveOption, chosenAnswers } = props const {
contract,
resolveOption,
setResolveOption,
chosenAnswers,
isAdmin,
isCreator,
} = props
const answers = Object.keys(chosenAnswers) const answers = Object.keys(chosenAnswers)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -76,7 +85,14 @@ export function AnswerResolvePanel(props: {
return ( return (
<Col className="gap-4 rounded"> <Col className="gap-4 rounded">
<Row className="justify-between">
<div>Resolve your market</div> <div>Resolve your market</div>
{isAdmin && !isCreator && (
<span className="rounded bg-red-200 p-1 text-xs text-red-600">
ADMIN
</span>
)}
</Row>
<Col className="gap-4 sm:flex-row sm:items-center"> <Col className="gap-4 sm:flex-row sm:items-center">
<ChooseCancelSelector <ChooseCancelSelector
className="sm:!flex-row sm:items-center" className="sm:!flex-row sm:items-center"

View File

@ -24,10 +24,13 @@ import { Linkify } from 'web/components/linkify'
import { BuyButton } from 'web/components/yes-no-selector' import { BuyButton } from 'web/components/yes-no-selector'
import { UserLink } from 'web/components/user-link' import { UserLink } from 'web/components/user-link'
import { Button } from 'web/components/button' import { Button } from 'web/components/button'
import { useAdmin } from 'web/hooks/use-admin'
import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]'
export function AnswersPanel(props: { export function AnswersPanel(props: {
contract: FreeResponseContract | MultipleChoiceContract contract: FreeResponseContract | MultipleChoiceContract
}) { }) {
const isAdmin = useAdmin()
const { contract } = props const { contract } = props
const { creatorId, resolution, resolutions, totalBets, outcomeType } = const { creatorId, resolution, resolutions, totalBets, outcomeType } =
contract contract
@ -154,10 +157,13 @@ export function AnswersPanel(props: {
<CreateAnswerPanel contract={contract} /> <CreateAnswerPanel contract={contract} />
)} )}
{user?.id === creatorId && !resolution && ( {(user?.id === creatorId || (isAdmin && needsAdminToResolve(contract))) &&
!resolution && (
<> <>
<Spacer h={2} /> <Spacer h={2} />
<AnswerResolvePanel <AnswerResolvePanel
isAdmin={isAdmin}
isCreator={user?.id === creatorId}
contract={contract} contract={contract}
resolveOption={resolveOption} resolveOption={resolveOption}
setResolveOption={setResolveOption} setResolveOption={setResolveOption}

View File

@ -12,11 +12,13 @@ import { BucketInput } from './bucket-input'
import { getPseudoProbability } from 'common/pseudo-numeric' import { getPseudoProbability } from 'common/pseudo-numeric'
export function NumericResolutionPanel(props: { export function NumericResolutionPanel(props: {
isAdmin: boolean
isCreator: boolean
creator: User creator: User
contract: NumericContract | PseudoNumericContract contract: NumericContract | PseudoNumericContract
className?: string className?: string
}) { }) {
const { contract, className } = props const { contract, className, isAdmin, isCreator } = props
const { min, max, outcomeType } = contract const { min, max, outcomeType } = contract
const [outcomeMode, setOutcomeMode] = useState< const [outcomeMode, setOutcomeMode] = useState<
@ -78,10 +80,20 @@ export function NumericResolutionPanel(props: {
: 'btn-disabled' : 'btn-disabled'
return ( return (
<Col className={clsx('rounded-md bg-white px-8 py-6', className)}> <Col
<div className="mb-6 whitespace-nowrap text-2xl">Resolve market</div> className={clsx(
'relative w-full rounded-md bg-white px-8 py-6',
className
)}
>
{isAdmin && !isCreator && (
<span className="absolute right-4 top-4 rounded bg-red-200 p-1 text-xs text-red-600">
ADMIN
</span>
)}
<div className="whitespace-nowrap text-2xl">Resolve market</div>
<div className="mb-3 text-sm text-gray-500">Outcome</div> <div className="my-3 text-sm text-gray-500">Outcome</div>
<Spacer h={4} /> <Spacer h={4} />

View File

@ -12,11 +12,13 @@ import { getProbability } from 'common/calculate'
import { BinaryContract, resolution } from 'common/contract' import { BinaryContract, resolution } from 'common/contract'
export function ResolutionPanel(props: { export function ResolutionPanel(props: {
isAdmin: boolean
isCreator: boolean
creator: User creator: User
contract: BinaryContract contract: BinaryContract
className?: string className?: string
}) { }) {
const { contract, className } = props const { contract, className, isAdmin, isCreator } = props
// const earnedFees = // const earnedFees =
// contract.mechanism === 'dpm-2' // contract.mechanism === 'dpm-2'
@ -66,7 +68,12 @@ export function ResolutionPanel(props: {
: 'btn-disabled' : 'btn-disabled'
return ( return (
<Col className={clsx('rounded-md bg-white px-8 py-6', className)}> <Col className={clsx('relative rounded-md bg-white px-8 py-6', className)}>
{isAdmin && !isCreator && (
<span className="absolute right-4 top-4 rounded bg-red-200 p-1 text-xs text-red-600">
ADMIN
</span>
)}
<div className="mb-6 whitespace-nowrap text-2xl">Resolve market</div> <div className="mb-6 whitespace-nowrap text-2xl">Resolve market</div>
<div className="mb-3 text-sm text-gray-500">Outcome</div> <div className="mb-3 text-sm text-gray-500">Outcome</div>

View File

@ -45,6 +45,8 @@ import {
import { ContractsGrid } from 'web/components/contract/contracts-grid' import { ContractsGrid } from 'web/components/contract/contracts-grid'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { usePrefetch } from 'web/hooks/use-prefetch' import { usePrefetch } from 'web/hooks/use-prefetch'
import { useAdmin } from 'web/hooks/use-admin'
import dayjs from 'dayjs'
export const getStaticProps = fromPropz(getStaticPropz) export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { export async function getStaticPropz(props: {
@ -110,19 +112,28 @@ export default function ContractPage(props: {
) )
} }
// requires an admin to resolve a week after market closes
export function needsAdminToResolve(contract: Contract) {
return !contract.isResolved && dayjs().diff(contract.closeTime, 'day') > 7
}
export function ContractPageSidebar(props: { export function ContractPageSidebar(props: {
user: User | null | undefined user: User | null | undefined
contract: Contract contract: Contract
}) { }) {
const { contract, user } = props const { contract, user } = props
const { creatorId, isResolved, outcomeType } = contract const { creatorId, isResolved, outcomeType } = contract
const isCreator = user?.id === creatorId const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY' const isBinary = outcomeType === 'BINARY'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
const isNumeric = outcomeType === 'NUMERIC' const isNumeric = outcomeType === 'NUMERIC'
const allowTrade = tradingAllowed(contract) const allowTrade = tradingAllowed(contract)
const allowResolve = !isResolved && isCreator && !!user const isAdmin = useAdmin()
const allowResolve =
!isResolved &&
(isCreator || (needsAdminToResolve(contract) && isAdmin)) &&
!!user
const hasSidePanel = const hasSidePanel =
(isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve) (isBinary || isNumeric || isPseudoNumeric) && (allowTrade || allowResolve)
@ -139,9 +150,19 @@ export function ContractPageSidebar(props: {
))} ))}
{allowResolve && {allowResolve &&
(isNumeric || isPseudoNumeric ? ( (isNumeric || isPseudoNumeric ? (
<NumericResolutionPanel creator={user} contract={contract} /> <NumericResolutionPanel
isAdmin={isAdmin}
creator={user}
isCreator={isCreator}
contract={contract}
/>
) : ( ) : (
<ResolutionPanel creator={user} contract={contract} /> <ResolutionPanel
isAdmin={isAdmin}
creator={user}
isCreator={isCreator}
contract={contract}
/>
))} ))}
</Col> </Col>
) : null ) : null
@ -154,10 +175,8 @@ export function ContractPageContent(
} }
) { ) {
const { backToHome, comments, user } = props const { backToHome, comments, user } = props
const contract = useContractWithPreload(props.contract) ?? props.contract const contract = useContractWithPreload(props.contract) ?? props.contract
usePrefetch(user?.id) usePrefetch(user?.id)
useTracking( useTracking(
'view market', 'view market',
{ {