From 3145966a3c66ddf292be43b2f7008754cd39ad87 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Wed, 9 Mar 2022 11:35:11 -0600 Subject: [PATCH] share redemption --- common/bet.ts | 1 + functions/src/place-bet.ts | 142 +++++++++++++++++---------------- functions/src/redeem-shares.ts | 81 +++++++++++++++++++ 3 files changed, 156 insertions(+), 68 deletions(-) create mode 100644 functions/src/redeem-shares.ts diff --git a/common/bet.ts b/common/bet.ts index 7b0b4b79..a7795488 100644 --- a/common/bet.ts +++ b/common/bet.ts @@ -20,6 +20,7 @@ export type Bet = { isSold?: boolean // true if this BUY bet has been sold isAnte?: boolean isLiquidityProvision?: boolean + isRedemption?: boolean createdTime: number } diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 4a1e04f8..524abed1 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -11,6 +11,7 @@ import { } from '../../common/new-bet' import { removeUndefinedProps } from '../../common/util/object' import { Bet } from '../../common/bet' +import { redeemShares } from './redeem-shares' export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( @@ -33,87 +34,92 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( return { status: 'error', message: 'Invalid outcome' } // run as transaction to prevent race conditions - return await firestore.runTransaction(async (transaction) => { - const userDoc = firestore.doc(`users/${userId}`) - const userSnap = await transaction.get(userDoc) - if (!userSnap.exists) - return { status: 'error', message: 'User not found' } - const user = userSnap.data() as User + return await firestore + .runTransaction(async (transaction) => { + const userDoc = firestore.doc(`users/${userId}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) + return { status: 'error', message: 'User not found' } + const user = userSnap.data() as User - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await transaction.get(contractDoc) - if (!contractSnap.exists) - return { status: 'error', message: 'Invalid contract' } - const contract = contractSnap.data() as Contract - - const { closeTime, outcomeType, mechanism } = contract - if (closeTime && Date.now() > closeTime) - return { status: 'error', message: 'Trading is closed' } - - const yourBetsSnap = await transaction.get( - contractDoc.collection('bets').where('userId', '==', userId) - ) - const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet) - - const loanAmount = getLoanAmount(yourBets, amount) - if (user.balance < amount - loanAmount) - return { status: 'error', message: 'Insufficient balance' } - - if (outcomeType === 'FREE_RESPONSE') { - const answerSnap = await transaction.get( - contractDoc.collection('answers').doc(outcome) - ) - if (!answerSnap.exists) + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await transaction.get(contractDoc) + if (!contractSnap.exists) return { status: 'error', message: 'Invalid contract' } - } + const contract = contractSnap.data() as Contract - const newBetDoc = firestore - .collection(`contracts/${contractId}/bets`) - .doc() + const { closeTime, outcomeType, mechanism } = contract + if (closeTime && Date.now() > closeTime) + return { status: 'error', message: 'Trading is closed' } - const { newBet, newPool, newTotalShares, newTotalBets, newBalance } = - outcomeType === 'BINARY' - ? mechanism === 'dpm-2' - ? getNewBinaryDpmBetInfo( + const yourBetsSnap = await transaction.get( + contractDoc.collection('bets').where('userId', '==', userId) + ) + const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet) + + const loanAmount = getLoanAmount(yourBets, amount) + if (user.balance < amount - loanAmount) + return { status: 'error', message: 'Insufficient balance' } + + if (outcomeType === 'FREE_RESPONSE') { + const answerSnap = await transaction.get( + contractDoc.collection('answers').doc(outcome) + ) + if (!answerSnap.exists) + return { status: 'error', message: 'Invalid contract' } + } + + const newBetDoc = firestore + .collection(`contracts/${contractId}/bets`) + .doc() + + const { newBet, newPool, newTotalShares, newTotalBets, newBalance } = + outcomeType === 'BINARY' + ? mechanism === 'dpm-2' + ? getNewBinaryDpmBetInfo( + user, + outcome as 'YES' | 'NO', + amount, + contract, + loanAmount, + newBetDoc.id + ) + : (getNewBinaryCpmmBetInfo( + user, + outcome as 'YES' | 'NO', + amount, + contract, + loanAmount, + newBetDoc.id + ) as any) + : getNewMultiBetInfo( user, - outcome as 'YES' | 'NO', + outcome, amount, - contract, + contract as any, loanAmount, newBetDoc.id ) - : (getNewBinaryCpmmBetInfo( - user, - outcome as 'YES' | 'NO', - amount, - contract, - loanAmount, - newBetDoc.id - ) as any) - : getNewMultiBetInfo( - user, - outcome, - amount, - contract as any, - loanAmount, - newBetDoc.id - ) - transaction.create(newBetDoc, newBet) + transaction.create(newBetDoc, newBet) - transaction.update( - contractDoc, - removeUndefinedProps({ - pool: newPool, - totalShares: newTotalShares, - totalBets: newTotalBets, - }) - ) + transaction.update( + contractDoc, + removeUndefinedProps({ + pool: newPool, + totalShares: newTotalShares, + totalBets: newTotalBets, + }) + ) - transaction.update(userDoc, { balance: newBalance }) + transaction.update(userDoc, { balance: newBalance }) - return { status: 'success', betId: newBetDoc.id } - }) + return { status: 'success', betId: newBetDoc.id } + }) + .then(async (result) => { + await redeemShares(userId, contractId) + return result + }) } ) diff --git a/functions/src/redeem-shares.ts b/functions/src/redeem-shares.ts new file mode 100644 index 00000000..e052fa3e --- /dev/null +++ b/functions/src/redeem-shares.ts @@ -0,0 +1,81 @@ +import * as admin from 'firebase-admin' +import * as _ from 'lodash' + +import { Bet } from '../../common/bet' +import { getProbability } from '../../common/calculate' + +import { Binary, CPMM, FullContract } from '../../common/contract' +import { User } from '../../common/user' + +export const redeemShares = async (userId: string, contractId: string) => { + return await firestore.runTransaction(async (transaction) => { + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await transaction.get(contractDoc) + if (!contractSnap.exists) + return { status: 'error', message: 'Invalid contract' } + + const contract = contractSnap.data() as FullContract + if (contract.outcomeType !== 'BINARY' || contract.mechanism !== 'cpmm-1') + return { status: 'success' } + + const betsSnap = await transaction.get( + firestore + .collection(`contracts/${contract.id}/bets`) + .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 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, + outcome: 'YES', + probBefore: p, + probAfter: p, + createdTime, + isRedemption: true, + } + + 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, + outcome: 'NO', + probBefore: p, + probAfter: p, + createdTime, + isRedemption: true, + } + + const userDoc = firestore.doc(`users/${userId}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) return { status: 'error', message: 'User not found' } + + const user = userSnap.data() as User + + const newBalance = user.balance + amount + transaction.update(userDoc, { balance: newBalance }) + + transaction.create(yesDoc, yesBet) + transaction.create(noDoc, noBet) + + return { status: 'success' } + }) +} + +const firestore = admin.firestore()