Merge branch 'main' into limit-orders
This commit is contained in:
parent
5be2ea8583
commit
89ac26417b
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 || '')
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user