Merge branch 'main' into limit-orders

This commit is contained in:
James Grugett 2022-07-09 14:42:46 -05:00
parent 5be2ea8583
commit 89ac26417b
16 changed files with 407 additions and 502 deletions

View File

@ -64,11 +64,7 @@ function calculateCpmmShares(
: n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p)) : n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p))
} }
export function getCpmmLiquidityFee( export function getCpmmFees(state: CpmmState, bet: number, outcome: string) {
state: CpmmState,
bet: number,
outcome: string
) {
const prob = getCpmmProbabilityAfterBetBeforeFees(state, outcome, bet) const prob = getCpmmProbabilityAfterBetBeforeFees(state, outcome, bet)
const betP = outcome === 'YES' ? 1 - prob : prob const betP = outcome === 'YES' ? 1 - prob : prob
@ -89,7 +85,7 @@ export function calculateCpmmSharesAfterFee(
outcome: string outcome: string
) { ) {
const { pool, p } = state const { pool, p } = state
const { remainingBet } = getCpmmLiquidityFee(state, bet, outcome) const { remainingBet } = getCpmmFees(state, bet, outcome)
return calculateCpmmShares(pool, p, remainingBet, outcome) return calculateCpmmShares(pool, p, remainingBet, outcome)
} }
@ -100,9 +96,7 @@ export function calculateCpmmPurchase(
outcome: string outcome: string
) { ) {
const { pool, p } = state const { pool, p } = state
const { remainingBet, fees } = getCpmmLiquidityFee(state, bet, outcome) const { remainingBet, fees } = getCpmmFees(state, bet, outcome)
// const remainingBet = bet
// const fees = noFees
const shares = calculateCpmmShares(pool, p, remainingBet, outcome) const shares = calculateCpmmShares(pool, p, remainingBet, outcome)
const { YES: y, NO: n } = pool const { YES: y, NO: n } = pool

View File

@ -76,14 +76,14 @@ const computeFill = (
? Math.min(matchedBet.limitProb, limitProb ?? 1) ? Math.min(matchedBet.limitProb, limitProb ?? 1)
: Math.max(matchedBet.limitProb, limitProb ?? 0) : Math.max(matchedBet.limitProb, limitProb ?? 0)
const poolAmount = const buyAmount =
limit === undefined limit === undefined
? amount ? amount
: Math.min(amount, calculateCpmmAmount(cpmmState, limit, outcome)) : Math.min(amount, calculateCpmmAmount(cpmmState, limit, outcome))
const { shares, newPool, newP, fees } = calculateCpmmPurchase( const { shares, newPool, newP, fees } = calculateCpmmPurchase(
cpmmState, cpmmState,
poolAmount, buyAmount,
outcome outcome
) )
const newState = { pool: newPool, p: newP } const newState = { pool: newPool, p: newP }
@ -92,7 +92,7 @@ const computeFill = (
maker: { maker: {
matchedBetId: null, matchedBetId: null,
shares, shares,
amount: poolAmount, amount: buyAmount,
state: newState, state: newState,
fees, fees,
timestamp, timestamp,
@ -100,7 +100,7 @@ const computeFill = (
taker: { taker: {
matchedBetId: null, matchedBetId: null,
shares, shares,
amount: poolAmount, amount: buyAmount,
timestamp, timestamp,
}, },
} }

View File

@ -1,105 +1,96 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
import { removeUndefinedProps } from '../../common/util/object' import { removeUndefinedProps } from '../../common/util/object'
import { redeemShares } from './redeem-shares' import { redeemShares } from './redeem-shares'
import { getNewLiquidityProvision } from '../../common/add-liquidity' import { getNewLiquidityProvision } from '../../common/add-liquidity'
import { APIError, newEndpoint, validate } from './api'
export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall( const bodySchema = z.object({
async ( contractId: z.string(),
data: { amount: z.number().gt(0),
amount: number })
contractId: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const { amount, contractId } = data export const addliquidity = newEndpoint({}, async (req, auth) => {
const { amount, contractId } = validate(bodySchema, req.body)
if (amount <= 0 || isNaN(amount) || !isFinite(amount)) if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
return { status: 'error', message: 'Invalid amount' }
// run as transaction to prevent race conditions // run as transaction to prevent race conditions
return await firestore return await firestore
.runTransaction(async (transaction) => { .runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${userId}`) const userDoc = firestore.doc(`users/${auth.uid}`)
const userSnap = await transaction.get(userDoc) const userSnap = await transaction.get(userDoc)
if (!userSnap.exists) if (!userSnap.exists) throw new APIError(400, 'User not found')
return { status: 'error', message: 'User not found' } const user = userSnap.data() as User
const user = userSnap.data() as User
const contractDoc = firestore.doc(`contracts/${contractId}`) const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc) const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists) if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract
const contract = contractSnap.data() as Contract if (
if ( contract.mechanism !== 'cpmm-1' ||
contract.mechanism !== 'cpmm-1' || (contract.outcomeType !== 'BINARY' &&
(contract.outcomeType !== 'BINARY' && contract.outcomeType !== 'PSEUDO_NUMERIC')
contract.outcomeType !== 'PSEUDO_NUMERIC') )
) throw new APIError(400, 'Invalid contract')
return { status: 'error', message: 'Invalid contract' }
const { closeTime } = contract const { closeTime } = contract
if (closeTime && Date.now() > closeTime) if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' } throw new APIError(400, 'Trading is closed')
if (user.balance < amount) if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
return { status: 'error', message: 'Insufficient balance' }
const newLiquidityProvisionDoc = firestore const newLiquidityProvisionDoc = firestore
.collection(`contracts/${contractId}/liquidity`) .collection(`contracts/${contractId}/liquidity`)
.doc() .doc()
const { newLiquidityProvision, newPool, newP, newTotalLiquidity } = const { newLiquidityProvision, newPool, newP, newTotalLiquidity } =
getNewLiquidityProvision( getNewLiquidityProvision(
user, user,
amount, amount,
contract, contract,
newLiquidityProvisionDoc.id newLiquidityProvisionDoc.id
)
if (newP !== undefined && !isFinite(newP)) {
return {
status: 'error',
message: 'Liquidity injection rejected due to overflow error.',
}
}
transaction.update(
contractDoc,
removeUndefinedProps({
pool: newPool,
p: newP,
totalLiquidity: newTotalLiquidity,
})
) )
const newBalance = user.balance - amount if (newP !== undefined && !isFinite(newP)) {
const newTotalDeposits = user.totalDeposits - amount return {
status: 'error',
if (!isFinite(newBalance)) { message: 'Liquidity injection rejected due to overflow error.',
throw new Error('Invalid user balance for ' + user.username)
} }
}
transaction.update(userDoc, { transaction.update(
balance: newBalance, contractDoc,
totalDeposits: newTotalDeposits, removeUndefinedProps({
pool: newPool,
p: newP,
totalLiquidity: newTotalLiquidity,
}) })
)
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) const newBalance = user.balance - amount
const newTotalDeposits = user.totalDeposits - amount
return { status: 'success', newLiquidityProvision } if (!isFinite(newBalance)) {
throw new APIError(500, 'Invalid user balance for ' + user.username)
}
transaction.update(userDoc, {
balance: newBalance,
totalDeposits: newTotalDeposits,
}) })
.then(async (result) => {
await redeemShares(userId, contractId) transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
return result
}) return newLiquidityProvision
} })
) .then(async (result) => {
await redeemShares(auth.uid, contractId)
return result
})
})
const firestore = admin.firestore() const firestore = admin.firestore()

View File

@ -1,5 +1,5 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod'
import { getUser } from './utils' import { getUser } from './utils'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
@ -11,37 +11,23 @@ import {
} from '../../common/util/clean-username' } from '../../common/util/clean-username'
import { removeUndefinedProps } from '../../common/util/object' import { removeUndefinedProps } from '../../common/util/object'
import { Answer } from '../../common/answer' import { Answer } from '../../common/answer'
import { APIError, newEndpoint, validate } from './api'
export const changeUserInfo = functions const bodySchema = z.object({
.runWith({ minInstances: 1 }) username: z.string().optional(),
.https.onCall( name: z.string().optional(),
async ( avatarUrl: z.string().optional(),
data: { })
username?: string
name?: string
avatarUrl?: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const user = await getUser(userId) export const changeuserinfo = newEndpoint({}, async (req, auth) => {
if (!user) return { status: 'error', message: 'User not found' } const { username, name, avatarUrl } = validate(bodySchema, req.body)
const { username, name, avatarUrl } = data const user = await getUser(auth.uid)
if (!user) throw new APIError(400, 'User not found')
return await changeUser(user, { username, name, avatarUrl }) await changeUser(user, { username, name, avatarUrl })
.then(() => { return { message: 'Successfully changed user info.' }
console.log('succesfully changed', user.username, 'to', data) })
return { status: 'success' }
})
.catch((e) => {
console.log('Error', e.message)
return { status: 'error', message: e.message }
})
}
)
export const changeUser = async ( export const changeUser = async (
user: User, user: User,
@ -55,14 +41,14 @@ export const changeUser = async (
if (update.username) { if (update.username) {
update.username = cleanUsername(update.username) update.username = cleanUsername(update.username)
if (!update.username) { if (!update.username) {
throw new Error('Invalid username') throw new APIError(400, 'Invalid username')
} }
const sameNameUser = await transaction.get( const sameNameUser = await transaction.get(
firestore.collection('users').where('username', '==', update.username) firestore.collection('users').where('username', '==', update.username)
) )
if (!sameNameUser.empty) { if (!sameNameUser.empty) {
throw new Error('Username already exists') throw new APIError(400, 'Username already exists')
} }
} }
@ -104,17 +90,10 @@ export const changeUser = async (
) )
const answerUpdate: Partial<Answer> = removeUndefinedProps(update) const answerUpdate: Partial<Answer> = removeUndefinedProps(update)
await transaction.update(userRef, userUpdate) transaction.update(userRef, userUpdate)
commentSnap.docs.forEach((d) => transaction.update(d.ref, commentUpdate))
await Promise.all( answerSnap.docs.forEach((d) => transaction.update(d.ref, answerUpdate))
commentSnap.docs.map((d) => transaction.update(d.ref, commentUpdate)) contracts.docs.forEach((d) => transaction.update(d.ref, contractUpdate))
)
await Promise.all(
answerSnap.docs.map((d) => transaction.update(d.ref, answerUpdate))
)
await contracts.docs.map((d) => transaction.update(d.ref, contractUpdate))
}) })
} }

View File

@ -1,102 +1,104 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod'
import { User } from 'common/user' import { User } from 'common/user'
import { Manalink } from 'common/manalink' import { Manalink } from 'common/manalink'
import { runTxn, TxnData } from './transact' import { runTxn, TxnData } from './transact'
import { APIError, newEndpoint, validate } from './api'
export const claimManalink = functions const bodySchema = z.object({
.runWith({ minInstances: 1 }) slug: z.string(),
.https.onCall(async (slug: string, context) => { })
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
// Run as transaction to prevent race conditions. export const claimmanalink = newEndpoint({}, async (req, auth) => {
return await firestore.runTransaction(async (transaction) => { const { slug } = validate(bodySchema, req.body)
// Look up the manalink
const manalinkDoc = firestore.doc(`manalinks/${slug}`)
const manalinkSnap = await transaction.get(manalinkDoc)
if (!manalinkSnap.exists) {
return { status: 'error', message: 'Manalink not found' }
}
const manalink = manalinkSnap.data() as Manalink
const { amount, fromId, claimedUserIds } = manalink // Run as transaction to prevent race conditions.
return await firestore.runTransaction(async (transaction) => {
// Look up the manalink
const manalinkDoc = firestore.doc(`manalinks/${slug}`)
const manalinkSnap = await transaction.get(manalinkDoc)
if (!manalinkSnap.exists) {
throw new APIError(400, 'Manalink not found')
}
const manalink = manalinkSnap.data() as Manalink
if (amount <= 0 || isNaN(amount) || !isFinite(amount)) const { amount, fromId, claimedUserIds } = manalink
return { status: 'error', message: 'Invalid amount' }
const fromDoc = firestore.doc(`users/${fromId}`) if (amount <= 0 || isNaN(amount) || !isFinite(amount))
const fromSnap = await transaction.get(fromDoc) throw new APIError(500, 'Invalid amount')
if (!fromSnap.exists) {
return { status: 'error', message: `User ${fromId} not found` }
}
const fromUser = fromSnap.data() as User
// Only permit one redemption per user per link const fromDoc = firestore.doc(`users/${fromId}`)
if (claimedUserIds.includes(userId)) { const fromSnap = await transaction.get(fromDoc)
return { if (!fromSnap.exists) {
status: 'error', throw new APIError(500, `User ${fromId} not found`)
message: `${fromUser.name} already redeemed manalink ${slug}`, }
} const fromUser = fromSnap.data() as User
}
// Disallow expired or maxed out links // Only permit one redemption per user per link
if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) { if (claimedUserIds.includes(auth.uid)) {
return { throw new APIError(400, `You already redeemed manalink ${slug}`)
status: 'error', }
message: `Manalink ${slug} expired on ${new Date(
manalink.expiresTime
).toLocaleString()}`,
}
}
if (
manalink.maxUses != null &&
manalink.maxUses <= manalink.claims.length
) {
return {
status: 'error',
message: `Manalink ${slug} has reached its max uses of ${manalink.maxUses}`,
}
}
if (fromUser.balance < amount) { // Disallow expired or maxed out links
return { if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) {
status: 'error', throw new APIError(
message: `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} `, 400,
} `Manalink ${slug} expired on ${new Date(
} manalink.expiresTime
).toLocaleString()}`
)
}
if (
manalink.maxUses != null &&
manalink.maxUses <= manalink.claims.length
) {
throw new APIError(
400,
`Manalink ${slug} has reached its max uses of ${manalink.maxUses}`
)
}
// Actually execute the txn if (fromUser.balance < amount) {
const data: TxnData = { throw new APIError(
fromId, 400,
fromType: 'USER', `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} `
toId: userId, )
toType: 'USER', }
amount,
token: 'M$',
category: 'MANALINK',
description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${userId}`,
}
const result = await runTxn(transaction, data)
const txnId = result.txn?.id
if (!txnId) {
return { status: 'error', message: result.message }
}
// Update the manalink object with this info // Actually execute the txn
const claim = { const data: TxnData = {
toId: userId, fromId,
txnId, fromType: 'USER',
claimedTime: Date.now(), toId: auth.uid,
} toType: 'USER',
transaction.update(manalinkDoc, { amount,
claimedUserIds: [...claimedUserIds, userId], token: 'M$',
claims: [...manalink.claims, claim], category: 'MANALINK',
}) description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${auth.uid}`,
}
const result = await runTxn(transaction, data)
const txnId = result.txn?.id
if (!txnId) {
throw new APIError(
500,
result.message ?? 'An error occurred posting the transaction.'
)
}
return { status: 'success', message: 'Manalink claimed' } // Update the manalink object with this info
const claim = {
toId: auth.uid,
txnId,
claimedTime: Date.now(),
}
transaction.update(manalinkDoc, {
claimedUserIds: [...claimedUserIds, auth.uid],
claims: [...manalink.claims, claim],
}) })
return { message: 'Manalink claimed' }
}) })
})
const firestore = admin.firestore() const firestore = admin.firestore()

View File

@ -1,5 +1,5 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
@ -7,122 +7,103 @@ import { getNewMultiBetInfo } from '../../common/new-bet'
import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer' import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
import { getContract, getValues } from './utils' import { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails' import { sendNewAnswerEmail } from './emails'
import { APIError, newEndpoint, validate } from './api'
export const createAnswer = functions const bodySchema = z.object({
.runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] }) contractId: z.string().max(MAX_ANSWER_LENGTH),
.https.onCall( amount: z.number().gt(0),
async ( text: z.string(),
data: { })
contractId: string
amount: number
text: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const { contractId, amount, text } = data const opts = { secrets: ['MAILGUN_KEY'] }
if (amount <= 0 || isNaN(amount) || !isFinite(amount)) export const createanswer = newEndpoint(opts, async (req, auth) => {
return { status: 'error', message: 'Invalid amount' } const { contractId, amount, text } = validate(bodySchema, req.body)
if (!text || typeof text !== 'string' || text.length > MAX_ANSWER_LENGTH) if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
return { status: 'error', message: 'Invalid text' }
// Run as transaction to prevent race conditions. // Run as transaction to prevent race conditions.
const result = await firestore.runTransaction(async (transaction) => { const answer = await firestore.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${userId}`) const userDoc = firestore.doc(`users/${auth.uid}`)
const userSnap = await transaction.get(userDoc) const userSnap = await transaction.get(userDoc)
if (!userSnap.exists) if (!userSnap.exists) throw new APIError(400, 'User not found')
return { status: 'error', message: 'User not found' } const user = userSnap.data() as User
const user = userSnap.data() as User
if (user.balance < amount) if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
return { status: 'error', message: 'Insufficient balance' }
const contractDoc = firestore.doc(`contracts/${contractId}`) const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc) const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists) if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract
const contract = contractSnap.data() as Contract
if (contract.outcomeType !== 'FREE_RESPONSE') if (contract.outcomeType !== 'FREE_RESPONSE')
return { throw new APIError(400, 'Requires a free response contract')
status: 'error',
message: 'Requires a free response contract',
}
const { closeTime, volume } = contract const { closeTime, volume } = contract
if (closeTime && Date.now() > closeTime) if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' } throw new APIError(400, 'Trading is closed')
const [lastAnswer] = await getValues<Answer>( const [lastAnswer] = await getValues<Answer>(
firestore firestore
.collection(`contracts/${contractId}/answers`) .collection(`contracts/${contractId}/answers`)
.orderBy('number', 'desc') .orderBy('number', 'desc')
.limit(1) .limit(1)
) )
if (!lastAnswer) if (!lastAnswer) throw new APIError(500, 'Could not fetch last answer')
return { status: 'error', message: 'Could not fetch last answer' }
const number = lastAnswer.number + 1 const number = lastAnswer.number + 1
const id = `${number}` const id = `${number}`
const newAnswerDoc = firestore const newAnswerDoc = firestore
.collection(`contracts/${contractId}/answers`) .collection(`contracts/${contractId}/answers`)
.doc(id) .doc(id)
const answerId = newAnswerDoc.id const answerId = newAnswerDoc.id
const { username, name, avatarUrl } = user const { username, name, avatarUrl } = user
const answer: Answer = { const answer: Answer = {
id, id,
number, number,
contractId, contractId,
createdTime: Date.now(), createdTime: Date.now(),
userId: user.id, userId: user.id,
username, username,
name, name,
avatarUrl, avatarUrl,
text, text,
}
transaction.create(newAnswerDoc, answer)
const loanAmount = 0
const { newBet, newPool, newTotalShares, newTotalBets } =
getNewMultiBetInfo(answerId, amount, contract, loanAmount)
const newBalance = user.balance - amount
const betDoc = firestore
.collection(`contracts/${contractId}/bets`)
.doc()
transaction.create(betDoc, {
id: betDoc.id,
userId: user.id,
...newBet,
})
transaction.update(userDoc, { balance: newBalance })
transaction.update(contractDoc, {
pool: newPool,
totalShares: newTotalShares,
totalBets: newTotalBets,
answers: [...(contract.answers ?? []), answer],
volume: volume + amount,
})
return { status: 'success', answerId, betId: betDoc.id, answer }
})
const { answer } = result
const contract = await getContract(contractId)
if (answer && contract) await sendNewAnswerEmail(answer, contract)
return result
} }
) transaction.create(newAnswerDoc, answer)
const loanAmount = 0
const { newBet, newPool, newTotalShares, newTotalBets } =
getNewMultiBetInfo(answerId, amount, contract, loanAmount)
const newBalance = user.balance - amount
const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
transaction.create(betDoc, {
id: betDoc.id,
userId: user.id,
...newBet,
})
transaction.update(userDoc, { balance: newBalance })
transaction.update(contractDoc, {
pool: newPool,
totalShares: newTotalShares,
totalBets: newTotalBets,
answers: [...(contract.answers ?? []), answer],
volume: volume + amount,
})
return answer
})
const contract = await getContract(contractId)
if (answer && contract) await sendNewAnswerEmail(answer, contract)
return answer
})
const firestore = admin.firestore() const firestore = admin.firestore()

