b2501d8145
* Answer datatype and MULTI outcome type for Contract
* Create free answer contract
* Automatically sort Tailwind classes with Prettier (#45)
* Add Prettier Tailwind plugin
* Autoformat Tailwind classes with Prettier
* Allow for non-binary contracts in contract page and related components
* logo with white inside, transparent bg
* Create answer
* Some UI for showing answers
* Answer bet panel
* Convert rest of calcuate file to generic multi contracts
* Working betting with ante'd NONE answer
* Numbered answers. Layout & calculation tweaks
* Can bet. More layout tweaks!
* Resolve answer UI
* Resolve multi market
* Resolved market UI
* Fix feed and cards for multi contracts
* Sell bets. Various fixes
* Tweaks for trades page
* Always dev mode
* Create answer bet has isAnte: true
* Fix card showing 0% for multi contracts
* Fix grouped bets feed for multi outcomes
* None option converted to none of the above label at bottom of list. Button to resolve none.
* Tweaks to no answers yet, resolve button layout
* Show ante bets on new answers in the feed
* Update placeholder text for description
* Consolidate firestore rules for subcollections
* Remove Contract and Bet type params. Use string type for outcomes.
* Increase char limit to 10k for answers. Preserve line breaks.
* Don't show resolve options after answer chosen
* Fix type error in script
* Remove NONE resolution option
* Change outcomeType to include 'MULTI' and 'FREE_RESPONSE'
* Show bet probability change and payout when creating answer
* User info change: also change answers
* Append answers to contract field 'answers'
* sort trades by resolved
* Don't include trailing !:,.; in links
* Stop flooring inputs into formatMoney
* Revert "Stop flooring inputs into formatMoney"
This reverts commit 2f7ab18429
.
* Consistently floor user.balance
* Expand create panel on focus
From Richard Hanania's feedback
* welcome email: include link to manifold
* Fix home page in dev on branches that are not free-response
* Close emails (#50)
* script init for stephen dev
* market close emails
* order of operations
* template email
* sendMarketCloseEmail: handle unsubscribe
* remove debugging
* marketCloseEmails: every hour
* sendMarketCloseEmails: check undefined
* marketCloseEmails: "every hour" => "every 1 hours"
* Set up a read API using Vercel serverless functions (#49)
* Set up read API using Vercel serverless functions
Featuring:
/api/v0/markets
/api/v0/market/[contractId]
/api/v0/slug/[contractSlug]
* Include tags in API
* Tweaks. Remove filter for only binary contract
* Fix bet probability change for NO bets
* Put back isProd calculation
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: mantikoros <95266179+mantikoros@users.noreply.github.com>
148 lines
4.0 KiB
TypeScript
148 lines
4.0 KiB
TypeScript
import * as functions from 'firebase-functions'
|
|
import * as admin from 'firebase-admin'
|
|
|
|
import { chargeUser, getUser } from './utils'
|
|
import { Contract, outcomeType } from '../../common/contract'
|
|
import { slugify } from '../../common/util/slugify'
|
|
import { randomString } from '../../common/util/random'
|
|
import { getNewContract } from '../../common/new-contract'
|
|
import {
|
|
getAnteBets,
|
|
getFreeAnswerAnte,
|
|
MINIMUM_ANTE,
|
|
} from '../../common/antes'
|
|
import { getNoneAnswer } from '../../common/answer'
|
|
|
|
export const createContract = functions
|
|
.runWith({ minInstances: 1 })
|
|
.https.onCall(
|
|
async (
|
|
data: {
|
|
question: string
|
|
outcomeType: outcomeType
|
|
description: string
|
|
initialProb: number
|
|
ante: number
|
|
closeTime: number
|
|
tags?: string[]
|
|
},
|
|
context
|
|
) => {
|
|
const userId = context?.auth?.uid
|
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
|
|
|
const creator = await getUser(userId)
|
|
if (!creator) return { status: 'error', message: 'User not found' }
|
|
|
|
const { question, description, initialProb, ante, closeTime, tags } = data
|
|
|
|
if (!question)
|
|
return { status: 'error', message: 'Missing question field' }
|
|
|
|
let outcomeType = data.outcomeType ?? 'BINARY'
|
|
if (!['BINARY', 'MULTI', 'FREE_RESPONSE'].includes(outcomeType))
|
|
return { status: 'error', message: 'Invalid outcomeType' }
|
|
|
|
if (
|
|
outcomeType === 'BINARY' &&
|
|
(!initialProb || initialProb < 1 || initialProb > 99)
|
|
)
|
|
return { status: 'error', message: 'Invalid initial probability' }
|
|
|
|
if (
|
|
ante === undefined ||
|
|
ante < MINIMUM_ANTE ||
|
|
ante > creator.balance ||
|
|
isNaN(ante) ||
|
|
!isFinite(ante)
|
|
)
|
|
return { status: 'error', message: 'Invalid ante' }
|
|
|
|
console.log(
|
|
'creating contract for',
|
|
creator.username,
|
|
'on',
|
|
question,
|
|
'ante:',
|
|
ante || 0
|
|
)
|
|
|
|
const slug = await getSlug(question)
|
|
|
|
const contractRef = firestore.collection('contracts').doc()
|
|
|
|
const contract = getNewContract(
|
|
contractRef.id,
|
|
slug,
|
|
creator,
|
|
question,
|
|
outcomeType,
|
|
description,
|
|
initialProb,
|
|
ante,
|
|
closeTime,
|
|
tags ?? []
|
|
)
|
|
|
|
if (ante) await chargeUser(creator.id, ante)
|
|
|
|
await contractRef.create(contract)
|
|
|
|
if (ante) {
|
|
if (outcomeType === 'BINARY') {
|
|
const yesBetDoc = firestore
|
|
.collection(`contracts/${contract.id}/bets`)
|
|
.doc()
|
|
|
|
const noBetDoc = firestore
|
|
.collection(`contracts/${contract.id}/bets`)
|
|
.doc()
|
|
|
|
const { yesBet, noBet } = getAnteBets(
|
|
creator,
|
|
contract,
|
|
yesBetDoc.id,
|
|
noBetDoc.id
|
|
)
|
|
await yesBetDoc.set(yesBet)
|
|
await noBetDoc.set(noBet)
|
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
|
const noneAnswerDoc = firestore
|
|
.collection(`contracts/${contract.id}/answers`)
|
|
.doc('0')
|
|
const noneAnswer = getNoneAnswer(contract.id, creator)
|
|
await noneAnswerDoc.set(noneAnswer)
|
|
|
|
const anteBetDoc = firestore
|
|
.collection(`contracts/${contract.id}/bets`)
|
|
.doc()
|
|
const anteBet = getFreeAnswerAnte(creator, contract, anteBetDoc.id)
|
|
await anteBetDoc.set(anteBet)
|
|
}
|
|
}
|
|
|
|
return { status: 'success', contract }
|
|
}
|
|
)
|
|
|
|
const getSlug = async (question: string) => {
|
|
const proposedSlug = slugify(question)
|
|
|
|
const preexistingContract = await getContractFromSlug(proposedSlug)
|
|
|
|
return preexistingContract
|
|
? proposedSlug + '-' + randomString()
|
|
: proposedSlug
|
|
}
|
|
|
|
const firestore = admin.firestore()
|
|
|
|
export async function getContractFromSlug(slug: string) {
|
|
const snap = await firestore
|
|
.collection('contracts')
|
|
.where('slug', '==', slug)
|
|
.get()
|
|
|
|
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
|
|
}
|