diff --git a/common/.eslintrc.js b/common/.eslintrc.js index 3d6cfa82..c6f9703e 100644 --- a/common/.eslintrc.js +++ b/common/.eslintrc.js @@ -1,6 +1,7 @@ module.exports = { plugins: ['lodash'], extends: ['eslint:recommended'], + ignorePatterns: ['lib'], env: { browser: true, node: true, @@ -31,6 +32,7 @@ module.exports = { rules: { 'no-extra-semi': 'off', 'no-constant-condition': ['error', { checkLoops: false }], + 'linebreak-style': ['error', 'unix'], 'lodash/import-scope': [2, 'member'], }, } diff --git a/functions/.eslintrc.js b/functions/.eslintrc.js index 7f571610..2c607231 100644 --- a/functions/.eslintrc.js +++ b/functions/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { plugins: ['lodash'], extends: ['eslint:recommended'], - ignorePatterns: ['lib'], + ignorePatterns: ['dist', 'lib'], env: { node: true, }, @@ -30,6 +30,7 @@ module.exports = { }, ], rules: { + 'linebreak-style': ['error', 'unix'], 'lodash/import-scope': [2, 'member'], }, } diff --git a/functions/src/withdraw-liquidity.ts b/functions/src/withdraw-liquidity.ts index 4c48ce49..cc8c84cf 100644 --- a/functions/src/withdraw-liquidity.ts +++ b/functions/src/withdraw-liquidity.ts @@ -1,138 +1,138 @@ -import * as functions from 'firebase-functions' -import * as admin from 'firebase-admin' - -import { CPMMContract } from '../../common/contract' -import { User } from '../../common/user' -import { subtractObjects } from '../../common/util/object' -import { LiquidityProvision } from '../../common/liquidity-provision' -import { getUserLiquidityShares } from '../../common/calculate-cpmm' -import { Bet } from '../../common/bet' -import { getProbability } from '../../common/calculate' -import { noFees } from '../../common/fees' - -import { APIError } from './api' -import { redeemShares } from './redeem-shares' - -export const withdrawLiquidity = functions - .runWith({ minInstances: 1 }) - .https.onCall( - async ( - data: { - contractId: string - }, - context - ) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } - - const { contractId } = data - if (!contractId) - return { status: 'error', message: 'Missing contract id' } - - return await firestore - .runTransaction(async (trans) => { - const lpDoc = firestore.doc(`users/${userId}`) - const lpSnap = await trans.get(lpDoc) - if (!lpSnap.exists) throw new APIError(400, 'User not found.') - const lp = lpSnap.data() as User - - const contractDoc = firestore.doc(`contracts/${contractId}`) - const contractSnap = await trans.get(contractDoc) - if (!contractSnap.exists) - throw new APIError(400, 'Contract not found.') - const contract = contractSnap.data() as CPMMContract - - const liquidityCollection = firestore.collection( - `contracts/${contractId}/liquidity` - ) - - const liquiditiesSnap = await trans.get(liquidityCollection) - - const liquidities = liquiditiesSnap.docs.map( - (doc) => doc.data() as LiquidityProvision - ) - - const userShares = getUserLiquidityShares( - userId, - contract, - liquidities - ) - - // zero all added amounts for now - // can add support for partial withdrawals in the future - liquiditiesSnap.docs - .filter( - (_, i) => - !liquidities[i].isAnte && liquidities[i].userId === userId - ) - .forEach((doc) => trans.update(doc.ref, { amount: 0 })) - - const payout = Math.min(...Object.values(userShares)) - if (payout <= 0) return {} - - const newBalance = lp.balance + payout - const newTotalDeposits = lp.totalDeposits + payout - trans.update(lpDoc, { - balance: newBalance, - totalDeposits: newTotalDeposits, - } as Partial) - - const newPool = subtractObjects(contract.pool, userShares) - - const minPoolShares = Math.min(...Object.values(newPool)) - const adjustedTotal = contract.totalLiquidity - payout - - // total liquidity is a bogus number; use minPoolShares to prevent from going negative - const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares) - - trans.update(contractDoc, { - pool: newPool, - totalLiquidity: newTotalLiquidity, - }) - - const prob = getProbability(contract) - - // surplus shares become user's bets - const bets = Object.entries(userShares) - .map(([outcome, shares]) => - shares - payout < 1 // don't create bet if less than 1 share - ? undefined - : ({ - userId: userId, - contractId: contract.id, - amount: - (outcome === 'YES' ? prob : 1 - prob) * (shares - payout), - shares: shares - payout, - outcome, - probBefore: prob, - probAfter: prob, - createdTime: Date.now(), - isLiquidityProvision: true, - fees: noFees, - } as Omit) - ) - .filter((x) => x !== undefined) - - for (const bet of bets) { - const doc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() - trans.create(doc, { id: doc.id, ...bet }) - } - - return userShares - }) - .then(async (result) => { - // redeem surplus bet with pre-existing bets - await redeemShares(userId, contractId) - - console.log('userid', userId, 'withdraws', result) - return { status: 'success', userShares: result } - }) - .catch((e) => { - return { status: 'error', message: e.message } - }) - } - ) - -const firestore = admin.firestore() +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +import { CPMMContract } from '../../common/contract' +import { User } from '../../common/user' +import { subtractObjects } from '../../common/util/object' +import { LiquidityProvision } from '../../common/liquidity-provision' +import { getUserLiquidityShares } from '../../common/calculate-cpmm' +import { Bet } from '../../common/bet' +import { getProbability } from '../../common/calculate' +import { noFees } from '../../common/fees' + +import { APIError } from './api' +import { redeemShares } from './redeem-shares' + +export const withdrawLiquidity = functions + .runWith({ minInstances: 1 }) + .https.onCall( + async ( + data: { + contractId: string + }, + context + ) => { + const userId = context?.auth?.uid + if (!userId) return { status: 'error', message: 'Not authorized' } + + const { contractId } = data + if (!contractId) + return { status: 'error', message: 'Missing contract id' } + + return await firestore + .runTransaction(async (trans) => { + const lpDoc = firestore.doc(`users/${userId}`) + const lpSnap = await trans.get(lpDoc) + if (!lpSnap.exists) throw new APIError(400, 'User not found.') + const lp = lpSnap.data() as User + + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await trans.get(contractDoc) + if (!contractSnap.exists) + throw new APIError(400, 'Contract not found.') + const contract = contractSnap.data() as CPMMContract + + const liquidityCollection = firestore.collection( + `contracts/${contractId}/liquidity` + ) + + const liquiditiesSnap = await trans.get(liquidityCollection) + + const liquidities = liquiditiesSnap.docs.map( + (doc) => doc.data() as LiquidityProvision + ) + + const userShares = getUserLiquidityShares( + userId, + contract, + liquidities + ) + + // zero all added amounts for now + // can add support for partial withdrawals in the future + liquiditiesSnap.docs + .filter( + (_, i) => + !liquidities[i].isAnte && liquidities[i].userId === userId + ) + .forEach((doc) => trans.update(doc.ref, { amount: 0 })) + + const payout = Math.min(...Object.values(userShares)) + if (payout <= 0) return {} + + const newBalance = lp.balance + payout + const newTotalDeposits = lp.totalDeposits + payout + trans.update(lpDoc, { + balance: newBalance, + totalDeposits: newTotalDeposits, + } as Partial) + + const newPool = subtractObjects(contract.pool, userShares) + + const minPoolShares = Math.min(...Object.values(newPool)) + const adjustedTotal = contract.totalLiquidity - payout + + // total liquidity is a bogus number; use minPoolShares to prevent from going negative + const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares) + + trans.update(contractDoc, { + pool: newPool, + totalLiquidity: newTotalLiquidity, + }) + + const prob = getProbability(contract) + + // surplus shares become user's bets + const bets = Object.entries(userShares) + .map(([outcome, shares]) => + shares - payout < 1 // don't create bet if less than 1 share + ? undefined + : ({ + userId: userId, + contractId: contract.id, + amount: + (outcome === 'YES' ? prob : 1 - prob) * (shares - payout), + shares: shares - payout, + outcome, + probBefore: prob, + probAfter: prob, + createdTime: Date.now(), + isLiquidityProvision: true, + fees: noFees, + } as Omit) + ) + .filter((x) => x !== undefined) + + for (const bet of bets) { + const doc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() + trans.create(doc, { id: doc.id, ...bet }) + } + + return userShares + }) + .then(async (result) => { + // redeem surplus bet with pre-existing bets + await redeemShares(userId, contractId) + + console.log('userid', userId, 'withdraws', result) + return { status: 'success', userShares: result } + }) + .catch((e) => { + return { status: 'error', message: e.message } + }) + } + ) + +const firestore = admin.firestore() diff --git a/web/.eslintrc.js b/web/.eslintrc.js index b55b3277..fec650f9 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { ], '@next/next/no-img-element': 'off', '@next/next/no-typos': 'off', + 'linebreak-style': ['error', 'unix'], 'lodash/import-scope': [2, 'member'], }, env: {