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