f533d9bfcb
* Fetch balance of users with open limit orders & cancel orders with insufficient balance
* Fix imports
* Fix bugs
* Fix a bug
* Remove redundant cast
* buttons overlaying content fix (#1005)
* buttons overlaying content fix
* stats: round DAU number
* made set width for portfolio/profit fields (#1006)
* tournaments: included resolved markets
* made delete red, moved button for regular posts (#1008)
* Fix localstorage saved user being overwritten on every page load
* Market page: Show no right panel while user loading
* Don't flash sign in button if user is loading
* election map coloring
* market group modal scroll fix (#1009)
* midterms: posititoning, make mobile friendly
* Un-daisy share buttons (#1010)
* Make embed and challenge buttons non-daisyui
* Allow link Buttons. Change tweet, dupe buttons.
* lint
* don't insert extra lines when upload photos
* Map fixes (#1011)
* usa map: fix sizing
* useSetIframeBackbroundColor
* preload contracts
* seo
* remove hook
* turn off sprig on dev
* Render timestamp only on client to prevent error of server not matching client
* Make sized container have default height so graph doesn't jump
* midterms: use null in static props
* Create common card component (#1012)
* Create common card component
* lint
* add key prop to pills
* redirect to /home after login
* create market: use transaction
* card: reduce border size
* Update groupContracts in db trigger
* Default sort to best
* Save comment sort per user rather than per contract
* Refactor Pinned Items into a reusable component
* Revert "create market: use transaction"
This reverts commit e1f24f24a9
.
* Mark @v with a (Bot) label
* fix padding on daily movers
* fix type errors
* Wrap sprig init in check for window
* unindex date-docs from search engines
* Auto-prettification
* compute elasticity
* change dpm elasticity
* Fix google lighthouse issues (#1013)
* don't hide free response panel on open resolve
* liquidity sort
* Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'.
* Date doc: Toggle to disable creating a prediction market
* Listen for date doc changes
* Fix merge error
* Don't cancel all a users limit orders if they go negative
Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
Co-authored-by: mantikoros <95266179+mantikoros@users.noreply.github.com>
Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: Pico2x <pico2x@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: sipec <sipec@users.noreply.github.com>
154 lines
5.2 KiB
TypeScript
154 lines
5.2 KiB
TypeScript
import { mapValues, groupBy, sumBy, uniq } from 'lodash'
|
|
import * as admin from 'firebase-admin'
|
|
import { z } from 'zod'
|
|
import { FieldValue } from 'firebase-admin/firestore'
|
|
|
|
import { APIError, newEndpoint, validate } from './api'
|
|
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
|
|
import { User } from '../../common/user'
|
|
import { getCpmmSellBetInfo } from '../../common/sell-bet'
|
|
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
|
import { log } from './utils'
|
|
import { Bet } from '../../common/bet'
|
|
import { floatingEqual, floatingLesserEqual } from '../../common/util/math'
|
|
import { getUnfilledBetsAndUserBalances, updateMakers } from './place-bet'
|
|
import { redeemShares } from './redeem-shares'
|
|
import { removeUserFromContractFollowers } from './follow-market'
|
|
|
|
const bodySchema = z.object({
|
|
contractId: z.string(),
|
|
shares: z.number().optional(), // leave it out to sell all shares
|
|
outcome: z.enum(['YES', 'NO']).optional(), // leave it out to sell whichever you have
|
|
})
|
|
|
|
export const sellshares = newEndpoint({}, async (req, auth) => {
|
|
const { contractId, shares, outcome } = validate(bodySchema, req.body)
|
|
|
|
// Run as transaction to prevent race conditions.
|
|
const result = await firestore.runTransaction(async (transaction) => {
|
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
|
const userDoc = firestore.doc(`users/${auth.uid}`)
|
|
const betsQ = contractDoc.collection('bets').where('userId', '==', auth.uid)
|
|
const [
|
|
[contractSnap, userSnap],
|
|
userBetsSnap,
|
|
{ unfilledBets, balanceByUserId },
|
|
] = await Promise.all([
|
|
transaction.getAll(contractDoc, userDoc),
|
|
transaction.get(betsQ),
|
|
getUnfilledBetsAndUserBalances(transaction, contractDoc),
|
|
])
|
|
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
|
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
|
const userBets = userBetsSnap.docs.map((doc) => doc.data() as Bet)
|
|
|
|
const contract = contractSnap.data() as Contract
|
|
const user = userSnap.data() as User
|
|
|
|
const { closeTime, mechanism, collectedFees, volume } = contract
|
|
|
|
if (mechanism !== 'cpmm-1')
|
|
throw new APIError(400, 'You can only sell shares on CPMM-1 contracts.')
|
|
if (closeTime && Date.now() > closeTime)
|
|
throw new APIError(400, 'Trading is closed.')
|
|
|
|
const loanAmount = sumBy(userBets, (bet) => bet.loanAmount ?? 0)
|
|
const betsByOutcome = groupBy(userBets, (bet) => bet.outcome)
|
|
const sharesByOutcome = mapValues(betsByOutcome, (bets) =>
|
|
sumBy(bets, (b) => b.shares)
|
|
)
|
|
|
|
let chosenOutcome: 'YES' | 'NO'
|
|
if (outcome != null) {
|
|
chosenOutcome = outcome
|
|
} else {
|
|
const nonzeroShares = Object.entries(sharesByOutcome).filter(
|
|
([_k, v]) => !floatingEqual(0, v)
|
|
)
|
|
if (nonzeroShares.length == 0) {
|
|
throw new APIError(400, "You don't own any shares in this market.")
|
|
}
|
|
if (nonzeroShares.length > 1) {
|
|
throw new APIError(
|
|
400,
|
|
`You own multiple kinds of shares, but did not specify which to sell.`
|
|
)
|
|
}
|
|
chosenOutcome = nonzeroShares[0][0] as 'YES' | 'NO'
|
|
}
|
|
|
|
const maxShares = sharesByOutcome[chosenOutcome]
|
|
const sharesToSell = shares ?? maxShares
|
|
|
|
if (!floatingLesserEqual(sharesToSell, maxShares))
|
|
throw new APIError(400, `You can only sell up to ${maxShares} shares.`)
|
|
|
|
const soldShares = Math.min(sharesToSell, maxShares)
|
|
const saleFrac = soldShares / maxShares
|
|
let loanPaid = saleFrac * loanAmount
|
|
if (!isFinite(loanPaid)) loanPaid = 0
|
|
|
|
const { newBet, newPool, newP, fees, makers, ordersToCancel } =
|
|
getCpmmSellBetInfo(
|
|
soldShares,
|
|
chosenOutcome,
|
|
contract,
|
|
unfilledBets,
|
|
balanceByUserId,
|
|
loanPaid
|
|
)
|
|
|
|
if (
|
|
!newP ||
|
|
!isFinite(newP) ||
|
|
Math.min(...Object.values(newPool ?? {})) < CPMM_MIN_POOL_QTY
|
|
) {
|
|
throw new APIError(400, 'Sale too large for current liquidity pool.')
|
|
}
|
|
|
|
const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
|
|
|
|
updateMakers(makers, newBetDoc.id, contractDoc, transaction)
|
|
|
|
transaction.update(userDoc, {
|
|
balance: FieldValue.increment(-newBet.amount + (newBet.loanAmount ?? 0)),
|
|
})
|
|
transaction.create(newBetDoc, {
|
|
id: newBetDoc.id,
|
|
userId: user.id,
|
|
userAvatarUrl: user.avatarUrl,
|
|
userUsername: user.username,
|
|
userName: user.name,
|
|
...newBet,
|
|
})
|
|
transaction.update(
|
|
contractDoc,
|
|
removeUndefinedProps({
|
|
pool: newPool,
|
|
p: newP,
|
|
collectedFees: addObjects(fees, collectedFees),
|
|
volume: volume + Math.abs(newBet.amount),
|
|
})
|
|
)
|
|
|
|
for (const bet of ordersToCancel) {
|
|
transaction.update(contractDoc.collection('bets').doc(bet.id), {
|
|
isCancelled: true,
|
|
})
|
|
}
|
|
|
|
return { newBet, makers, maxShares, soldShares }
|
|
})
|
|
|
|
if (result.maxShares === result.soldShares) {
|
|
await removeUserFromContractFollowers(contractId, auth.uid)
|
|
}
|
|
const userIds = uniq(result.makers.map((maker) => maker.bet.userId))
|
|
await Promise.all(userIds.map((userId) => redeemShares(userId, contractId)))
|
|
log('Share redemption transaction finished.')
|
|
|
|
return { status: 'success' }
|
|
})
|
|
|
|
const firestore = admin.firestore()
|