Merge branch 'main' into CPM-ui
This commit is contained in:
		
						commit
						0224305fac
					
				
							
								
								
									
										35
									
								
								common/add-liquidity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								common/add-liquidity.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | import { addCpmmLiquidity, getCpmmLiquidity } from './calculate-cpmm' | ||||||
|  | import { Binary, CPMM, FullContract } from './contract' | ||||||
|  | import { LiquidityProvision } from './liquidity-provision' | ||||||
|  | import { User } from './user' | ||||||
|  | 
 | ||||||
|  | export const getNewLiquidityProvision = ( | ||||||
|  |   user: User, | ||||||
|  |   amount: number, | ||||||
|  |   contract: FullContract<CPMM, Binary>, | ||||||
|  |   newLiquidityProvisionId: string | ||||||
|  | ) => { | ||||||
|  |   const { pool, p, totalLiquidity } = contract | ||||||
|  | 
 | ||||||
|  |   const { newPool, newP } = addCpmmLiquidity(pool, p, amount) | ||||||
|  | 
 | ||||||
|  |   const liquidity = | ||||||
|  |     getCpmmLiquidity(newPool, newP) - getCpmmLiquidity(pool, newP) | ||||||
|  | 
 | ||||||
|  |   const newLiquidityProvision: LiquidityProvision = { | ||||||
|  |     id: newLiquidityProvisionId, | ||||||
|  |     userId: user.id, | ||||||
|  |     contractId: contract.id, | ||||||
|  |     amount, | ||||||
|  |     pool: newPool, | ||||||
|  |     p: newP, | ||||||
|  |     liquidity, | ||||||
|  |     createdTime: Date.now(), | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const newTotalLiquidity = (totalLiquidity ?? 0) + amount | ||||||
|  | 
 | ||||||
|  |   const newBalance = user.balance - amount | ||||||
|  | 
 | ||||||
|  |   return { newLiquidityProvision, newPool, newP, newBalance, newTotalLiquidity } | ||||||
|  | } | ||||||
|  | @ -5,7 +5,7 @@ import { User } from './user' | ||||||
| import { LiquidityProvision } from './liquidity-provision' | import { LiquidityProvision } from './liquidity-provision' | ||||||
| import { noFees } from './fees' | import { noFees } from './fees' | ||||||
| 
 | 
 | ||||||
| export const FIXED_ANTE = 100 | export const FIXED_ANTE = 50 | ||||||
| 
 | 
 | ||||||
| // deprecated
 | // deprecated
 | ||||||
| export const PHANTOM_ANTE = 0.001 | export const PHANTOM_ANTE = 0.001 | ||||||
|  |  | ||||||
|  | @ -174,17 +174,19 @@ export function calculateCpmmSale( | ||||||
|     throw new Error('Cannot sell non-positive shares') |     throw new Error('Cannot sell non-positive shares') | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const rawSaleValue = calculateCpmmShareValue( |   const saleValue = calculateCpmmShareValue( | ||||||
|     contract, |     contract, | ||||||
|     shares, |     shares, | ||||||
|     outcome as 'YES' | 'NO' |     outcome as 'YES' | 'NO' | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
|   const { fees, remainingBet: saleValue } = getCpmmLiquidityFee( |   const fees = noFees | ||||||
|     contract, | 
 | ||||||
|     rawSaleValue, |   // const { fees, remainingBet: saleValue } = getCpmmLiquidityFee(
 | ||||||
|     outcome === 'YES' ? 'NO' : 'YES' |   //   contract,
 | ||||||
|   ) |   //   rawSaleValue,
 | ||||||
|  |   //   outcome === 'YES' ? 'NO' : 'YES'
 | ||||||
|  |   // )
 | ||||||
| 
 | 
 | ||||||
|   const { pool } = contract |   const { pool } = contract | ||||||
|   const { YES: y, NO: n } = pool |   const { YES: y, NO: n } = pool | ||||||
|  |  | ||||||
|  | @ -181,7 +181,7 @@ export function getContractBetNullMetrics() { | ||||||
| export function getTopAnswer(contract: FreeResponseContract) { | export function getTopAnswer(contract: FreeResponseContract) { | ||||||
|   const { answers } = contract |   const { answers } = contract | ||||||
|   const top = _.maxBy( |   const top = _.maxBy( | ||||||
|     answers.map((answer) => ({ |     answers?.map((answer) => ({ | ||||||
|       answer, |       answer, | ||||||
|       prob: getOutcomeProbability(contract, answer.id), |       prob: getOutcomeProbability(contract, answer.id), | ||||||
|     })), |     })), | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| export type Comment = { | export type Comment = { | ||||||
|   id: string |   id: string | ||||||
|   contractId: string |   contractId: string | ||||||
|   betId: string |   betId?: string | ||||||
|   userId: string |   userId: string | ||||||
| 
 | 
 | ||||||
|   text: string |   text: string | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								functions/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								functions/.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -1,5 +1,6 @@ | ||||||
| # Secrets | # Secrets | ||||||
| .env* | .env* | ||||||
|  | .runtimeconfig.json | ||||||
| 
 | 
 | ||||||
| # Compiled JavaScript files | # Compiled JavaScript files | ||||||
| lib/**/*.js | lib/**/*.js | ||||||
|  | @ -14,3 +15,5 @@ node_modules/ | ||||||
| package-lock.json | package-lock.json | ||||||
| ui-debug.log | ui-debug.log | ||||||
| firebase-debug.log | firebase-debug.log | ||||||
|  | firestore-debug.log | ||||||
|  | firestore_export/ | ||||||
|  | @ -19,15 +19,33 @@ Adapted from https://firebase.google.com/docs/functions/get-started | ||||||
| 2. `$ yarn` to install JS dependencies | 2. `$ yarn` to install JS dependencies | ||||||
| 3. `$ firebase login` to authenticate the CLI tools to Firebase | 3. `$ firebase login` to authenticate the CLI tools to Firebase | ||||||
| 4. `$ firebase use dev` to choose the dev project | 4. `$ firebase use dev` to choose the dev project | ||||||
| 5. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev | 
 | ||||||
|  | ### For local development | ||||||
|  | 
 | ||||||
|  | 0. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev | ||||||
|  | 1. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI | ||||||
|  | 2. `$ brew install java` to install java if you don't already have it | ||||||
|  |    1. `$ echo 'export PATH="/usr/local/opt/openjdk/bin:$PATH"' >> ~/.zshrc` to add java to your path | ||||||
|  | 3. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud | ||||||
|  | 4. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options) | ||||||
|  | 5. `$ mkdir firestore_export` to create a folder to store the exported database | ||||||
|  | 6. `$ yarn db:update-local-from-remote` to pull the remote db from Firestore to local | ||||||
|  |    1. TODO: this won't work when open source, we'll have to point to the public db | ||||||
| 
 | 
 | ||||||
| ## Developing locally | ## Developing locally | ||||||
| 
 | 
 | ||||||
| 0. `$ yarn dev` to spin up the emulators | 1. `$ yarn serve` to spin up the emulators | ||||||
|    The Emulator UI is at http://localhost:4000; the functions are hosted on :5001. |    The Emulator UI is at http://localhost:4000; the functions are hosted on :5001. | ||||||
|    Note: You have to kill and restart emulators when you change code; no hot reload =( |    Note: You have to kill and restart emulators when you change code; no hot reload =( | ||||||
|    Note2: You may even have to find the process ID of the emulator and kill it manually. | 2. `$ yarn dev:emulate` in `/web` to connect to emulators with the frontend | ||||||
| 1. Connect by uncommenting the `connectFunctionsEmulator` in `web/lib/firebase/api-call.ts` |    1. Note: emulated database is cleared after every shutdown | ||||||
|  | 
 | ||||||
|  | ## Firestore Commands | ||||||
|  | 
 | ||||||
|  | - `db:update-local-from-remote` - Pull the remote db from Firestore to local, also calls: | ||||||
|  |   - `db:backup-remote` - Exports the remote dev db to the backup folder on Google Cloud Storage (called on every `db:update-local-from-remote`) | ||||||
|  |   - `db:rename-remote-backup-folder` - Renames the remote backup folder (called on every `db:backup-remote` to preserve the previous db backup) | ||||||
|  | - `db:backup-local` - Save the local db changes to the disk (overwrites existing) | ||||||
| 
 | 
 | ||||||
| ## Debugging | ## Debugging | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,21 @@ | ||||||
| { | { | ||||||
|   "name": "functions", |   "name": "functions", | ||||||
|   "version": "1.0.0", |   "version": "1.0.0", | ||||||
|  |   "config": { | ||||||
|  |     "firestore": "dev-mantic-markets.appspot.com" | ||||||
|  |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "build": "tsc", |     "build": "tsc", | ||||||
|     "watch": "tsc -w", |     "watch": "tsc -w", | ||||||
|     "serve": "yarn build && firebase emulators:start --only functions", |  | ||||||
|     "shell": "yarn build && firebase functions:shell", |     "shell": "yarn build && firebase functions:shell", | ||||||
|     "start": "yarn shell", |     "start": "yarn shell", | ||||||
|     "deploy": "firebase deploy --only functions", |     "deploy": "firebase deploy --only functions", | ||||||
|     "logs": "firebase functions:log", |     "logs": "firebase functions:log", | ||||||
|     "dev": "yarn serve" |     "serve": "yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export", | ||||||
|  |     "db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export", | ||||||
|  |     "db:backup-local": "firebase emulators:export --force ./firestore_export", | ||||||
|  |     "db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)", | ||||||
|  |     "db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/" | ||||||
|   }, |   }, | ||||||
|   "main": "lib/functions/src/index.js", |   "main": "lib/functions/src/index.js", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |  | ||||||
							
								
								
									
										103
									
								
								functions/src/add-liquidity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								functions/src/add-liquidity.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,103 @@ | ||||||
|  | import * as functions from 'firebase-functions' | ||||||
|  | import * as admin from 'firebase-admin' | ||||||
|  | 
 | ||||||
|  | import { Contract } from '../../common/contract' | ||||||
|  | import { User } from '../../common/user' | ||||||
|  | import { removeUndefinedProps } from '../../common/util/object' | ||||||
|  | import { redeemShares } from './redeem-shares' | ||||||
|  | import { getNewLiquidityProvision } from '../../common/add-liquidity' | ||||||
|  | 
 | ||||||
|  | export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall( | ||||||
|  |   async ( | ||||||
|  |     data: { | ||||||
|  |       amount: number | ||||||
|  |       contractId: string | ||||||
|  |     }, | ||||||
|  |     context | ||||||
|  |   ) => { | ||||||
|  |     const userId = context?.auth?.uid | ||||||
|  |     if (!userId) return { status: 'error', message: 'Not authorized' } | ||||||
|  | 
 | ||||||
|  |     const { amount, contractId } = data | ||||||
|  | 
 | ||||||
|  |     if (amount <= 0 || isNaN(amount) || !isFinite(amount)) | ||||||
|  |       return { status: 'error', message: 'Invalid amount' } | ||||||
|  | 
 | ||||||
|  |     // 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 | ||||||
|  | 
 | ||||||
|  |         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 | ||||||
|  |         if ( | ||||||
|  |           contract.mechanism !== 'cpmm-1' || | ||||||
|  |           contract.outcomeType !== 'BINARY' | ||||||
|  |         ) | ||||||
|  |           return { status: 'error', message: 'Invalid contract' } | ||||||
|  | 
 | ||||||
|  |         const { closeTime } = contract | ||||||
|  |         if (closeTime && Date.now() > closeTime) | ||||||
|  |           return { status: 'error', message: 'Trading is closed' } | ||||||
|  | 
 | ||||||
|  |         if (user.balance < amount) | ||||||
|  |           return { status: 'error', message: 'Insufficient balance' } | ||||||
|  | 
 | ||||||
|  |         const newLiquidityProvisionDoc = firestore | ||||||
|  |           .collection(`contracts/${contractId}/liquidity`) | ||||||
|  |           .doc() | ||||||
|  | 
 | ||||||
|  |         const { | ||||||
|  |           newLiquidityProvision, | ||||||
|  |           newPool, | ||||||
|  |           newP, | ||||||
|  |           newBalance, | ||||||
|  |           newTotalLiquidity, | ||||||
|  |         } = getNewLiquidityProvision( | ||||||
|  |           user, | ||||||
|  |           amount, | ||||||
|  |           contract, | ||||||
|  |           newLiquidityProvisionDoc.id | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         if (newP !== undefined && !isFinite(newP)) { | ||||||
|  |           return { | ||||||
|  |             status: 'error', | ||||||
|  |             message: 'Liquidity injection rejected due to overflow error.', | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         transaction.update( | ||||||
|  |           contractDoc, | ||||||
|  |           removeUndefinedProps({ | ||||||
|  |             pool: newPool, | ||||||
|  |             p: newP, | ||||||
|  |             totalLiquidity: newTotalLiquidity, | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         if (!isFinite(newBalance)) { | ||||||
|  |           throw new Error('Invalid user balance for ' + user.username) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         transaction.update(userDoc, { balance: newBalance }) | ||||||
|  | 
 | ||||||
|  |         transaction.create(newLiquidityProvisionDoc, newLiquidityProvision) | ||||||
|  | 
 | ||||||
|  |         return { status: 'success', newLiquidityProvision } | ||||||
|  |       }) | ||||||
|  |       .then(async (result) => { | ||||||
|  |         await redeemShares(userId, contractId) | ||||||
|  |         return result | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const firestore = admin.firestore() | ||||||
|  | @ -167,7 +167,7 @@ export const sendNewCommentEmail = async ( | ||||||
|   commentCreator: User, |   commentCreator: User, | ||||||
|   contract: Contract, |   contract: Contract, | ||||||
|   comment: Comment, |   comment: Comment, | ||||||
|   bet: Bet, |   bet?: Bet, | ||||||
|   answer?: Answer |   answer?: Answer | ||||||
| ) => { | ) => { | ||||||
|   const privateUser = await getPrivateUser(userId) |   const privateUser = await getPrivateUser(userId) | ||||||
|  | @ -186,8 +186,11 @@ export const sendNewCommentEmail = async ( | ||||||
|   const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator |   const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator | ||||||
|   const { text } = comment |   const { text } = comment | ||||||
| 
 | 
 | ||||||
|   const { amount, sale, outcome } = bet |   let betDescription = '' | ||||||
|   let betDescription = `${sale ? 'sold' : 'bought'} M$ ${Math.round(amount)}` |   if (bet) { | ||||||
|  |     const { amount, sale } = bet | ||||||
|  |     betDescription = `${sale ? 'sold' : 'bought'} M$ ${Math.round(amount)}` | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   const subject = `Comment on ${question}` |   const subject = `Comment on ${question}` | ||||||
|   const from = `${commentorName} <info@manifold.markets>` |   const from = `${commentorName} <info@manifold.markets>` | ||||||
|  | @ -213,11 +216,12 @@ export const sendNewCommentEmail = async ( | ||||||
|       { from } |       { from } | ||||||
|     ) |     ) | ||||||
|   } else { |   } else { | ||||||
|  |     if (bet) { | ||||||
|       betDescription = `${betDescription} of ${toDisplayResolution( |       betDescription = `${betDescription} of ${toDisplayResolution( | ||||||
|         contract, |         contract, | ||||||
|       outcome |         bet.outcome | ||||||
|       )}` |       )}` | ||||||
| 
 |     } | ||||||
|     await sendTemplateEmail( |     await sendTemplateEmail( | ||||||
|       privateUser.email, |       privateUser.email, | ||||||
|       subject, |       subject, | ||||||
|  |  | ||||||
|  | @ -22,3 +22,4 @@ export * from './update-user-metrics' | ||||||
| export * from './backup-db' | export * from './backup-db' | ||||||
| export * from './change-user-info' | export * from './change-user-info' | ||||||
| export * from './market-close-emails' | export * from './market-close-emails' | ||||||
|  | export * from './add-liquidity' | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import { getContract, getUser, getValues } from './utils' | ||||||
| import { Comment } from '../../common/comment' | import { Comment } from '../../common/comment' | ||||||
| import { sendNewCommentEmail } from './emails' | import { sendNewCommentEmail } from './emails' | ||||||
| import { Bet } from '../../common/bet' | import { Bet } from '../../common/bet' | ||||||
|  | import { Answer } from '../../common/answer' | ||||||
| 
 | 
 | ||||||
| const firestore = admin.firestore() | const firestore = admin.firestore() | ||||||
| 
 | 
 | ||||||
|  | @ -24,18 +25,22 @@ export const onCreateComment = functions.firestore | ||||||
|     const commentCreator = await getUser(comment.userId) |     const commentCreator = await getUser(comment.userId) | ||||||
|     if (!commentCreator) return |     if (!commentCreator) return | ||||||
| 
 | 
 | ||||||
|  |     let bet: Bet | undefined | ||||||
|  |     let answer: Answer | undefined | ||||||
|  |     if (comment.betId) { | ||||||
|       const betSnapshot = await firestore |       const betSnapshot = await firestore | ||||||
|         .collection('contracts') |         .collection('contracts') | ||||||
|         .doc(contractId) |         .doc(contractId) | ||||||
|         .collection('bets') |         .collection('bets') | ||||||
|         .doc(comment.betId) |         .doc(comment.betId) | ||||||
|         .get() |         .get() | ||||||
|     const bet = betSnapshot.data() as Bet |       bet = betSnapshot.data() as Bet | ||||||
| 
 | 
 | ||||||
|     const answer = |       answer = | ||||||
|         contract.outcomeType === 'FREE_RESPONSE' && contract.answers |         contract.outcomeType === 'FREE_RESPONSE' && contract.answers | ||||||
|         ? contract.answers.find((answer) => answer.id === bet.outcome) |           ? contract.answers.find((answer) => answer.id === bet?.outcome) | ||||||
|           : undefined |           : undefined | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     const comments = await getValues<Comment>( |     const comments = await getValues<Comment>( | ||||||
|       firestore.collection('contracts').doc(contractId).collection('comments') |       firestore.collection('contracts').doc(contractId).collection('comments') | ||||||
|  |  | ||||||
							
								
								
									
										2100
									
								
								functions/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										2100
									
								
								functions/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -8,6 +8,11 @@ | ||||||
| 
 | 
 | ||||||
| (`yarn dev` will point you to prod database) | (`yarn dev` will point you to prod database) | ||||||
| 
 | 
 | ||||||
|  | ### Running with local emulated database and functions | ||||||
|  | 
 | ||||||
|  | 1. `yarn serve` first in `/functions` and wait for it to start | ||||||
|  | 2. `yarn dev:emulate` will point you to the emulated database | ||||||
|  | 
 | ||||||
| ## Formatting | ## Formatting | ||||||
| 
 | 
 | ||||||
| Before committing, run `yarn format` to format your code. | Before committing, run `yarn format` to format your code. | ||||||
|  |  | ||||||
							
								
								
									
										85
									
								
								web/components/add-liquidity-panel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								web/components/add-liquidity-panel.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,85 @@ | ||||||
|  | import clsx from 'clsx' | ||||||
|  | import { useState } from 'react' | ||||||
|  | 
 | ||||||
|  | import { Contract } from '../../common/contract' | ||||||
|  | import { formatMoney } from '../../common/util/format' | ||||||
|  | import { useUser } from '../hooks/use-user' | ||||||
|  | import { addLiquidity } from '../lib/firebase/api-call' | ||||||
|  | import { AmountInput } from './amount-input' | ||||||
|  | import { Row } from './layout/row' | ||||||
|  | 
 | ||||||
|  | export function AddLiquidityPanel(props: { contract: Contract }) { | ||||||
|  |   const { contract } = props | ||||||
|  |   const { id: contractId } = contract | ||||||
|  | 
 | ||||||
|  |   const user = useUser() | ||||||
|  | 
 | ||||||
|  |   const [amount, setAmount] = useState<number | undefined>(undefined) | ||||||
|  |   const [error, setError] = useState<string | undefined>(undefined) | ||||||
|  |   const [isSuccess, setIsSuccess] = useState(false) | ||||||
|  |   const [isLoading, setIsLoading] = useState(false) | ||||||
|  | 
 | ||||||
|  |   const onAmountChange = (amount: number | undefined) => { | ||||||
|  |     setIsSuccess(false) | ||||||
|  |     setAmount(amount) | ||||||
|  | 
 | ||||||
|  |     // Check for errors.
 | ||||||
|  |     if (amount !== undefined) { | ||||||
|  |       if (user && user.balance < amount) { | ||||||
|  |         setError('Insufficient balance') | ||||||
|  |       } else if (amount < 1) { | ||||||
|  |         setError('Minimum amount: ' + formatMoney(1)) | ||||||
|  |       } else { | ||||||
|  |         setError(undefined) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const submit = () => { | ||||||
|  |     if (!amount) return | ||||||
|  | 
 | ||||||
|  |     setIsLoading(true) | ||||||
|  |     setIsSuccess(false) | ||||||
|  | 
 | ||||||
|  |     addLiquidity({ amount, contractId }) | ||||||
|  |       .then((r) => { | ||||||
|  |         if (r.status === 'success') { | ||||||
|  |           setIsSuccess(true) | ||||||
|  |           setError(undefined) | ||||||
|  |           setIsLoading(false) | ||||||
|  |         } else { | ||||||
|  |           setError('Server error') | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .catch((e) => setError('Server error')) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <div>Subsidize this market by adding liquidity for traders.</div> | ||||||
|  | 
 | ||||||
|  |       <Row> | ||||||
|  |         <AmountInput | ||||||
|  |           amount={amount} | ||||||
|  |           onChange={onAmountChange} | ||||||
|  |           label="M$" | ||||||
|  |           error={error} | ||||||
|  |           disabled={isLoading} | ||||||
|  |         /> | ||||||
|  |         <button | ||||||
|  |           className={clsx('btn btn-primary ml-2', isLoading && 'btn-disabled')} | ||||||
|  |           onClick={submit} | ||||||
|  |           disabled={isLoading} | ||||||
|  |         > | ||||||
|  |           Add | ||||||
|  |         </button> | ||||||
|  |       </Row> | ||||||
|  | 
 | ||||||
|  |       {isSuccess && amount && ( | ||||||
|  |         <div>Success! Added {formatMoney(amount)} in liquidity.</div> | ||||||
|  |       )} | ||||||
|  | 
 | ||||||
|  |       {isLoading && <div>Processing...</div>} | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | @ -23,7 +23,10 @@ export default function BetRow(props: { | ||||||
|   ) |   ) | ||||||
|   const user = useUser() |   const user = useUser() | ||||||
|   const userBets = useUserContractBets(user?.id, contract.id) |   const userBets = useUserContractBets(user?.id, contract.id) | ||||||
|   const { yesFloorShares, noFloorShares } = useSaveShares(contract, userBets) |   const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( | ||||||
|  |     contract, | ||||||
|  |     userBets | ||||||
|  |   ) | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|  | @ -39,13 +42,23 @@ export default function BetRow(props: { | ||||||
|               setBetChoice(choice) |               setBetChoice(choice) | ||||||
|             }} |             }} | ||||||
|             replaceNoButton={ |             replaceNoButton={ | ||||||
|               yesFloorShares > noFloorShares && yesFloorShares > 0 ? ( |               yesFloorShares > 0 ? ( | ||||||
|                 <SellButton contract={contract} user={user} /> |                 <SellButton | ||||||
|  |                   contract={contract} | ||||||
|  |                   user={user} | ||||||
|  |                   sharesOutcome={'YES'} | ||||||
|  |                   shares={yesShares} | ||||||
|  |                 /> | ||||||
|               ) : undefined |               ) : undefined | ||||||
|             } |             } | ||||||
|             replaceYesButton={ |             replaceYesButton={ | ||||||
|               noFloorShares > yesFloorShares && noFloorShares > 0 ? ( |               noFloorShares > 0 ? ( | ||||||
|                 <SellButton contract={contract} user={user} /> |                 <SellButton | ||||||
|  |                   contract={contract} | ||||||
|  |                   user={user} | ||||||
|  |                   sharesOutcome={'NO'} | ||||||
|  |                   shares={noShares} | ||||||
|  |                 /> | ||||||
|               ) : undefined |               ) : undefined | ||||||
|             } |             } | ||||||
|           /> |           /> | ||||||
|  |  | ||||||
|  | @ -113,6 +113,13 @@ export function BetsList(props: { user: User }) { | ||||||
|   const displayedContracts = _.sortBy(contracts, SORTS[sort]) |   const displayedContracts = _.sortBy(contracts, SORTS[sort]) | ||||||
|     .reverse() |     .reverse() | ||||||
|     .filter(FILTERS[filter]) |     .filter(FILTERS[filter]) | ||||||
|  |     .filter((c) => { | ||||||
|  |       if (sort === 'profit') return true | ||||||
|  | 
 | ||||||
|  |       // Filter out contracts where you don't have shares anymore.
 | ||||||
|  |       const metrics = contractsMetrics[c.id] | ||||||
|  |       return metrics.payout > 0 | ||||||
|  |     }) | ||||||
| 
 | 
 | ||||||
|   const [settled, unsettled] = _.partition( |   const [settled, unsettled] = _.partition( | ||||||
|     contracts, |     contracts, | ||||||
|  | @ -206,7 +213,7 @@ const NoBets = () => { | ||||||
|   return ( |   return ( | ||||||
|     <div className="mx-4 text-gray-500"> |     <div className="mx-4 text-gray-500"> | ||||||
|       You have not made any bets yet.{' '} |       You have not made any bets yet.{' '} | ||||||
|       <SiteLink href="/" className="underline"> |       <SiteLink href="/home" className="underline"> | ||||||
|         Find a prediction market! |         Find a prediction market! | ||||||
|       </SiteLink> |       </SiteLink> | ||||||
|     </div> |     </div> | ||||||
|  | @ -226,11 +233,10 @@ function MyContractBets(props: { | ||||||
|   const isBinary = outcomeType === 'BINARY' |   const isBinary = outcomeType === 'BINARY' | ||||||
|   const probPercent = getBinaryProbPercent(contract) |   const probPercent = getBinaryProbPercent(contract) | ||||||
| 
 | 
 | ||||||
|   const { payout, profit, profitPercent } = getContractBetMetrics( |   const { payout, profit, profitPercent, invested } = getContractBetMetrics( | ||||||
|     contract, |     contract, | ||||||
|     bets |     bets | ||||||
|   ) |   ) | ||||||
| 
 |  | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
|       tabIndex={0} |       tabIndex={0} | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import { | ||||||
|   contractPath, |   contractPath, | ||||||
|   getBinaryProbPercent, |   getBinaryProbPercent, | ||||||
| } from '../../lib/firebase/contracts' | } from '../../lib/firebase/contracts' | ||||||
|  | import { AddLiquidityPanel } from '../add-liquidity-panel' | ||||||
| import { CopyLinkButton } from '../copy-link-button' | import { CopyLinkButton } from '../copy-link-button' | ||||||
| import { Col } from '../layout/col' | import { Col } from '../layout/col' | ||||||
| import { Modal } from '../layout/modal' | import { Modal } from '../layout/modal' | ||||||
|  | @ -110,8 +111,16 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { | ||||||
| 
 | 
 | ||||||
|           <div className="text-gray-500">Tags</div> |           <div className="text-gray-500">Tags</div> | ||||||
|           <TagsInput contract={contract} /> |           <TagsInput contract={contract} /> | ||||||
| 
 |  | ||||||
|           <div /> |           <div /> | ||||||
|  | 
 | ||||||
|  |           {contract.mechanism === 'cpmm-1' && | ||||||
|  |             !contract.resolution && | ||||||
|  |             (!closeTime || closeTime > Date.now()) && ( | ||||||
|  |               <> | ||||||
|  |                 <div className="text-gray-500">Add liquidity</div> | ||||||
|  |                 <AddLiquidityPanel contract={contract} /> | ||||||
|  |               </> | ||||||
|  |             )} | ||||||
|         </Col> |         </Col> | ||||||
|       </Modal> |       </Modal> | ||||||
|     </> |     </> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { SparklesIcon, XIcon } from '@heroicons/react/solid' | import { SparklesIcon, XIcon } from '@heroicons/react/solid' | ||||||
| import { Avatar } from './avatar' | import { Avatar } from './avatar' | ||||||
| import { useRef, useState } from 'react' | import { useEffect, useRef, useState } from 'react' | ||||||
| import { Spacer } from './layout/spacer' | import { Spacer } from './layout/spacer' | ||||||
| import { NewContract } from '../pages/create' | import { NewContract } from '../pages/create' | ||||||
| import { firebaseLogin, User } from '../lib/firebase/users' | import { firebaseLogin, User } from '../lib/firebase/users' | ||||||
|  | @ -10,6 +10,7 @@ import { Col } from './layout/col' | ||||||
| import clsx from 'clsx' | import clsx from 'clsx' | ||||||
| import { Row } from './layout/row' | import { Row } from './layout/row' | ||||||
| import { ENV_CONFIG } from '../../common/envs/constants' | import { ENV_CONFIG } from '../../common/envs/constants' | ||||||
|  | import _ from 'lodash' | ||||||
| 
 | 
 | ||||||
| export function FeedPromo(props: { hotContracts: Contract[] }) { | export function FeedPromo(props: { hotContracts: Contract[] }) { | ||||||
|   const { hotContracts } = props |   const { hotContracts } = props | ||||||
|  | @ -71,14 +72,19 @@ export default function FeedCreate(props: { | ||||||
|   const [isExpanded, setIsExpanded] = useState(false) |   const [isExpanded, setIsExpanded] = useState(false) | ||||||
|   const inputRef = useRef<HTMLTextAreaElement | null>() |   const inputRef = useRef<HTMLTextAreaElement | null>() | ||||||
| 
 | 
 | ||||||
|   const placeholders = ENV_CONFIG.newQuestionPlaceholders |  | ||||||
|   // Rotate through a new placeholder each day
 |   // Rotate through a new placeholder each day
 | ||||||
|   // Easter egg idea: click your own name to shuffle the placeholder
 |   // Easter egg idea: click your own name to shuffle the placeholder
 | ||||||
|   // const daysSinceEpoch = Math.floor(Date.now() / 1000 / 60 / 60 / 24)
 |   // const daysSinceEpoch = Math.floor(Date.now() / 1000 / 60 / 60 / 24)
 | ||||||
|   const [randIndex] = useState( | 
 | ||||||
|     Math.floor(Math.random() * 1e10) % placeholders.length |   // Take care not to produce a different placeholder on the server and client
 | ||||||
|  |   const [defaultPlaceholder, setDefaultPlaceholder] = useState('') | ||||||
|  |   useEffect(() => { | ||||||
|  |     setDefaultPlaceholder( | ||||||
|  |       `e.g. ${_.sample(ENV_CONFIG.newQuestionPlaceholders)}` | ||||||
|     ) |     ) | ||||||
|   const placeholder = props.placeholder ?? `e.g. ${placeholders[randIndex]}` |   }, []) | ||||||
|  | 
 | ||||||
|  |   const placeholder = props.placeholder ?? defaultPlaceholder | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
|  |  | ||||||
|  | @ -22,12 +22,19 @@ export type ActivityItem = | ||||||
|   | AnswerGroupItem |   | AnswerGroupItem | ||||||
|   | CloseItem |   | CloseItem | ||||||
|   | ResolveItem |   | ResolveItem | ||||||
|  |   | CommentInputItem | ||||||
| 
 | 
 | ||||||
| type BaseActivityItem = { | type BaseActivityItem = { | ||||||
|   id: string |   id: string | ||||||
|   contract: Contract |   contract: Contract | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export type CommentInputItem = BaseActivityItem & { | ||||||
|  |   type: 'commentInput' | ||||||
|  |   bets: Bet[] | ||||||
|  |   commentsByBetId: Record<string, Comment> | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export type DescriptionItem = BaseActivityItem & { | export type DescriptionItem = BaseActivityItem & { | ||||||
|   type: 'description' |   type: 'description' | ||||||
| } | } | ||||||
|  | @ -48,7 +55,7 @@ export type BetItem = BaseActivityItem & { | ||||||
| export type CommentItem = BaseActivityItem & { | export type CommentItem = BaseActivityItem & { | ||||||
|   type: 'comment' |   type: 'comment' | ||||||
|   comment: Comment |   comment: Comment | ||||||
|   bet: Bet |   bet: Bet | undefined | ||||||
|   hideOutcome: boolean |   hideOutcome: boolean | ||||||
|   truncate: boolean |   truncate: boolean | ||||||
|   smallAvatar: boolean |   smallAvatar: boolean | ||||||
|  | @ -249,6 +256,47 @@ function getAnswerGroups( | ||||||
|   return answerGroups |   return answerGroups | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function groupBetsAndComments( | ||||||
|  |   bets: Bet[], | ||||||
|  |   comments: Comment[], | ||||||
|  |   contract: Contract, | ||||||
|  |   userId: string | undefined, | ||||||
|  |   options: { | ||||||
|  |     hideOutcome: boolean | ||||||
|  |     abbreviated: boolean | ||||||
|  |     smallAvatar: boolean | ||||||
|  |     reversed: boolean | ||||||
|  |   } | ||||||
|  | ) { | ||||||
|  |   const commentsWithoutBets = comments | ||||||
|  |     .filter((comment) => !comment.betId) | ||||||
|  |     .map((comment) => ({ | ||||||
|  |       type: 'comment' as const, | ||||||
|  |       id: comment.id, | ||||||
|  |       contract: contract, | ||||||
|  |       comment, | ||||||
|  |       bet: undefined, | ||||||
|  |       truncate: false, | ||||||
|  |       hideOutcome: true, | ||||||
|  |       smallAvatar: false, | ||||||
|  |     })) | ||||||
|  | 
 | ||||||
|  |   const groupedBets = groupBets(bets, comments, contract, userId, options) | ||||||
|  | 
 | ||||||
|  |   // iterate through the bets and comment activity items and add them to the items in order of comment creation time:
 | ||||||
|  |   const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets] | ||||||
|  |   const sortedBetsAndComments = _.sortBy(unorderedBetsAndComments, (item) => { | ||||||
|  |     if (item.type === 'comment') { | ||||||
|  |       return item.comment.createdTime | ||||||
|  |     } else if (item.type === 'bet') { | ||||||
|  |       return item.bet.createdTime | ||||||
|  |     } else if (item.type === 'betgroup') { | ||||||
|  |       return item.bets[0].createdTime | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |   return sortedBetsAndComments | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function getAllContractActivityItems( | export function getAllContractActivityItems( | ||||||
|   contract: Contract, |   contract: Contract, | ||||||
|   bets: Bet[], |   bets: Bet[], | ||||||
|  | @ -279,9 +327,9 @@ export function getAllContractActivityItems( | ||||||
|       ] |       ] | ||||||
|     : [{ type: 'description', id: '0', contract }] |     : [{ type: 'description', id: '0', contract }] | ||||||
| 
 | 
 | ||||||
|  |   if (outcomeType === 'FREE_RESPONSE') { | ||||||
|     items.push( |     items.push( | ||||||
|     ...(outcomeType === 'FREE_RESPONSE' |       ...getAnswerGroups( | ||||||
|       ? getAnswerGroups( |  | ||||||
|         contract as FullContract<DPM, FreeResponse>, |         contract as FullContract<DPM, FreeResponse>, | ||||||
|         bets, |         bets, | ||||||
|         comments, |         comments, | ||||||
|  | @ -292,13 +340,17 @@ export function getAllContractActivityItems( | ||||||
|           reversed, |           reversed, | ||||||
|         } |         } | ||||||
|       ) |       ) | ||||||
|       : groupBets(bets, comments, contract, user?.id, { |     ) | ||||||
|  |   } else { | ||||||
|  |     items.push( | ||||||
|  |       ...groupBetsAndComments(bets, comments, contract, user?.id, { | ||||||
|         hideOutcome: false, |         hideOutcome: false, | ||||||
|         abbreviated, |         abbreviated, | ||||||
|         smallAvatar: false, |         smallAvatar: false, | ||||||
|           reversed: false, |         reversed, | ||||||
|         })) |       }) | ||||||
|     ) |     ) | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   if (contract.closeTime && contract.closeTime <= Date.now()) { |   if (contract.closeTime && contract.closeTime <= Date.now()) { | ||||||
|     items.push({ type: 'close', id: `${contract.closeTime}`, contract }) |     items.push({ type: 'close', id: `${contract.closeTime}`, contract }) | ||||||
|  | @ -307,6 +359,15 @@ export function getAllContractActivityItems( | ||||||
|     items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract }) |     items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   const commentsByBetId = mapCommentsByBetId(comments) | ||||||
|  |   items.push({ | ||||||
|  |     type: 'commentInput', | ||||||
|  |     id: 'commentInput', | ||||||
|  |     bets, | ||||||
|  |     commentsByBetId, | ||||||
|  |     contract, | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|   if (reversed) items.reverse() |   if (reversed) items.reverse() | ||||||
| 
 | 
 | ||||||
|   return items |   return items | ||||||
|  | @ -348,7 +409,7 @@ export function getRecentContractActivityItems( | ||||||
|             reversed: false, |             reversed: false, | ||||||
|           } |           } | ||||||
|         ) |         ) | ||||||
|       : groupBets(bets, comments, contract, user?.id, { |       : groupBetsAndComments(bets, comments, contract, user?.id, { | ||||||
|           hideOutcome: false, |           hideOutcome: false, | ||||||
|           abbreviated: true, |           abbreviated: true, | ||||||
|           smallAvatar: false, |           smallAvatar: false, | ||||||
|  |  | ||||||
|  | @ -47,6 +47,7 @@ import { useSaveSeenContract } from '../../hooks/use-seen-contracts' | ||||||
| import { User } from '../../../common/user' | import { User } from '../../../common/user' | ||||||
| import { Modal } from '../layout/modal' | import { Modal } from '../layout/modal' | ||||||
| import { trackClick } from '../../lib/firebase/tracking' | import { trackClick } from '../../lib/firebase/tracking' | ||||||
|  | import { firebaseLogin } from '../../lib/firebase/users' | ||||||
| import { DAY_MS } from '../../../common/util/time' | import { DAY_MS } from '../../../common/util/time' | ||||||
| import NewContractBadge from '../new-contract-badge' | import NewContractBadge from '../new-contract-badge' | ||||||
| 
 | 
 | ||||||
|  | @ -107,24 +108,30 @@ function FeedItem(props: { item: ActivityItem }) { | ||||||
|       return <FeedClose {...item} /> |       return <FeedClose {...item} /> | ||||||
|     case 'resolve': |     case 'resolve': | ||||||
|       return <FeedResolve {...item} /> |       return <FeedResolve {...item} /> | ||||||
|  |     case 'commentInput': | ||||||
|  |       return <CommentInput {...item} /> | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function FeedComment(props: { | export function FeedComment(props: { | ||||||
|   contract: Contract |   contract: Contract | ||||||
|   comment: Comment |   comment: Comment | ||||||
|   bet: Bet |   bet: Bet | undefined | ||||||
|   hideOutcome: boolean |   hideOutcome: boolean | ||||||
|   truncate: boolean |   truncate: boolean | ||||||
|   smallAvatar: boolean |   smallAvatar: boolean | ||||||
| }) { | }) { | ||||||
|   const { contract, comment, bet, hideOutcome, truncate, smallAvatar } = props |   const { contract, comment, bet, hideOutcome, truncate, smallAvatar } = props | ||||||
|   const { amount, outcome } = bet |   let money: string | undefined | ||||||
|  |   let outcome: string | undefined | ||||||
|  |   let bought: string | undefined | ||||||
|  |   if (bet) { | ||||||
|  |     outcome = bet.outcome | ||||||
|  |     bought = bet.amount >= 0 ? 'bought' : 'sold' | ||||||
|  |     money = formatMoney(Math.abs(bet.amount)) | ||||||
|  |   } | ||||||
|   const { text, userUsername, userName, userAvatarUrl, createdTime } = comment |   const { text, userUsername, userName, userAvatarUrl, createdTime } = comment | ||||||
| 
 | 
 | ||||||
|   const bought = amount >= 0 ? 'bought' : 'sold' |  | ||||||
|   const money = formatMoney(Math.abs(amount)) |  | ||||||
| 
 |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Avatar |       <Avatar | ||||||
|  | @ -147,7 +154,7 @@ export function FeedComment(props: { | ||||||
|                 {' '} |                 {' '} | ||||||
|                 of{' '} |                 of{' '} | ||||||
|                 <OutcomeLabel |                 <OutcomeLabel | ||||||
|                   outcome={outcome} |                   outcome={outcome ? outcome : ''} | ||||||
|                   contract={contract} |                   contract={contract} | ||||||
|                   truncate="short" |                   truncate="short" | ||||||
|                 /> |                 /> | ||||||
|  | @ -177,6 +184,78 @@ function RelativeTimestamp(props: { time: number }) { | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function CommentInput(props: { | ||||||
|  |   contract: Contract | ||||||
|  |   commentsByBetId: Record<string, Comment> | ||||||
|  |   bets: Bet[] | ||||||
|  | }) { | ||||||
|  |   // see if we can comment input on any bet:
 | ||||||
|  |   const { contract, bets, commentsByBetId } = props | ||||||
|  |   const { outcomeType } = contract | ||||||
|  |   const user = useUser() | ||||||
|  |   const [comment, setComment] = useState('') | ||||||
|  | 
 | ||||||
|  |   if (outcomeType === 'FREE_RESPONSE') { | ||||||
|  |     return <div /> | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let canCommentOnABet = false | ||||||
|  |   bets.some((bet) => { | ||||||
|  |     // make sure there is not already a comment with a matching bet id:
 | ||||||
|  |     const matchingComment = commentsByBetId[bet.id] | ||||||
|  |     if (matchingComment) { | ||||||
|  |       return false | ||||||
|  |     } | ||||||
|  |     const { createdTime, userId } = bet | ||||||
|  |     canCommentOnABet = canCommentOnBet(userId, createdTime, user) | ||||||
|  |     return canCommentOnABet | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   if (canCommentOnABet) return <div /> | ||||||
|  | 
 | ||||||
|  |   async function submitComment() { | ||||||
|  |     if (!comment) return | ||||||
|  |     if (!user) { | ||||||
|  |       return await firebaseLogin() | ||||||
|  |     } | ||||||
|  |     await createComment(contract.id, comment, user) | ||||||
|  |     setComment('') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <div> | ||||||
|  |         <Avatar avatarUrl={user?.avatarUrl} username={user?.username} /> | ||||||
|  |       </div> | ||||||
|  |       <div className={'min-w-0 flex-1 py-1.5'}> | ||||||
|  |         <div className="text-sm text-gray-500"> | ||||||
|  |           <div className="mt-2"> | ||||||
|  |             <Textarea | ||||||
|  |               value={comment} | ||||||
|  |               onChange={(e) => setComment(e.target.value)} | ||||||
|  |               className="textarea textarea-bordered w-full resize-none" | ||||||
|  |               placeholder="Add a comment..." | ||||||
|  |               rows={3} | ||||||
|  |               maxLength={MAX_COMMENT_LENGTH} | ||||||
|  |               onKeyDown={(e) => { | ||||||
|  |                 if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { | ||||||
|  |                   submitComment() | ||||||
|  |                 } | ||||||
|  |               }} | ||||||
|  |             /> | ||||||
|  |             <button | ||||||
|  |               className="btn btn-outline btn-sm mt-1" | ||||||
|  |               onClick={submitComment} | ||||||
|  |             > | ||||||
|  |               Comment | ||||||
|  |             </button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function FeedBet(props: { | export function FeedBet(props: { | ||||||
|   contract: Contract |   contract: Contract | ||||||
|   bet: Bet |   bet: Bet | ||||||
|  | @ -188,14 +267,12 @@ export function FeedBet(props: { | ||||||
|   const { id, amount, outcome, createdTime, userId } = bet |   const { id, amount, outcome, createdTime, userId } = bet | ||||||
|   const user = useUser() |   const user = useUser() | ||||||
|   const isSelf = user?.id === userId |   const isSelf = user?.id === userId | ||||||
| 
 |   const canComment = canCommentOnBet(userId, createdTime, user) | ||||||
|   // You can comment if your bet was posted in the last hour
 |  | ||||||
|   const canComment = isSelf && Date.now() - createdTime < 60 * 60 * 1000 |  | ||||||
| 
 | 
 | ||||||
|   const [comment, setComment] = useState('') |   const [comment, setComment] = useState('') | ||||||
|   async function submitComment() { |   async function submitComment() { | ||||||
|     if (!user || !comment || !canComment) return |     if (!user || !comment || !canComment) return | ||||||
|     await createComment(contract.id, id, comment, user) |     await createComment(contract.id, comment, user, id) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const bought = amount >= 0 ? 'bought' : 'sold' |   const bought = amount >= 0 ? 'bought' : 'sold' | ||||||
|  | @ -378,6 +455,16 @@ export function FeedQuestion(props: { | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function canCommentOnBet( | ||||||
|  |   userId: string, | ||||||
|  |   createdTime: number, | ||||||
|  |   user?: User | null | ||||||
|  | ) { | ||||||
|  |   const isSelf = user?.id === userId | ||||||
|  |   // You can comment if your bet was posted in the last hour
 | ||||||
|  |   return isSelf && Date.now() - createdTime < 60 * 60 * 1000 | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function FeedDescription(props: { contract: Contract }) { | function FeedDescription(props: { contract: Contract }) { | ||||||
|   const { contract } = props |   const { contract } = props | ||||||
|   const { creatorName, creatorUsername } = contract |   const { creatorName, creatorUsername } = contract | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ import { Binary, CPMM, DPM, FullContract } from '../../common/contract' | ||||||
| import { User } from '../../common/user' | import { User } from '../../common/user' | ||||||
| import { useUserContractBets } from '../hooks/use-user-bets' | import { useUserContractBets } from '../hooks/use-user-bets' | ||||||
| import { useState } from 'react' | import { useState } from 'react' | ||||||
| import { useSaveShares } from './use-save-shares' |  | ||||||
| import { Col } from './layout/col' | import { Col } from './layout/col' | ||||||
| import clsx from 'clsx' | import clsx from 'clsx' | ||||||
| import { SellSharesModal } from './sell-modal' | import { SellSharesModal } from './sell-modal' | ||||||
|  | @ -10,23 +9,13 @@ import { SellSharesModal } from './sell-modal' | ||||||
| export function SellButton(props: { | export function SellButton(props: { | ||||||
|   contract: FullContract<DPM | CPMM, Binary> |   contract: FullContract<DPM | CPMM, Binary> | ||||||
|   user: User | null | undefined |   user: User | null | undefined | ||||||
|  |   sharesOutcome: 'YES' | 'NO' | undefined | ||||||
|  |   shares: number | ||||||
| }) { | }) { | ||||||
|   const { contract, user } = props |   const { contract, user, sharesOutcome, shares } = props | ||||||
| 
 |  | ||||||
|   const userBets = useUserContractBets(user?.id, contract.id) |   const userBets = useUserContractBets(user?.id, contract.id) | ||||||
|   const [showSellModal, setShowSellModal] = useState(false) |   const [showSellModal, setShowSellModal] = useState(false) | ||||||
| 
 |  | ||||||
|   const { mechanism } = contract |   const { mechanism } = contract | ||||||
|   const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares( |  | ||||||
|     contract, |  | ||||||
|     userBets |  | ||||||
|   ) |  | ||||||
|   const floorShares = yesFloorShares || noFloorShares |  | ||||||
|   const sharesOutcome = yesFloorShares |  | ||||||
|     ? 'YES' |  | ||||||
|     : noFloorShares |  | ||||||
|     ? 'NO' |  | ||||||
|     : undefined |  | ||||||
| 
 | 
 | ||||||
|   if (sharesOutcome && user && mechanism === 'cpmm-1') { |   if (sharesOutcome && user && mechanism === 'cpmm-1') { | ||||||
|     return ( |     return ( | ||||||
|  | @ -45,14 +34,14 @@ export function SellButton(props: { | ||||||
|           {'Sell ' + sharesOutcome} |           {'Sell ' + sharesOutcome} | ||||||
|         </button> |         </button> | ||||||
|         <div className={'mt-1 w-24 text-center text-sm text-gray-500'}> |         <div className={'mt-1 w-24 text-center text-sm text-gray-500'}> | ||||||
|           {'(' + floorShares + ' shares)'} |           {'(' + Math.floor(shares) + ' shares)'} | ||||||
|         </div> |         </div> | ||||||
|         {showSellModal && ( |         {showSellModal && ( | ||||||
|           <SellSharesModal |           <SellSharesModal | ||||||
|             contract={contract as FullContract<CPMM, Binary>} |             contract={contract as FullContract<CPMM, Binary>} | ||||||
|             user={user} |             user={user} | ||||||
|             userBets={userBets ?? []} |             userBets={userBets ?? []} | ||||||
|             shares={yesShares || noShares} |             shares={shares} | ||||||
|             sharesOutcome={sharesOutcome} |             sharesOutcome={sharesOutcome} | ||||||
|             setOpen={setShowSellModal} |             setOpen={setShowSellModal} | ||||||
|           /> |           /> | ||||||
|  |  | ||||||
|  | @ -1,16 +1,9 @@ | ||||||
| import { | import { httpsCallable } from 'firebase/functions' | ||||||
|   getFunctions, |  | ||||||
|   httpsCallable, |  | ||||||
|   connectFunctionsEmulator, |  | ||||||
| } from 'firebase/functions' |  | ||||||
| import { Fold } from '../../../common/fold' | import { Fold } from '../../../common/fold' | ||||||
| import { User } from '../../../common/user' | import { User } from '../../../common/user' | ||||||
| import { randomString } from '../../../common/util/random' | import { randomString } from '../../../common/util/random' | ||||||
| import './init' | import './init' | ||||||
| 
 | import { functions } from './init' | ||||||
| const functions = getFunctions() |  | ||||||
| // Uncomment to connect to local emulators:
 |  | ||||||
| // connectFunctionsEmulator(functions, 'localhost', 5001)
 |  | ||||||
| 
 | 
 | ||||||
| export const cloudFunction = <RequestData, ResponseData>(name: string) => | export const cloudFunction = <RequestData, ResponseData>(name: string) => | ||||||
|   httpsCallable<RequestData, ResponseData>(functions, name) |   httpsCallable<RequestData, ResponseData>(functions, name) | ||||||
|  | @ -74,3 +67,9 @@ export const changeUserInfo = (data: { | ||||||
|     .then((r) => r.data as { status: string; message?: string }) |     .then((r) => r.data as { status: string; message?: string }) | ||||||
|     .catch((e) => ({ status: 'error', message: e.message })) |     .catch((e) => ({ status: 'error', message: e.message })) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export const addLiquidity = (data: { amount: number; contractId: string }) => { | ||||||
|  |   return cloudFunction('addLiquidity')(data) | ||||||
|  |     .then((r) => r.data as { status: string }) | ||||||
|  |     .catch((e) => ({ status: 'error', message: e.message })) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -19,16 +19,16 @@ export const MAX_COMMENT_LENGTH = 10000 | ||||||
| 
 | 
 | ||||||
| export async function createComment( | export async function createComment( | ||||||
|   contractId: string, |   contractId: string, | ||||||
|   betId: string, |  | ||||||
|   text: string, |   text: string, | ||||||
|   commenter: User |   commenter: User, | ||||||
|  |   betId?: string | ||||||
| ) { | ) { | ||||||
|   const ref = doc(getCommentsCollection(contractId), betId) |   const ref = betId | ||||||
| 
 |     ? doc(getCommentsCollection(contractId), betId) | ||||||
|  |     : doc(getCommentsCollection(contractId)) | ||||||
|   const comment: Comment = { |   const comment: Comment = { | ||||||
|     id: ref.id, |     id: ref.id, | ||||||
|     contractId, |     contractId, | ||||||
|     betId, |  | ||||||
|     userId: commenter.id, |     userId: commenter.id, | ||||||
|     text: text.slice(0, MAX_COMMENT_LENGTH), |     text: text.slice(0, MAX_COMMENT_LENGTH), | ||||||
|     createdTime: Date.now(), |     createdTime: Date.now(), | ||||||
|  | @ -36,7 +36,9 @@ export async function createComment( | ||||||
|     userUsername: commenter.username, |     userUsername: commenter.username, | ||||||
|     userAvatarUrl: commenter.avatarUrl, |     userAvatarUrl: commenter.avatarUrl, | ||||||
|   } |   } | ||||||
| 
 |   if (betId) { | ||||||
|  |     comment.betId = betId | ||||||
|  |   } | ||||||
|   return await setDoc(ref, comment) |   return await setDoc(ref, comment) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -67,8 +69,10 @@ export function listenForComments( | ||||||
| export function mapCommentsByBetId(comments: Comment[]) { | export function mapCommentsByBetId(comments: Comment[]) { | ||||||
|   const map: Record<string, Comment> = {} |   const map: Record<string, Comment> = {} | ||||||
|   for (const comment of comments) { |   for (const comment of comments) { | ||||||
|  |     if (comment.betId) { | ||||||
|       map[comment.betId] = comment |       map[comment.betId] = comment | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|   return map |   return map | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,26 @@ | ||||||
| import { getFirestore } from '@firebase/firestore' | import { getFirestore } from '@firebase/firestore' | ||||||
| import { initializeApp, getApps, getApp } from 'firebase/app' | import { initializeApp, getApps, getApp } from 'firebase/app' | ||||||
| import { FIREBASE_CONFIG } from '../../../common/envs/constants' | import { FIREBASE_CONFIG } from '../../../common/envs/constants' | ||||||
|  | import { connectFirestoreEmulator } from 'firebase/firestore' | ||||||
|  | import { connectFunctionsEmulator, getFunctions } from 'firebase/functions' | ||||||
| 
 | 
 | ||||||
| // Initialize Firebase
 | // Initialize Firebase
 | ||||||
| export const app = getApps().length ? getApp() : initializeApp(FIREBASE_CONFIG) | export const app = getApps().length ? getApp() : initializeApp(FIREBASE_CONFIG) | ||||||
|  | export const db = getFirestore() | ||||||
|  | export const functions = getFunctions() | ||||||
| 
 | 
 | ||||||
| export const db = getFirestore(app) | const EMULATORS_STARTED = 'EMULATORS_STARTED' | ||||||
|  | function startEmulators() { | ||||||
|  |   // I don't like this but this is the only way to reconnect to the emulators without error, see: https://stackoverflow.com/questions/65066963/firebase-firestore-emulator-error-host-has-been-set-in-both-settings-and-usee
 | ||||||
|  |   // @ts-ignore
 | ||||||
|  |   if (!global[EMULATORS_STARTED]) { | ||||||
|  |     // @ts-ignore
 | ||||||
|  |     global[EMULATORS_STARTED] = true | ||||||
|  |     connectFirestoreEmulator(db, 'localhost', 8080) | ||||||
|  |     connectFunctionsEmulator(functions, 'localhost', 5001) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { | ||||||
|  |   startEmulators() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ | ||||||
|     "devdev": "NEXT_PUBLIC_FIREBASE_ENV=DEV concurrently -n NEXT,TS -c magenta,cyan \"FIREBASE_ENV=DEV next dev -p 3000\" \"FIREBASE_ENV=DEV yarn ts --watch\" # see https://github.com/vercel/next.js/discussions/33634", |     "devdev": "NEXT_PUBLIC_FIREBASE_ENV=DEV concurrently -n NEXT,TS -c magenta,cyan \"FIREBASE_ENV=DEV next dev -p 3000\" \"FIREBASE_ENV=DEV yarn ts --watch\" # see https://github.com/vercel/next.js/discussions/33634", | ||||||
|     "dev:dev": "yarn devdev", |     "dev:dev": "yarn devdev", | ||||||
|     "dev:the": "NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"FIREBASE_ENV=THEOREMONE yarn ts --watch\"", |     "dev:the": "NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"FIREBASE_ENV=THEOREMONE yarn ts --watch\"", | ||||||
|  |     "dev:emulate": "NEXT_PUBLIC_FIREBASE_EMULATE=TRUE yarn devdev", | ||||||
|     "ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty", |     "ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty", | ||||||
|     "build": "next build", |     "build": "next build", | ||||||
|     "start": "next start", |     "start": "next start", | ||||||
|  | @ -53,7 +54,7 @@ | ||||||
|     "pretty-quick": "3.1.2", |     "pretty-quick": "3.1.2", | ||||||
|     "tailwindcss": "3.0.1", |     "tailwindcss": "3.0.1", | ||||||
|     "tsc-files": "1.1.3", |     "tsc-files": "1.1.3", | ||||||
|     "typescript": "4.5.2" |     "typescript": "4.5.3" | ||||||
|   }, |   }, | ||||||
|   "lint-staged": { |   "lint-staged": { | ||||||
|     "*.{ts,tsx}": "tsc-files --noEmit --incremental false" |     "*.{ts,tsx}": "tsc-files --noEmit --incremental false" | ||||||
|  |  | ||||||
|  | @ -250,7 +250,10 @@ function ContractTopTrades(props: { | ||||||
|   const topBettor = useUserById(betsById[topBetId]?.userId) |   const topBettor = useUserById(betsById[topBetId]?.userId) | ||||||
| 
 | 
 | ||||||
|   // And also the commentId of the comment with the highest profit
 |   // And also the commentId of the comment with the highest profit
 | ||||||
|   const topCommentId = _.sortBy(comments, (c) => -profitById[c.betId])[0]?.id |   const topCommentId = _.sortBy( | ||||||
|  |     comments, | ||||||
|  |     (c) => c.betId && -profitById[c.betId] | ||||||
|  |   )[0]?.id | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="mt-12 max-w-sm"> |     <div className="mt-12 max-w-sm"> | ||||||
|  |  | ||||||
|  | @ -15,6 +15,10 @@ | ||||||
|     "jsx": "preserve", |     "jsx": "preserve", | ||||||
|     "incremental": true |     "incremental": true | ||||||
|   }, |   }, | ||||||
|  | 
 | ||||||
|  |   "watchOptions": { | ||||||
|  |     "excludeDirectories": [".next"] | ||||||
|  |   }, | ||||||
|   "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../common/**/*.ts"], |   "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../common/**/*.ts"], | ||||||
|   "exclude": ["node_modules", ".next"] |   "exclude": ["node_modules"] | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										4905
									
								
								web/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										4905
									
								
								web/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -5198,11 +5198,6 @@ typedarray-to-buffer@^3.1.5: | ||||||
|   dependencies: |   dependencies: | ||||||
|     is-typedarray "^1.0.0" |     is-typedarray "^1.0.0" | ||||||
| 
 | 
 | ||||||
| typescript@4.5.2: |  | ||||||
|   version "4.5.2" |  | ||||||
|   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998" |  | ||||||
|   integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw== |  | ||||||
| 
 |  | ||||||
| typescript@4.5.3: | typescript@4.5.3: | ||||||
|   version "4.5.3" |   version "4.5.3" | ||||||
|   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.3.tgz#afaa858e68c7103317d89eb90c5d8906268d353c" |   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.3.tgz#afaa858e68c7103317d89eb90c5d8906268d353c" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user