diff --git a/common/new-bet.ts b/common/new-bet.ts index 236c0908..57739af3 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -20,9 +20,9 @@ import { noFees } from './fees' import { addObjects } from './util/object' import { NUMERIC_FIXED_VAR } from './numeric-constants' -export type CandidateBet = Omit +export type CandidateBet = Omit export type BetInfo = { - newBet: CandidateBet + newBet: CandidateBet newPool?: { [outcome: string]: number } newTotalShares?: { [outcome: string]: number } newTotalBets?: { [outcome: string]: number } @@ -46,7 +46,7 @@ export const getNewBinaryCpmmBetInfo = ( const probBefore = getCpmmProbability(pool, p) const probAfter = getCpmmProbability(newPool, newP) - const newBet: CandidateBet = { + const newBet: CandidateBet = { contractId: contract.id, amount, shares, @@ -96,7 +96,7 @@ export const getNewBinaryDpmBetInfo = ( const probBefore = getDpmProbability(contract.totalShares) const probAfter = getDpmProbability(newTotalShares) - const newBet: CandidateBet = { + const newBet: CandidateBet = { contractId: contract.id, amount, loanAmount, @@ -133,7 +133,7 @@ export const getNewMultiBetInfo = ( const probBefore = getDpmOutcomeProbability(totalShares, outcome) const probAfter = getDpmOutcomeProbability(newTotalShares, outcome) - const newBet: CandidateBet = { + const newBet: CandidateBet = { contractId: contract.id, amount, loanAmount, diff --git a/common/redeem.ts b/common/redeem.ts new file mode 100644 index 00000000..4a4080f6 --- /dev/null +++ b/common/redeem.ts @@ -0,0 +1,54 @@ +import { partition, sumBy } from 'lodash' + +import { Bet } from './bet' +import { getProbability } from './calculate' +import { CPMMContract } from './contract' +import { noFees } from './fees' +import { CandidateBet } from './new-bet' + +type RedeemableBet = Pick + +export const getRedeemableAmount = (bets: RedeemableBet[]) => { + const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES') + const yesShares = sumBy(yesBets, (b) => b.shares) + const noShares = sumBy(noBets, (b) => b.shares) + const shares = Math.max(Math.min(yesShares, noShares), 0) + const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0) + const loanPayment = Math.min(loanAmount, shares) + const netAmount = shares - loanPayment + return { shares, loanPayment, netAmount } +} + +export const getRedemptionBets = ( + shares: number, + loanPayment: number, + contract: CPMMContract +) => { + const p = getProbability(contract) + const createdTime = Date.now() + const yesBet: CandidateBet = { + contractId: contract.id, + amount: p * -shares, + shares: -shares, + loanAmount: loanPayment ? -loanPayment / 2 : 0, + outcome: 'YES', + probBefore: p, + probAfter: p, + createdTime, + isRedemption: true, + fees: noFees, + } + const noBet: CandidateBet = { + contractId: contract.id, + amount: (1 - p) * -shares, + shares: -shares, + loanAmount: loanPayment ? -loanPayment / 2 : 0, + outcome: 'NO', + probBefore: p, + probAfter: p, + createdTime, + isRedemption: true, + fees: noFees, + } + return [yesBet, noBet] +} diff --git a/functions/src/redeem-shares.ts b/functions/src/redeem-shares.ts index 67922a65..32b1d433 100644 --- a/functions/src/redeem-shares.ts +++ b/functions/src/redeem-shares.ts @@ -1,96 +1,43 @@ import * as admin from 'firebase-admin' -import { partition, sumBy } from 'lodash' import { Bet } from '../../common/bet' -import { getProbability } from '../../common/calculate' +import { getRedeemableAmount, getRedemptionBets } from '../../common/redeem' import { Contract } from '../../common/contract' -import { noFees } from '../../common/fees' import { User } from '../../common/user' export const redeemShares = async (userId: string, contractId: string) => { - return await firestore.runTransaction(async (transaction) => { + return await firestore.runTransaction(async (trans) => { const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await transaction.get(contractDoc) + const contractSnap = await trans.get(contractDoc) if (!contractSnap.exists) return { status: 'error', message: 'Invalid contract' } const contract = contractSnap.data() as Contract - const { mechanism, outcomeType } = contract - if ( - !(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') || - mechanism !== 'cpmm-1' - ) - return { status: 'success' } + const { mechanism } = contract + if (mechanism !== 'cpmm-1') return { status: 'success' } - const betsSnap = await transaction.get( - firestore - .collection(`contracts/${contract.id}/bets`) - .where('userId', '==', userId) - ) + const betsColl = firestore.collection(`contracts/${contract.id}/bets`) + const betsSnap = await trans.get(betsColl.where('userId', '==', userId)) const bets = betsSnap.docs.map((doc) => doc.data() as Bet) - const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES') - const yesShares = sumBy(yesBets, (b) => b.shares) - const noShares = sumBy(noBets, (b) => b.shares) - - const amount = Math.min(yesShares, noShares) - if (amount <= 0) return - - const prevLoanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0) - const loanPaid = Math.min(prevLoanAmount, amount) - const netAmount = amount - loanPaid - - const p = getProbability(contract) - const createdTime = Date.now() - - const yesDoc = firestore.collection(`contracts/${contract.id}/bets`).doc() - const yesBet: Bet = { - id: yesDoc.id, - userId: userId, - contractId: contract.id, - amount: p * -amount, - shares: -amount, - loanAmount: loanPaid ? -loanPaid / 2 : 0, - outcome: 'YES', - probBefore: p, - probAfter: p, - createdTime, - isRedemption: true, - fees: noFees, - } - - const noDoc = firestore.collection(`contracts/${contract.id}/bets`).doc() - const noBet: Bet = { - id: noDoc.id, - userId: userId, - contractId: contract.id, - amount: (1 - p) * -amount, - shares: -amount, - loanAmount: loanPaid ? -loanPaid / 2 : 0, - outcome: 'NO', - probBefore: p, - probAfter: p, - createdTime, - isRedemption: true, - fees: noFees, - } + const { shares, loanPayment, netAmount } = getRedeemableAmount(bets) + const [yesBet, noBet] = getRedemptionBets(shares, loanPayment, contract) const userDoc = firestore.doc(`users/${userId}`) - const userSnap = await transaction.get(userDoc) + const userSnap = await trans.get(userDoc) if (!userSnap.exists) return { status: 'error', message: 'User not found' } - const user = userSnap.data() as User - const newBalance = user.balance + netAmount if (!isFinite(newBalance)) { throw new Error('Invalid user balance for ' + user.username) } - transaction.update(userDoc, { balance: newBalance }) - - transaction.create(yesDoc, yesBet) - transaction.create(noDoc, noBet) + const yesDoc = betsColl.doc() + const noDoc = betsColl.doc() + trans.update(userDoc, { balance: newBalance }) + trans.create(yesDoc, { id: yesDoc.id, userId, ...yesBet }) + trans.create(noDoc, { id: noDoc.id, userId, ...noBet }) return { status: 'success' } })