Skip to content

Commit

Permalink
Nonbinary Sweepstakes (#3166)
Browse files Browse the repository at this point in the history
* multi sweeps (wip)

copy answers on create answer

fixup

* wip in progress

* more wip in progress

* rip in progress

* add apis

* refactor: simplify

* Disallow adding answers to sweepstakes question

* fix lint

* fix lint

* code review feedback

add back admin flag for sweepify lol
remove no longer necessary answers data passthrough

* Remove answer duplication logic

* pre-cache answers
  • Loading branch information
sipec authored Dec 2, 2024
1 parent 990772d commit a1eb12c
Show file tree
Hide file tree
Showing 33 changed files with 557 additions and 356 deletions.
9 changes: 9 additions & 0 deletions backend/api/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ This directory contains the implementation of various API endpoints for the Mani
- We use Supabase for database operations.
- Authentication is handled using the `APIHandler` type, which automatically manages user authentication based on the schema definition.

## Mana/Sweepstakes Market Relationships

- Mana markets can have sweepstakes counterpart markets (siblingContractId)
- The mana market is the source of truth - changes to mana markets should propagate to their sweepstakes counterparts
- Changes that need to propagate include:
- Adding new answers to multiple choice markets
- Market metadata updates
- Market resolution

## Adding a New API Endpoint

To add a new API endpoint, follow these steps:
Expand Down
100 changes: 46 additions & 54 deletions backend/api/src/create-answer-cpmm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { groupBy, partition, sumBy } from 'lodash'
import { CPMMMultiContract, add_answers_mode } from 'common/contract'
import { CPMMMultiContract } from 'common/contract'
import { User } from 'common/user'
import { getBetDownToOneMultiBetInfo } from 'common/new-bet'
import { Answer, getMaximumAnswers } from 'common/answer'
Expand Down Expand Up @@ -53,37 +53,36 @@ export const createAnswerCPMM: APIHandler<'market/:contractId/answer'> = async (
) => {
const { contractId, text } = props
return await betsQueue.enqueueFn(
() => createAnswerCpmmMain(contractId, text, auth.uid),
() => createAnswerCpmmFull(contractId, text, auth.uid),
[contractId, auth.uid]
)
}

export const createAnswerCpmmMain = async (
const createAnswerCpmmFull = async (
contractId: string,
text: string,
creatorId: string,
options: {
overrideAddAnswersMode?: add_answers_mode
specialLiquidityPerAnswer?: number
loverUserId?: string
} = {}
userId: string
) => {
const { overrideAddAnswersMode, specialLiquidityPerAnswer, loverUserId } =
options
log('Received ' + contractId + ' ' + text)
const contract = await verifyContract(contractId, userId)
return await createAnswerCpmmMain(contract, text, userId)
}

const verifyContract = async (contractId: string, creatorId: string) => {
const contract = await getContractSupabase(contractId)
if (!contract) throw new APIError(404, 'Contract not found')
if (contract.token !== 'MANA') {
throw new APIError(403, 'Cannot add answers to sweepstakes question')
}
if (contract.mechanism !== 'cpmm-multi-1')
throw new APIError(403, 'Requires a cpmm multiple choice contract')
if (contract.outcomeType === 'NUMBER')
throw new APIError(403, 'Cannot create new answers for numeric contracts')

const { closeTime, shouldAnswersSumToOne } = contract
const { closeTime } = contract
if (closeTime && Date.now() > closeTime)
throw new APIError(403, 'Trading is closed')

const addAnswersMode = overrideAddAnswersMode ?? contract.addAnswersMode
const addAnswersMode = contract.addAnswersMode

if (!addAnswersMode || addAnswersMode === 'DISABLED') {
throw new APIError(400, 'Adding answers is disabled')
Expand All @@ -97,6 +96,16 @@ export const createAnswerCpmmMain = async (
throw new APIError(403, 'Only the creator or an admin can create an answer')
}

return contract
}

const createAnswerCpmmMain = async (
contract: Awaited<ReturnType<typeof verifyContract>>,
text: string,
creatorId: string
) => {
const { shouldAnswersSumToOne } = contract

const answerCost = getTieredAnswerCost(
getTierFromLiquidity(contract, contract.totalLiquidity)
)
Expand All @@ -107,17 +116,15 @@ export const createAnswerCpmmMain = async (
if (!user) throw new APIError(401, 'Your account was not found')
if (user.isBannedFromPosting) throw new APIError(403, 'You are banned')

if (user.balance < answerCost && !specialLiquidityPerAnswer)
if (user.balance < answerCost)
throw new APIError(403, 'Insufficient balance, need M' + answerCost)

if (!specialLiquidityPerAnswer) {
await incrementBalance(pgTrans, user.id, {
balance: -answerCost,
totalDeposits: -answerCost,
})
}
await incrementBalance(pgTrans, user.id, {
balance: -answerCost,
totalDeposits: -answerCost,
})

const answers = await getAnswersForContract(pgTrans, contractId)
const answers = await getAnswersForContract(pgTrans, contract.id)
const unresolvedAnswers = answers.filter((a) => !a.resolution)
const maxAnswers = getMaximumAnswers(shouldAnswersSumToOne)
if (unresolvedAnswers.length >= maxAnswers) {
Expand All @@ -127,30 +134,18 @@ export const createAnswerCpmmMain = async (
)
}

let poolYes = answerCost
let poolNo = answerCost
let totalLiquidity = answerCost
let prob = 0.5

if (specialLiquidityPerAnswer) {
if (shouldAnswersSumToOne)
throw new APIError(
500,
"Can't specify specialLiquidityPerAnswer and shouldAnswersSumToOne"
)
prob = 0.02
poolYes = specialLiquidityPerAnswer
poolNo = specialLiquidityPerAnswer / (1 / prob - 1)
totalLiquidity = specialLiquidityPerAnswer
}
const poolYes = answerCost
const poolNo = answerCost
const totalLiquidity = answerCost
const prob = 0.5

const id = randomString()
const n = answers.length
const createdTime = Date.now()
const newAnswer: Answer = removeUndefinedProps({
id,
index: n,
contractId,
contractId: contract.id,
createdTime,
userId: user.id,
text,
Expand All @@ -161,7 +156,6 @@ export const createAnswerCpmmMain = async (
totalLiquidity,
subsidyPool: 0,
probChanges: { day: 0, week: 0, month: 0 },
loverUserId,
})

const updatedAnswers: Answer[] = []
Expand All @@ -180,21 +174,19 @@ export const createAnswerCpmmMain = async (
await insertAnswer(pgTrans, newAnswer)
}

if (!specialLiquidityPerAnswer) {
await updateContract(pgTrans, contractId, {
totalLiquidity: FieldVal.increment(answerCost),
})
await updateContract(pgTrans, contract.id, {
totalLiquidity: FieldVal.increment(answerCost),
})

const lp = getCpmmInitialLiquidity(
user.id,
contract,
answerCost,
createdTime,
newAnswer.id
)
const lp = getCpmmInitialLiquidity(
user.id,
contract,
answerCost,
createdTime,
newAnswer.id
)

await insertLiquidity(pgTrans, lp)
}
await insertLiquidity(pgTrans, lp)

return { newAnswer, updatedAnswers, user }
}
Expand All @@ -208,7 +200,7 @@ export const createAnswerCpmmMain = async (
contract
)
const pg = createSupabaseDirectClient()
await followContractInternal(pg, contractId, true, creatorId)
await followContractInternal(pg, contract.id, true, creatorId)
}
return { result: { newAnswerId: newAnswer.id }, continue: continuation }
}
Expand Down
7 changes: 6 additions & 1 deletion backend/api/src/create-cash-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export const createCashContract: APIHandler<'create-cash-contract'> = async (
'Only Manifold team members can create cash contracts'
)

const contract = await createCashContractMain(manaContractId, subsidyAmount)
const contract = await createCashContractMain(
manaContractId,
subsidyAmount,
auth.uid
)

return toLiteMarket(contract)
}
84 changes: 2 additions & 82 deletions backend/api/src/create-market.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { onCreateMarket } from 'api/helpers/on-create-market'
import { getNewLiquidityProvision } from 'common/add-liquidity'
import { getCpmmInitialLiquidity } from 'common/antes'
import {
createBinarySchema,
createBountySchema,
Expand All @@ -12,9 +10,6 @@ import {
} from 'common/api/market-types'
import { ValidatedAPIParams } from 'common/api/schema'
import {
BinaryContract,
CPMMMultiContract,
Contract,
MULTI_NUMERIC_CREATION_ENABLED,
NO_CLOSE_TIME_TYPES,
OutcomeType,
Expand All @@ -34,15 +29,13 @@ import { getCloseDate } from 'shared/helpers/openai-utils'
import {
generateContractEmbeddings,
getContractsDirect,
updateContract,
} from 'shared/supabase/contracts'
import {
SupabaseDirectClient,
SupabaseTransaction,
createSupabaseDirectClient,
pgp,
} from 'shared/supabase/init'
import { insertLiquidity } from 'shared/supabase/liquidity'
import { anythingToRichText } from 'shared/tiptap'
import { runTxnOutsideBetQueue } from 'shared/txn/run-txn'
import {
Expand All @@ -56,10 +49,11 @@ import {
} from 'shared/websockets/helpers'
import { APIError, AuthedUser, type APIHandler } from './helpers/endpoint'
import { Row } from 'common/supabase/utils'
import { bulkInsertQuery, FieldVal } from 'shared/supabase/utils'
import { bulkInsertQuery } from 'shared/supabase/utils'
import { z } from 'zod'
import { answerToRow } from 'shared/supabase/answers'
import { convertAnswer } from 'common/supabase/contracts'
import { generateAntes } from 'shared/create-contract-helpers'

type Body = ValidatedAPIParams<'market'>

Expand Down Expand Up @@ -490,77 +484,3 @@ async function getGroupCheckPermissions(

return group
}

export async function generateAntes(
pg: SupabaseDirectClient,
providerId: string,
contract: Contract,
outcomeType: OutcomeType,
ante: number,
totalMarketCost: number
) {
if (
contract.outcomeType === 'MULTIPLE_CHOICE' &&
contract.mechanism === 'cpmm-multi-1' &&
!contract.shouldAnswersSumToOne
) {
const { answers } = contract
for (const answer of answers) {
const ante = Math.sqrt(answer.poolYes * answer.poolNo)

const lp = getCpmmInitialLiquidity(
providerId,
contract,
ante,
contract.createdTime,
answer.id
)

await insertLiquidity(pg, lp)
}
} else if (
outcomeType === 'BINARY' ||
outcomeType === 'PSEUDO_NUMERIC' ||
outcomeType === 'STONK' ||
outcomeType === 'MULTIPLE_CHOICE' ||
outcomeType === 'NUMBER'
) {
const lp = getCpmmInitialLiquidity(
providerId,
contract as BinaryContract | CPMMMultiContract,
ante,
contract.createdTime
)

await insertLiquidity(pg, lp)
}
const drizzledAmount = totalMarketCost - ante
if (
drizzledAmount > 0 &&
(contract.mechanism === 'cpmm-1' || contract.mechanism === 'cpmm-multi-1')
) {
return await pg.txIf(async (tx) => {
await runTxnOutsideBetQueue(tx, {
fromId: providerId,
amount: drizzledAmount,
toId: contract.id,
toType: 'CONTRACT',
category: 'ADD_SUBSIDY',
token: 'M$',
fromType: 'USER',
})
const newLiquidityProvision = getNewLiquidityProvision(
providerId,
drizzledAmount,
contract
)

await insertLiquidity(tx, newLiquidityProvision)

await updateContract(tx, contract.id, {
subsidyPool: FieldVal.increment(drizzledAmount),
totalLiquidity: FieldVal.increment(drizzledAmount),
})
})
}
}
12 changes: 12 additions & 0 deletions backend/api/src/get-answer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { APIHandler } from './helpers/endpoint'
import { getAnswer } from 'shared/supabase/answers'
import { createSupabaseDirectClient } from 'shared/supabase/init'

export const getSingleAnswer: APIHandler<'answer/:answerId'> = async (props) => {
const pg = createSupabaseDirectClient()
const answer = await getAnswer(pg, props.answerId)
if (!answer) {
throw new Error('Answer not found')
}
return answer
}
8 changes: 8 additions & 0 deletions backend/api/src/get-contract-answers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { APIHandler } from './helpers/endpoint'
import { getAnswersForContract } from 'shared/supabase/answers'
import { createSupabaseDirectClient } from 'shared/supabase/init'

export const getContractAnswers: APIHandler<'market/:contractId/answers'> = async (props) => {
const pg = createSupabaseDirectClient()
return await getAnswersForContract(pg, props.contractId)
}
22 changes: 20 additions & 2 deletions backend/api/src/on-create-bet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { log, getUsers, revalidateContractStaticProps } from 'shared/utils'
import {
log,
getUsers,
revalidateContractStaticProps,
getContract,
} from 'shared/utils'
import { Bet, LimitBet } from 'common/bet'
import { Contract } from 'common/contract'
import { humanish, User } from 'common/user'
Expand Down Expand Up @@ -257,7 +262,20 @@ const handleBetReplyToComment = async (

if (!comment) return

const allBetReplies = await getBetsRepliedToComment(pg, comment, contract.id)
const manaContract =
contract.token === 'CASH'
? await getContract(pg, contract.siblingContractId!)
: contract

if (!manaContract) return

const allBetReplies = await getBetsRepliedToComment(
pg,
comment,
contract.id,
contract.siblingContractId
)

const bets = filterDefined(allBetReplies)
// This could potentially miss some bets if they're not replicated in time
if (!bets.some((b) => b.id === bet.id)) bets.push(bet)
Expand Down
Loading

0 comments on commit a1eb12c

Please sign in to comment.