View File

@ -3,12 +3,9 @@ import * as admin from 'firebase-admin'
admin.initializeApp() admin.initializeApp()
// v1 // v1
// export * from './keep-awake'
export * from './claim-manalink'
export * from './transact' export * from './transact'
export * from './stripe' export * from './stripe'
export * from './create-user' export * from './create-user'
export * from './create-answer'
export * from './on-create-bet' export * from './on-create-bet'
export * from './on-create-comment-on-contract' export * from './on-create-comment-on-contract'
export * from './on-view' export * from './on-view'
@ -16,9 +13,7 @@ export * from './unsubscribe'
export * from './update-metrics' export * from './update-metrics'
export * from './update-stats' export * from './update-stats'
export * from './backup-db' export * from './backup-db'
export * from './change-user-info'
export * from './market-close-notifications' export * from './market-close-notifications'
export * from './add-liquidity'
export * from './on-create-answer' export * from './on-create-answer'
export * from './on-update-contract' export * from './on-update-contract'
export * from './on-create-contract' export * from './on-create-contract'
@ -33,11 +28,15 @@ export * from './on-create-txn'
// v2 // v2
export * from './health' export * from './health'
export * from './change-user-info'
export * from './create-answer'
export * from './place-bet' export * from './place-bet'
export * from './cancel-bet' export * from './cancel-bet'
export * from './sell-bet' export * from './sell-bet'
export * from './sell-shares' export * from './sell-shares'
export * from './claim-manalink'
export * from './create-contract' export * from './create-contract'
export * from './add-liquidity'
export * from './withdraw-liquidity' export * from './withdraw-liquidity'
export * from './create-group' export * from './create-group'
export * from './resolve-market' export * from './resolve-market'

