From f2748db21de58acbe6cb52799c0d8259ca3f34d4 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Tue, 14 Dec 2021 01:02:50 -0600 Subject: [PATCH] resolve markets --- functions/package.json | 3 +- functions/src/index.ts | 3 +- functions/src/resolve-market.ts | 107 ++++++++++++++++++++++++++++ web/components/resolution-panel.tsx | 17 +++-- 4 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 functions/src/resolve-market.ts diff --git a/functions/package.json b/functions/package.json index f9fedf5b..bb9a37d0 100644 --- a/functions/package.json +++ b/functions/package.json @@ -14,7 +14,8 @@ "main": "lib/index.js", "dependencies": { "firebase-admin": "10.0.0", - "firebase-functions": "3.16.0" + "firebase-functions": "3.16.0", + "lodash": "4.17.21" }, "devDependencies": { "firebase-functions-test": "0.3.3", diff --git a/functions/src/index.ts b/functions/src/index.ts index d866c043..cde0b30c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -2,4 +2,5 @@ import * as admin from 'firebase-admin' admin.initializeApp() -export * from './place-bet' \ No newline at end of file +export * from './place-bet' +export * from './resolve-market' \ No newline at end of file diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts new file mode 100644 index 00000000..9b2b10da --- /dev/null +++ b/functions/src/resolve-market.ts @@ -0,0 +1,107 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' +import * as _ from 'lodash' + +import { Contract } from './types/contract' +import { User } from './types/user' +import { Bet } from './types/bet' + +export const PLATFORM_FEE = 0.01 // 1% +export const CREATOR_FEE = 0.01 // 1% + +export const resolveMarket = functions + .runWith({ minInstances: 1 }) + .https + .onCall(async (data: { + outcome: string + contractId: string + }, context) => { + const userId = context?.auth?.uid + if (!userId) return { status: 'error', message: 'Not authorized' } + + const { outcome, contractId } = data + + if (!['YES', 'NO', 'CANCEL'].includes(outcome)) + return { status: 'error', message: 'Invalid outcome' } + + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await contractDoc.get() + if (!contractSnap.exists) + return { status: 'error', message: 'Invalid contract' } + const contract = contractSnap.data() as Contract + + if (contract.creatorId !== userId) + return { status: 'error', message: 'User not creator of contract' } + + if (contract.resolution) + return { status: 'error', message: 'Contract already resolved' } + + await contractDoc.update({ + isResolved: true, + resolution: outcome, + resolutionTime: Date.now() + }) + + console.log('contract ', contractId, 'resolved to:', outcome) + + const betsSnap = await firestore.collection(`contracts/${contractId}/bets`).get() + const bets = betsSnap.docs.map(doc => doc.data() as Bet) + + const payouts = outcome === 'CANCEL' + ? bets.map(bet => ({ + userId: bet.userId, + payout: bet.amount + })) + + : getPayouts(outcome, contract, bets) + + console.log('payouts:', payouts) + + const groups = _.groupBy(payouts, payout => payout.userId) + const userPayouts = _.mapValues(groups, group => _.sumBy(group, g => g.payout)) + + const payoutPromises = Object + .entries(userPayouts) + .map(payUser) + + return await Promise.all(payoutPromises) + .catch(e => ({ status: 'error', message: e })) + .then(() => ({ status: 'success' })) + }) + +const firestore = admin.firestore() + +const getPayouts = (outcome: string, contract: Contract, bets: Bet[]) => { + const [yesBets, noBets] = _.partition(bets, bet => bet.outcome === 'YES') + + const [pot, winningBets] = outcome === 'YES' + ? [contract.pot.NO, yesBets] + : [contract.pot.YES, noBets] + + const finalPot = (1 - PLATFORM_FEE - CREATOR_FEE) * pot + const creatorPayout = CREATOR_FEE * pot + console.log('final pot:', finalPot, 'creator fee:', creatorPayout) + + const sumWeights = _.sumBy(winningBets, bet => bet.dpmWeight) + + const winnerPayouts = winningBets.map(bet => ({ + userId: bet.userId, + payout: bet.amount + (bet.dpmWeight / sumWeights * finalPot) + })) + + return winnerPayouts + .concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee +} + +const payUser = ([userId, payout]: [string, number]) => { + return firestore.runTransaction(async transaction => { + const userDoc = firestore.doc(`users/${userId}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) return + const user = userSnap.data() as User + + const newUserBalance = user.balance + payout + transaction.update(userDoc, { balance: newUserBalance }) + }) +} + diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index acdd38e7..526701e8 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -1,5 +1,6 @@ import clsx from 'clsx' import React, { useState } from 'react' +import { getFunctions, httpsCallable } from 'firebase/functions' import { Contract } from '../lib/firebase/contracts' import { Col } from './layout/col' @@ -9,6 +10,9 @@ import { YesNoCancelSelector } from './yes-no-selector' import { Spacer } from './layout/spacer' import { ConfirmationModal } from './confirmation-modal' +const functions = getFunctions() +export const resolveMarket = httpsCallable(functions, 'resolveMarket') + export function ResolutionPanel(props: { creator: User contract: Contract @@ -18,18 +22,19 @@ export function ResolutionPanel(props: { const [outcome, setOutcome] = useState<'YES' | 'NO' | 'CANCEL' | undefined>() - function resolve() { - console.log('resolved', outcome) + const resolve = async () => { + const result = await resolveMarket({ outcome, contractId: contract.id }) + console.log('resolved', outcome, 'result:', result.data) } const submitButtonClass = outcome === 'YES' ? 'btn-primary' : outcome === 'NO' - ? 'bg-red-400 hover:bg-red-500' - : outcome === 'CANCEL' - ? 'bg-yellow-400 hover:bg-yellow-500' - : 'btn-disabled' + ? 'bg-red-400 hover:bg-red-500' + : outcome === 'CANCEL' + ? 'bg-yellow-400 hover:bg-yellow-500' + : 'btn-disabled' return (