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,55 +1,47 @@
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')
) )
return { status: 'error', message: 'Invalid contract' } throw new APIError(400, '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`)
@ -83,7 +75,7 @@ export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall(
const newTotalDeposits = user.totalDeposits - amount const newTotalDeposits = user.totalDeposits - amount
if (!isFinite(newBalance)) { if (!isFinite(newBalance)) {
throw new Error('Invalid user balance for ' + user.username) throw new APIError(500, 'Invalid user balance for ' + user.username)
} }
transaction.update(userDoc, { transaction.update(userDoc, {
@ -93,13 +85,12 @@ export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall(
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
return { status: 'success', newLiquidityProvision } return newLiquidityProvision
}) })
.then(async (result) => { .then(async (result) => {
await redeemShares(userId, contractId) await redeemShares(auth.uid, contractId)
return result 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)
if (!user) return { status: 'error', message: 'User not found' }
const { username, name, avatarUrl } = data
return await changeUser(user, { username, name, avatarUrl })
.then(() => {
console.log('succesfully changed', user.username, 'to', data)
return { status: 'success' }
}) })
.catch((e) => {
console.log('Error', e.message) export const changeuserinfo = newEndpoint({}, async (req, auth) => {
return { status: 'error', message: e.message } const { username, name, avatarUrl } = validate(bodySchema, req.body)
const user = await getUser(auth.uid)
if (!user) throw new APIError(400, 'User not found')
await changeUser(user, { username, name, avatarUrl })
return { message: 'Successfully changed user info.' }
}) })
}
)
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,15 +1,17 @@
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' } export const claimmanalink = newEndpoint({}, async (req, auth) => {
const { slug } = validate(bodySchema, req.body)
// Run as transaction to prevent race conditions. // Run as transaction to prevent race conditions.
return await firestore.runTransaction(async (transaction) => { return await firestore.runTransaction(async (transaction) => {
@ -17,85 +19,85 @@ export const claimManalink = functions
const manalinkDoc = firestore.doc(`manalinks/${slug}`) const manalinkDoc = firestore.doc(`manalinks/${slug}`)
const manalinkSnap = await transaction.get(manalinkDoc) const manalinkSnap = await transaction.get(manalinkDoc)
if (!manalinkSnap.exists) { if (!manalinkSnap.exists) {
return { status: 'error', message: 'Manalink not found' } throw new APIError(400, 'Manalink not found')
} }
const manalink = manalinkSnap.data() as Manalink const manalink = manalinkSnap.data() as Manalink
const { amount, fromId, claimedUserIds } = manalink const { amount, fromId, claimedUserIds } = manalink
if (amount <= 0 || isNaN(amount) || !isFinite(amount)) if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' } throw new APIError(500, 'Invalid amount')
const fromDoc = firestore.doc(`users/${fromId}`) const fromDoc = firestore.doc(`users/${fromId}`)
const fromSnap = await transaction.get(fromDoc) const fromSnap = await transaction.get(fromDoc)
if (!fromSnap.exists) { if (!fromSnap.exists) {
return { status: 'error', message: `User ${fromId} not found` } throw new APIError(500, `User ${fromId} not found`)
} }
const fromUser = fromSnap.data() as User const fromUser = fromSnap.data() as User
// Only permit one redemption per user per link // Only permit one redemption per user per link
if (claimedUserIds.includes(userId)) { if (claimedUserIds.includes(auth.uid)) {
return { throw new APIError(400, `You already redeemed manalink ${slug}`)
status: 'error',
message: `${fromUser.name} already redeemed manalink ${slug}`,
}
} }
// Disallow expired or maxed out links // Disallow expired or maxed out links
if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) { if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) {
return { throw new APIError(
status: 'error', 400,
message: `Manalink ${slug} expired on ${new Date( `Manalink ${slug} expired on ${new Date(
manalink.expiresTime manalink.expiresTime
).toLocaleString()}`, ).toLocaleString()}`
} )
} }
if ( if (
manalink.maxUses != null && manalink.maxUses != null &&
manalink.maxUses <= manalink.claims.length manalink.maxUses <= manalink.claims.length
) { ) {
return { throw new APIError(
status: 'error', 400,
message: `Manalink ${slug} has reached its max uses of ${manalink.maxUses}`, `Manalink ${slug} has reached its max uses of ${manalink.maxUses}`
} )
} }
if (fromUser.balance < amount) { if (fromUser.balance < amount) {
return { throw new APIError(
status: 'error', 400,
message: `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} `, `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} `
} )
} }
// Actually execute the txn // Actually execute the txn
const data: TxnData = { const data: TxnData = {
fromId, fromId,
fromType: 'USER', fromType: 'USER',
toId: userId, toId: auth.uid,
toType: 'USER', toType: 'USER',
amount, amount,
token: 'M$', token: 'M$',
category: 'MANALINK', category: 'MANALINK',
description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${userId}`, description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${auth.uid}`,
} }
const result = await runTxn(transaction, data) const result = await runTxn(transaction, data)
const txnId = result.txn?.id const txnId = result.txn?.id
if (!txnId) { if (!txnId) {
return { status: 'error', message: result.message } throw new APIError(
500,
result.message ?? 'An error occurred posting the transaction.'
)
} }
// Update the manalink object with this info // Update the manalink object with this info
const claim = { const claim = {
toId: userId, toId: auth.uid,
txnId, txnId,
claimedTime: Date.now(), claimedTime: Date.now(),
} }
transaction.update(manalinkDoc, { transaction.update(manalinkDoc, {
claimedUserIds: [...claimedUserIds, userId], claimedUserIds: [...claimedUserIds, auth.uid],
claims: [...manalink.claims, claim], claims: [...manalink.claims, claim],
}) })
return { status: 'success', message: 'Manalink claimed' } return { message: 'Manalink claimed' }
}) })
}) })

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,55 +7,41 @@ 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
@ -64,8 +50,7 @@ export const createAnswer = functions
.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}`
@ -96,9 +81,7 @@ export const createAnswer = functions
getNewMultiBetInfo(answerId, amount, contract, loanAmount) getNewMultiBetInfo(answerId, amount, contract, loanAmount)
const newBalance = user.balance - amount const newBalance = user.balance - amount
const betDoc = firestore const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
.collection(`contracts/${contractId}/bets`)
.doc()
transaction.create(betDoc, { transaction.create(betDoc, {
id: betDoc.id, id: betDoc.id,
userId: user.id, userId: user.id,
@ -113,16 +96,14 @@ export const createAnswer = functions
volume: volume + amount, volume: volume + amount,
}) })
return { status: 'success', answerId, betId: betDoc.id, answer } return answer
}) })
const { answer } = result
const contract = await getContract(contractId) const contract = await getContract(contractId)
if (answer && contract) await sendNewAnswerEmail(answer, contract) if (answer && contract) await sendNewAnswerEmail(answer, contract)
return result 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,36 +10,26 @@ 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(
@ -52,18 +42,13 @@ export const withdrawLiquidity = functions
(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 }))
@ -98,7 +83,7 @@ export const withdrawLiquidity = functions
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),
@ -114,9 +99,7 @@ export const withdrawLiquidity = functions
.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`)
.doc()
trans.create(doc, { id: doc.id, ...bet }) trans.create(doc, { id: doc.id, ...bet })
} }
@ -124,15 +107,10 @@ export const withdrawLiquidity = functions
}) })
.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 {
await createAnswer({
contractId: contract.id, contractId: contract.id,
text, text,
amount: betAmount, amount: betAmount,
}).then((r) => r.data) })
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 || '')
} }