View File

@ -1,5 +1,5 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod'
import { CPMMContract } from '../../common/contract' import { CPMMContract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
@ -10,129 +10,107 @@ import { Bet } from '../../common/bet'
import { getProbability } from '../../common/calculate' import { getProbability } from '../../common/calculate'
import { noFees } from '../../common/fees' import { noFees } from '../../common/fees'
import { APIError } from './api' import { APIError, newEndpoint, validate } from './api'
import { redeemShares } from './redeem-shares' import { redeemShares } from './redeem-shares'
export const withdrawLiquidity = functions const bodySchema = z.object({
.runWith({ minInstances: 1 }) contractId: z.string(),
.https.onCall( })
async (
data: {
contractId: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const { contractId } = data export const withdrawliquidity = newEndpoint({}, async (req, auth) => {
if (!contractId) const { contractId } = validate(bodySchema, req.body)
return { status: 'error', message: 'Missing contract id' }
return await firestore return await firestore
.runTransaction(async (trans) => { .runTransaction(async (trans) => {
const lpDoc = firestore.doc(`users/${userId}`) const lpDoc = firestore.doc(`users/${auth.uid}`)
const lpSnap = await trans.get(lpDoc) const lpSnap = await trans.get(lpDoc)
if (!lpSnap.exists) throw new APIError(400, 'User not found.') if (!lpSnap.exists) throw new APIError(400, 'User not found.')
const lp = lpSnap.data() as User const lp = lpSnap.data() as User
const contractDoc = firestore.doc(`contracts/${contractId}`) const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await trans.get(contractDoc) const contractSnap = await trans.get(contractDoc)
if (!contractSnap.exists) if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
throw new APIError(400, 'Contract not found.') const contract = contractSnap.data() as CPMMContract
const contract = contractSnap.data() as CPMMContract
const liquidityCollection = firestore.collection( const liquidityCollection = firestore.collection(
`contracts/${contractId}/liquidity` `contracts/${contractId}/liquidity`
) )
const liquiditiesSnap = await trans.get(liquidityCollection) const liquiditiesSnap = await trans.get(liquidityCollection)
const liquidities = liquiditiesSnap.docs.map( const liquidities = liquiditiesSnap.docs.map(
(doc) => doc.data() as LiquidityProvision (doc) => doc.data() as LiquidityProvision
) )
const userShares = getUserLiquidityShares( const userShares = getUserLiquidityShares(auth.uid, contract, liquidities)
userId,
contract,
liquidities
)
// zero all added amounts for now // zero all added amounts for now
// can add support for partial withdrawals in the future // can add support for partial withdrawals in the future
liquiditiesSnap.docs liquiditiesSnap.docs
.filter( .filter(
(_, i) => (_, i) => !liquidities[i].isAnte && liquidities[i].userId === auth.uid
!liquidities[i].isAnte && liquidities[i].userId === userId )
) .forEach((doc) => trans.update(doc.ref, { amount: 0 }))
.forEach((doc) => trans.update(doc.ref, { amount: 0 }))
const payout = Math.min(...Object.values(userShares)) const payout = Math.min(...Object.values(userShares))
if (payout <= 0) return {} if (payout <= 0) return {}
const newBalance = lp.balance + payout const newBalance = lp.balance + payout
const newTotalDeposits = lp.totalDeposits + payout const newTotalDeposits = lp.totalDeposits + payout
trans.update(lpDoc, { trans.update(lpDoc, {
balance: newBalance, balance: newBalance,
totalDeposits: newTotalDeposits, totalDeposits: newTotalDeposits,
} as Partial<User>) } as Partial<User>)
const newPool = subtractObjects(contract.pool, userShares) const newPool = subtractObjects(contract.pool, userShares)
const minPoolShares = Math.min(...Object.values(newPool)) const minPoolShares = Math.min(...Object.values(newPool))
const adjustedTotal = contract.totalLiquidity - payout const adjustedTotal = contract.totalLiquidity - payout
// total liquidity is a bogus number; use minPoolShares to prevent from going negative // total liquidity is a bogus number; use minPoolShares to prevent from going negative
const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares) const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares)
trans.update(contractDoc, { trans.update(contractDoc, {
pool: newPool, pool: newPool,
totalLiquidity: newTotalLiquidity, totalLiquidity: newTotalLiquidity,
}) })
const prob = getProbability(contract) const prob = getProbability(contract)
// surplus shares become user's bets // surplus shares become user's bets
const bets = Object.entries(userShares) const bets = Object.entries(userShares)
.map(([outcome, shares]) => .map(([outcome, shares]) =>
shares - payout < 1 // don't create bet if less than 1 share shares - payout < 1 // don't create bet if less than 1 share
? undefined ? undefined
: ({ : ({
userId: userId, userId: auth.uid,
contractId: contract.id, contractId: contract.id,
amount: amount:
(outcome === 'YES' ? prob : 1 - prob) * (shares - payout), (outcome === 'YES' ? prob : 1 - prob) * (shares - payout),
shares: shares - payout, shares: shares - payout,
outcome, outcome,
probBefore: prob, probBefore: prob,
probAfter: prob, probAfter: prob,
createdTime: Date.now(), createdTime: Date.now(),
isLiquidityProvision: true, isLiquidityProvision: true,
fees: noFees, fees: noFees,
} as Omit<Bet, 'id'>) } as Omit<Bet, 'id'>)
) )
.filter((x) => x !== undefined) .filter((x) => x !== undefined)
for (const bet of bets) { for (const bet of bets) {
const doc = firestore const doc = firestore.collection(`contracts/${contract.id}/bets`).doc()
.collection(`contracts/${contract.id}/bets`) trans.create(doc, { id: doc.id, ...bet })
.doc() }
trans.create(doc, { id: doc.id, ...bet })
}
return userShares return userShares
}) })
.then(async (result) => { .then(async (result) => {
// redeem surplus bet with pre-existing bets // redeem surplus bet with pre-existing bets
await redeemShares(userId, contractId) await redeemShares(auth.uid, contractId)
console.log('userid', auth.uid, 'withdraws', result)
console.log('userid', userId, 'withdraws', result) return result
return { status: 'success', userShares: result } })
}) })
.catch((e) => {
return { status: 'error', message: e.message }
})
}
)
const firestore = admin.firestore() const firestore = admin.firestore()

