Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nonbinary Sweepstakes #3166

Merged
merged 13 commits into from
Dec 2, 2024
Merged
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
Loading