View File

@ -6,7 +6,7 @@ import { findBestMatch } from 'string-similarity'
import { FreeResponseContract } from 'common/contract' import { FreeResponseContract } from 'common/contract'
import { BuyAmountInput } from '../amount-input' import { BuyAmountInput } from '../amount-input'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { createAnswer } from 'web/lib/firebase/fn-call' import { APIError, createAnswer } from 'web/lib/firebase/api-call'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { import {
formatMoney, formatMoney,
@ -46,20 +46,23 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
if (canSubmit) { if (canSubmit) {
setIsSubmitting(true) setIsSubmitting(true)
const result = await createAnswer({ try {
contractId: contract.id, await createAnswer({
text, contractId: contract.id,
amount: betAmount, text,
}).then((r) => r.data) amount: betAmount,
})
setIsSubmitting(false)
if (result.status === 'success') {
setText('') setText('')
setBetAmount(10) setBetAmount(10)
setAmountError(undefined) setAmountError(undefined)
setPossibleDuplicateAnswer(undefined) setPossibleDuplicateAnswer(undefined)
} else setAmountError(result.message) } catch (e) {
if (e instanceof APIError) {
setAmountError(e.toString())
}
}
setIsSubmitting(false)
} }
} }

View File

@ -30,7 +30,7 @@ import { useUserContractBets } from 'web/hooks/use-user-bets'
import { import {
calculateCpmmSale, calculateCpmmSale,
getCpmmProbability, getCpmmProbability,
getCpmmLiquidityFee, getCpmmFees,
} from 'common/calculate-cpmm' } from 'common/calculate-cpmm'
import { import {
getFormattedMappedValue, getFormattedMappedValue,
@ -356,7 +356,7 @@ function BuyPanel(props: {
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0 const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
const currentReturnPercent = formatPercent(currentReturn) const currentReturnPercent = formatPercent(currentReturn)
const cpmmFees = getCpmmLiquidityFee( const cpmmFees = getCpmmFees(
contract, contract,
betAmount ?? 0, betAmount ?? 0,
betChoice ?? 'YES' betChoice ?? 'YES'

View File

@ -134,6 +134,13 @@ export function QuickBet(props: { contract: Contract; user: User }) {
}) })
} }
if (outcomeType === 'FREE_RESPONSE')
return (
<Col className="relative -my-4 -mr-5 min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle">
<QuickOutcomeView contract={contract} previewProb={previewProb} />
</Col>
)
return ( return (
<Col <Col
className={clsx( className={clsx(

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
import { CPMMContract } from 'common/contract' import { CPMMContract } from 'common/contract'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { addLiquidity, withdrawLiquidity } from 'web/lib/firebase/fn-call' import { addLiquidity, withdrawLiquidity } from 'web/lib/firebase/api-call'
import { AmountInput } from './amount-input' import { AmountInput } from './amount-input'
import { Row } from './layout/row' import { Row } from './layout/row'
import { useUserLiquidity } from 'web/hooks/use-liquidity' import { useUserLiquidity } from 'web/hooks/use-liquidity'
@ -90,14 +90,10 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
setIsSuccess(false) setIsSuccess(false)
addLiquidity({ amount, contractId }) addLiquidity({ amount, contractId })
.then((r) => { .then((_) => {
if (r.status === 'success') { setIsSuccess(true)
setIsSuccess(true) setError(undefined)
setError(undefined) setIsLoading(false)
setIsLoading(false)
} else {
setError('Server error')
}
}) })
.catch((_) => setError('Server error')) .catch((_) => setError('Server error'))

View File

@ -50,6 +50,21 @@ export function getFunctionUrl(name: string) {
} }
} }
export function createAnswer(params: any) {
return call(getFunctionUrl('createanswer'), 'POST', params)
}
export function changeUserInfo(params: any) {
return call(getFunctionUrl('changeuserinfo'), 'POST', params)
}
export function addLiquidity(params: any) {
return call(getFunctionUrl('addliquidity'), 'POST', params)
}
export function withdrawLiquidity(params: any) {
return call(getFunctionUrl('withdrawliquidity'), 'POST', params)
}
export function createMarket(params: any) { export function createMarket(params: any) {
return call(getFunctionUrl('createmarket'), 'POST', params) return call(getFunctionUrl('createmarket'), 'POST', params)
} }
@ -74,6 +89,10 @@ export function sellBet(params: any) {
return call(getFunctionUrl('sellbet'), 'POST', params) return call(getFunctionUrl('sellbet'), 'POST', params)
} }
export function claimManalink(params: any) {
return call(getFunctionUrl('claimmanalink'), 'POST', params)
}
export function createGroup(params: any) { export function createGroup(params: any) {
return call(getFunctionUrl('creategroup'), 'POST', params) return call(getFunctionUrl('creategroup'), 'POST', params)
} }

View File

@ -9,26 +9,11 @@ import { safeLocalStorage } from '../util/local'
export const cloudFunction = <RequestData, ResponseData>(name: string) => export const cloudFunction = <RequestData, ResponseData>(name: string) =>
httpsCallable<RequestData, ResponseData>(functions, name) httpsCallable<RequestData, ResponseData>(functions, name)
export const withdrawLiquidity = cloudFunction<
{ contractId: string },
{ status: 'error' | 'success'; userShares: { [outcome: string]: number } }
>('withdrawLiquidity')
export const transact = cloudFunction< export const transact = cloudFunction<
Omit<Txn, 'id' | 'createdTime'>, Omit<Txn, 'id' | 'createdTime'>,
{ status: 'error' | 'success'; message?: string; txn?: Txn } { status: 'error' | 'success'; message?: string; txn?: Txn }
>('transact') >('transact')
export const createAnswer = cloudFunction<
{ contractId: string; text: string; amount: number },
{
status: 'error' | 'success'
message?: string
answerId?: string
betId?: string
}
>('createAnswer')
export const createUser: () => Promise<User | null> = () => { export const createUser: () => Promise<User | null> = () => {
const local = safeLocalStorage() const local = safeLocalStorage()
let deviceToken = local?.getItem('device-token') let deviceToken = local?.getItem('device-token')
@ -41,24 +26,3 @@ export const createUser: () => Promise<User | null> = () => {
.then((r) => (r.data as any)?.user || null) .then((r) => (r.data as any)?.user || null)
.catch(() => null) .catch(() => null)
} }
export const changeUserInfo = (data: {
username?: string
name?: string
avatarUrl?: string
}) => {
return cloudFunction('changeUserInfo')(data)
.then((r) => r.data as { status: string; message?: string })
.catch((e) => ({ status: 'error', message: e.message }))
}
export const addLiquidity = (data: { amount: number; contractId: string }) => {
return cloudFunction('addLiquidity')(data)
.then((r) => r.data as { status: string })
.catch((e) => ({ status: 'error', message: e.message }))
}
export const claimManalink = cloudFunction<
string,
{ status: 'error' | 'success'; message?: string }
>('claimManalink')

View File

@ -2,7 +2,7 @@ import { useRouter } from 'next/router'
import { useState } from 'react' import { useState } from 'react'
import { SEO } from 'web/components/SEO' import { SEO } from 'web/components/SEO'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { claimManalink } from 'web/lib/firebase/fn-call' import { claimManalink } from 'web/lib/firebase/api-call'
import { useManalink } from 'web/lib/firebase/manalinks' import { useManalink } from 'web/lib/firebase/manalinks'
import { ManalinkCard } from 'web/components/manalink-card' import { ManalinkCard } from 'web/components/manalink-card'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
@ -42,10 +42,7 @@ export default function ClaimPage() {
if (user == null) { if (user == null) {
await firebaseLogin() await firebaseLogin()
} }
const result = await claimManalink(manalink.slug) await claimManalink({ slug: manalink.slug })
if (result.data.status == 'error') {
throw new Error(result.data.message)
}
user && router.push(`/${user.username}?claimed-mana=yes`) user && router.push(`/${user.username}?claimed-mana=yes`)
} catch (e) { } catch (e) {
console.log(e) console.log(e)

View File

@ -9,7 +9,7 @@ import { Title } from 'web/components/title'
import { usePrivateUser, useUser } from 'web/hooks/use-user' import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
import { changeUserInfo } from 'web/lib/firebase/fn-call' import { changeUserInfo } from 'web/lib/firebase/api-call'
import { uploadImage } from 'web/lib/firebase/storage' import { uploadImage } from 'web/lib/firebase/storage'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
@ -85,12 +85,9 @@ export default function ProfilePage() {
if (newName) { if (newName) {
setName(newName) setName(newName)
await changeUserInfo({ name: newName }).catch((_) =>
await changeUserInfo({ name: newName }) setName(user?.name || '')
.catch(() => ({ status: 'error' })) )
.then((r) => {
if (r.status === 'error') setName(user?.name || '')
})
} else { } else {
setName(user?.name || '') setName(user?.name || '')
} }
@ -101,11 +98,9 @@ export default function ProfilePage() {
if (newUsername) { if (newUsername) {
setUsername(newUsername) setUsername(newUsername)
await changeUserInfo({ username: newUsername }) await changeUserInfo({ username: newUsername }).catch((_) =>
.catch(() => ({ status: 'error' })) setUsername(user?.username || '')
.then((r) => { )
if (r.status === 'error') setUsername(user?.username || '')
})
} else { } else {
setUsername(user?.username || '') setUsername(user?.username || '')
} }