Sell shares panel (#69)
* Split BuyAmountInput out of AmountInput * Buy and sell tabs. Compute some sell info * In progress * BuyPanel & SellPanel with banner above that shows current shares and toggle button * Remove "Remaining balance" * Bring back 'Place a trade'. Tweaks * Sell shares cloud function. * Sell all shares by default. Switch back to buy if sell all your shares. * Cache your shares in local storage so sell banner doesn't flicker. * Compute sale value of shares with binary search to keep k constant. * Update bets table to show BUY or SELL * Fixes from Stephen's review * Don't allow selling more than max shares in cloud function * Use modal for sell shares on desktop. * Handle floating point precision in max shares you can sell.
This commit is contained in:
parent
a40d593d32
commit
ed5f69db7a
|
@ -114,28 +114,60 @@ export function calculateCpmmPurchase(
|
||||||
return { shares, newPool, newP, fees }
|
return { shares, newPool, newP, fees }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateCpmmShareValue(
|
function computeK(y: number, n: number, p: number) {
|
||||||
|
return y ** p * n ** (1 - p)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sellSharesK(
|
||||||
|
y: number,
|
||||||
|
n: number,
|
||||||
|
p: number,
|
||||||
|
s: number,
|
||||||
|
outcome: 'YES' | 'NO',
|
||||||
|
b: number
|
||||||
|
) {
|
||||||
|
return outcome === 'YES'
|
||||||
|
? computeK(y - b + s, n - b, p)
|
||||||
|
: computeK(y - b, n - b + s, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCpmmShareValue(
|
||||||
contract: FullContract<CPMM, Binary>,
|
contract: FullContract<CPMM, Binary>,
|
||||||
shares: number,
|
shares: number,
|
||||||
outcome: string
|
outcome: 'YES' | 'NO'
|
||||||
) {
|
) {
|
||||||
const { pool } = contract
|
const { pool, p } = contract
|
||||||
const { YES: y, NO: n } = pool
|
|
||||||
|
|
||||||
// TODO: calculate using new function
|
const k = computeK(pool.YES, pool.NO, p)
|
||||||
const poolChange = outcome === 'YES' ? shares + y - n : shares + n - y
|
|
||||||
const k = y * n
|
// Find bet amount that preserves k after selling shares.
|
||||||
const shareValue = 0.5 * (shares + y + n - Math.sqrt(4 * k + poolChange ** 2))
|
let lowAmount = 0
|
||||||
return shareValue
|
let highAmount = shares
|
||||||
|
let mid = 0
|
||||||
|
let kGuess = 0
|
||||||
|
while (Math.abs(k - kGuess) > 0.00000000001) {
|
||||||
|
mid = lowAmount + (highAmount - lowAmount) / 2
|
||||||
|
kGuess = sellSharesK(pool.YES, pool.NO, p, shares, outcome, mid)
|
||||||
|
if (kGuess < k) {
|
||||||
|
highAmount = mid
|
||||||
|
} else {
|
||||||
|
lowAmount = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mid
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateCpmmSale(
|
export function calculateCpmmSale(
|
||||||
contract: FullContract<CPMM, Binary>,
|
contract: FullContract<CPMM, Binary>,
|
||||||
bet: Bet
|
bet: { shares: number; outcome: string }
|
||||||
) {
|
) {
|
||||||
const { shares, outcome } = bet
|
const { shares, outcome } = bet
|
||||||
|
|
||||||
const rawSaleValue = calculateCpmmShareValue(contract, shares, outcome)
|
const rawSaleValue = calculateCpmmShareValue(
|
||||||
|
contract,
|
||||||
|
Math.abs(shares),
|
||||||
|
outcome as 'YES' | 'NO'
|
||||||
|
)
|
||||||
|
|
||||||
const { fees, remainingBet: saleValue } = getCpmmLiquidityFee(
|
const { fees, remainingBet: saleValue } = getCpmmLiquidityFee(
|
||||||
contract,
|
contract,
|
||||||
|
@ -153,9 +185,11 @@ export function calculateCpmmSale(
|
||||||
? [y + shares - saleValue + fee, n - saleValue + fee]
|
? [y + shares - saleValue + fee, n - saleValue + fee]
|
||||||
: [y - saleValue + fee, n + shares - saleValue + fee]
|
: [y - saleValue + fee, n + shares - saleValue + fee]
|
||||||
|
|
||||||
const newPool = { YES: newY, NO: newN }
|
const postBetPool = { YES: newY, NO: newN }
|
||||||
|
|
||||||
return { saleValue, newPool, fees }
|
const { newPool, newP } = addCpmmLiquidity(postBetPool, contract.p, fee)
|
||||||
|
|
||||||
|
return { saleValue, newPool, newP, fees }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCpmmProbabilityAfterSale(
|
export function getCpmmProbabilityAfterSale(
|
||||||
|
|
|
@ -85,21 +85,24 @@ export const getSellBetInfo = (
|
||||||
|
|
||||||
export const getCpmmSellBetInfo = (
|
export const getCpmmSellBetInfo = (
|
||||||
user: User,
|
user: User,
|
||||||
bet: Bet,
|
shares: number,
|
||||||
|
outcome: 'YES' | 'NO',
|
||||||
contract: FullContract<CPMM, Binary>,
|
contract: FullContract<CPMM, Binary>,
|
||||||
newBetId: string
|
newBetId: string
|
||||||
) => {
|
) => {
|
||||||
const { pool, p } = contract
|
const { pool, p } = contract
|
||||||
const { id: betId, amount, shares, outcome } = bet
|
|
||||||
|
|
||||||
const { saleValue, newPool, fees } = calculateCpmmSale(contract, bet)
|
const { saleValue, newPool, newP, fees } = calculateCpmmSale(contract, {
|
||||||
|
shares,
|
||||||
|
outcome,
|
||||||
|
})
|
||||||
|
|
||||||
const probBefore = getCpmmProbability(pool, p)
|
const probBefore = getCpmmProbability(pool, p)
|
||||||
const probAfter = getCpmmProbability(newPool, p)
|
const probAfter = getCpmmProbability(newPool, p)
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'SELL M$',
|
'SELL M$',
|
||||||
amount,
|
shares,
|
||||||
outcome,
|
outcome,
|
||||||
'for M$',
|
'for M$',
|
||||||
saleValue,
|
saleValue,
|
||||||
|
@ -117,10 +120,6 @@ export const getCpmmSellBetInfo = (
|
||||||
probBefore,
|
probBefore,
|
||||||
probAfter,
|
probAfter,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
sale: {
|
|
||||||
amount: saleValue,
|
|
||||||
betId,
|
|
||||||
},
|
|
||||||
fees,
|
fees,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +128,7 @@ export const getCpmmSellBetInfo = (
|
||||||
return {
|
return {
|
||||||
newBet,
|
newBet,
|
||||||
newPool,
|
newPool,
|
||||||
|
newP,
|
||||||
newBalance,
|
newBalance,
|
||||||
fees,
|
fees,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ export * from './place-bet'
|
||||||
export * from './resolve-market'
|
export * from './resolve-market'
|
||||||
export * from './stripe'
|
export * from './stripe'
|
||||||
export * from './sell-bet'
|
export * from './sell-bet'
|
||||||
|
export * from './sell-shares'
|
||||||
export * from './create-contract'
|
export * from './create-contract'
|
||||||
export * from './create-user'
|
export * from './create-user'
|
||||||
export * from './create-fold'
|
export * from './create-fold'
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as functions from 'firebase-functions'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getCpmmSellBetInfo, getSellBetInfo } from '../../common/sell-bet'
|
import { getSellBetInfo } from '../../common/sell-bet'
|
||||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||||
import { Fees } from '../../common/fees'
|
import { Fees } from '../../common/fees'
|
||||||
|
|
||||||
|
@ -34,8 +34,14 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (!contractSnap.exists)
|
if (!contractSnap.exists)
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
|
|
||||||
const { closeTime, mechanism, collectedFees, volume } = contract
|
const { closeTime, mechanism, collectedFees, volume } = contract
|
||||||
|
|
||||||
|
if (mechanism !== 'dpm-2')
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Sell shares only works with mechanism dpm-2',
|
||||||
|
}
|
||||||
|
|
||||||
if (closeTime && Date.now() > closeTime)
|
if (closeTime && Date.now() > closeTime)
|
||||||
return { status: 'error', message: 'Trading is closed' }
|
return { status: 'error', message: 'Trading is closed' }
|
||||||
|
|
||||||
|
@ -57,15 +63,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
newTotalBets,
|
newTotalBets,
|
||||||
newBalance,
|
newBalance,
|
||||||
fees,
|
fees,
|
||||||
} =
|
} = getSellBetInfo(user, bet, contract, newBetDoc.id)
|
||||||
mechanism === 'dpm-2'
|
|
||||||
? getSellBetInfo(user, bet, contract, newBetDoc.id)
|
|
||||||
: (getCpmmSellBetInfo(
|
|
||||||
user,
|
|
||||||
bet,
|
|
||||||
contract as any,
|
|
||||||
newBetDoc.id
|
|
||||||
) as any)
|
|
||||||
|
|
||||||
if (!isFinite(newBalance)) {
|
if (!isFinite(newBalance)) {
|
||||||
throw new Error('Invalid user balance for ' + user.username)
|
throw new Error('Invalid user balance for ' + user.username)
|
||||||
|
@ -81,7 +79,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
totalShares: newTotalShares,
|
totalShares: newTotalShares,
|
||||||
totalBets: newTotalBets,
|
totalBets: newTotalBets,
|
||||||
collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
|
collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
|
||||||
volume: volume + bet.amount,
|
volume: volume + Math.abs(newBet.amount),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
111
functions/src/sell-shares.ts
Normal file
111
functions/src/sell-shares.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
|
||||||
|
import { Binary, CPMM, FullContract } from '../../common/contract'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { getCpmmSellBetInfo } from '../../common/sell-bet'
|
||||||
|
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||||
|
import { getValues } from './utils'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
|
||||||
|
export const sellShares = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
|
async (
|
||||||
|
data: {
|
||||||
|
contractId: string
|
||||||
|
shares: number
|
||||||
|
outcome: 'YES' | 'NO'
|
||||||
|
},
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const userId = context?.auth?.uid
|
||||||
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
|
const { contractId, shares, outcome } = data
|
||||||
|
|
||||||
|
// 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 FullContract<CPMM, Binary>
|
||||||
|
const { closeTime, mechanism, collectedFees, volume } = contract
|
||||||
|
|
||||||
|
if (mechanism !== 'cpmm-1')
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Sell shares only works with mechanism cpmm-1',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeTime && Date.now() > closeTime)
|
||||||
|
return { status: 'error', message: 'Trading is closed' }
|
||||||
|
|
||||||
|
const userBets = await getValues<Bet>(
|
||||||
|
contractDoc.collection('bets').where('userId', '==', userId)
|
||||||
|
)
|
||||||
|
|
||||||
|
const [yesBets, noBets] = _.partition(
|
||||||
|
userBets ?? [],
|
||||||
|
(bet) => bet.outcome === 'YES'
|
||||||
|
)
|
||||||
|
const [yesShares, noShares] = [
|
||||||
|
_.sumBy(yesBets, (bet) => bet.shares),
|
||||||
|
_.sumBy(noBets, (bet) => bet.shares),
|
||||||
|
]
|
||||||
|
|
||||||
|
const maxShares = outcome === 'YES' ? yesShares : noShares
|
||||||
|
if (shares > maxShares + 0.000000000001) {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: `You can only sell ${maxShares} shares`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBetDoc = firestore
|
||||||
|
.collection(`contracts/${contractId}/bets`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
|
const { newBet, newPool, newP, newBalance, fees } = getCpmmSellBetInfo(
|
||||||
|
user,
|
||||||
|
shares,
|
||||||
|
outcome,
|
||||||
|
contract,
|
||||||
|
newBetDoc.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isFinite(newP)) {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Trade rejected due to overflow error.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFinite(newBalance)) {
|
||||||
|
throw new Error('Invalid user balance for ' + user.username)
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.update(userDoc, { balance: newBalance })
|
||||||
|
transaction.create(newBetDoc, newBet)
|
||||||
|
transaction.update(
|
||||||
|
contractDoc,
|
||||||
|
removeUndefinedProps({
|
||||||
|
pool: newPool,
|
||||||
|
p: newP,
|
||||||
|
collectedFees: addObjects(fees ?? {}, collectedFees ?? {}),
|
||||||
|
volume: volume + Math.abs(newBet.amount),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
|
@ -1,50 +1,39 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { formatMoney } from '../../common/util/format'
|
import { formatMoney, formatWithCommas } from '../../common/util/format'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { useUserContractBets } from '../hooks/use-user-bets'
|
import { Bet, MAX_LOAN_PER_CONTRACT } from '../../common/bet'
|
||||||
import { MAX_LOAN_PER_CONTRACT } from '../../common/bet'
|
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
|
import { calculateCpmmSale } from '../../common/calculate-cpmm'
|
||||||
|
import { Binary, CPMM, FullContract } from '../../common/contract'
|
||||||
|
|
||||||
export function AmountInput(props: {
|
export function AmountInput(props: {
|
||||||
amount: number | undefined
|
amount: number | undefined
|
||||||
onChange: (newAmount: number | undefined) => void
|
onChange: (newAmount: number | undefined) => void
|
||||||
error: string | undefined
|
error: string | undefined
|
||||||
setError: (error: string | undefined) => void
|
label: string
|
||||||
contractIdForLoan: string | undefined
|
|
||||||
minimumAmount?: number
|
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
inputClassName?: string
|
inputClassName?: string
|
||||||
// Needed to focus the amount input
|
// Needed to focus the amount input
|
||||||
inputRef?: React.MutableRefObject<any>
|
inputRef?: React.MutableRefObject<any>
|
||||||
|
children?: any
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
onChange,
|
onChange,
|
||||||
error,
|
error,
|
||||||
setError,
|
label,
|
||||||
contractIdForLoan,
|
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
minimumAmount,
|
|
||||||
inputRef,
|
inputRef,
|
||||||
|
children,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const user = useUser()
|
|
||||||
|
|
||||||
const userBets = useUserContractBets(user?.id, contractIdForLoan) ?? []
|
|
||||||
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
|
||||||
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
|
|
||||||
|
|
||||||
const loanAmount = contractIdForLoan
|
|
||||||
? Math.min(amount ?? 0, MAX_LOAN_PER_CONTRACT - prevLoanAmount)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
const onAmountChange = (str: string) => {
|
const onAmountChange = (str: string) => {
|
||||||
if (str.includes('-')) {
|
if (str.includes('-')) {
|
||||||
onChange(undefined)
|
onChange(undefined)
|
||||||
|
@ -56,28 +45,12 @@ export function AmountInput(props: {
|
||||||
if (amount >= 10 ** 9) return
|
if (amount >= 10 ** 9) return
|
||||||
|
|
||||||
onChange(str ? amount : undefined)
|
onChange(str ? amount : undefined)
|
||||||
|
|
||||||
const loanAmount = contractIdForLoan
|
|
||||||
? Math.min(amount, MAX_LOAN_PER_CONTRACT - prevLoanAmount)
|
|
||||||
: 0
|
|
||||||
const amountNetLoan = amount - loanAmount
|
|
||||||
|
|
||||||
if (user && user.balance < amountNetLoan) {
|
|
||||||
setError('Insufficient balance')
|
|
||||||
} else if (minimumAmount && amount < minimumAmount) {
|
|
||||||
setError('Minimum amount: ' + formatMoney(minimumAmount))
|
|
||||||
} else {
|
|
||||||
setError(undefined)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const amountNetLoan = (amount ?? 0) - loanAmount
|
|
||||||
const remainingBalance = Math.max(0, (user?.balance ?? 0) - amountNetLoan)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
<label className="input-group">
|
<label className="input-group">
|
||||||
<span className="bg-gray-200 text-sm">M$</span>
|
<span className="bg-gray-200 text-sm">{label}</span>
|
||||||
<input
|
<input
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'input input-bordered',
|
'input input-bordered',
|
||||||
|
@ -101,6 +74,79 @@ export function AmountInput(props: {
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BuyAmountInput(props: {
|
||||||
|
amount: number | undefined
|
||||||
|
onChange: (newAmount: number | undefined) => void
|
||||||
|
error: string | undefined
|
||||||
|
setError: (error: string | undefined) => void
|
||||||
|
contractIdForLoan: string | undefined
|
||||||
|
userBets?: Bet[]
|
||||||
|
minimumAmount?: number
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
inputClassName?: string
|
||||||
|
// Needed to focus the amount input
|
||||||
|
inputRef?: React.MutableRefObject<any>
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
amount,
|
||||||
|
onChange,
|
||||||
|
userBets,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
contractIdForLoan,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
inputClassName,
|
||||||
|
minimumAmount,
|
||||||
|
inputRef,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const openUserBets = (userBets ?? []).filter(
|
||||||
|
(bet) => !bet.isSold && !bet.sale
|
||||||
|
)
|
||||||
|
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
|
||||||
|
|
||||||
|
const loanAmount = contractIdForLoan
|
||||||
|
? Math.min(amount ?? 0, MAX_LOAN_PER_CONTRACT - prevLoanAmount)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const onAmountChange = (amount: number | undefined) => {
|
||||||
|
onChange(amount)
|
||||||
|
|
||||||
|
// Check for errors.
|
||||||
|
if (amount !== undefined) {
|
||||||
|
const amountNetLoan = amount - loanAmount
|
||||||
|
|
||||||
|
if (user && user.balance < amountNetLoan) {
|
||||||
|
setError('Insufficient balance')
|
||||||
|
} else if (minimumAmount && amount < minimumAmount) {
|
||||||
|
setError('Minimum amount: ' + formatMoney(minimumAmount))
|
||||||
|
} else {
|
||||||
|
setError(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AmountInput
|
||||||
|
amount={amount}
|
||||||
|
onChange={onAmountChange}
|
||||||
|
label="M$"
|
||||||
|
error={error}
|
||||||
|
disabled={disabled}
|
||||||
|
className={className}
|
||||||
|
inputClassName={inputClassName}
|
||||||
|
inputRef={inputRef}
|
||||||
|
>
|
||||||
{user && (
|
{user && (
|
||||||
<Col className="gap-3 text-sm">
|
<Col className="gap-3 text-sm">
|
||||||
{contractIdForLoan && (
|
{contractIdForLoan && (
|
||||||
|
@ -116,14 +162,106 @@ export function AmountInput(props: {
|
||||||
<span className="text-neutral">{formatMoney(loanAmount)}</span>{' '}
|
<span className="text-neutral">{formatMoney(loanAmount)}</span>{' '}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
<Row className="items-center justify-between gap-2 text-gray-500">
|
|
||||||
Remaining balance{' '}
|
|
||||||
<span className="text-neutral">
|
|
||||||
{formatMoney(Math.floor(remainingBalance))}
|
|
||||||
</span>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</AmountInput>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SellAmountInput(props: {
|
||||||
|
contract: FullContract<CPMM, Binary>
|
||||||
|
amount: number | undefined
|
||||||
|
onChange: (newAmount: number | undefined) => void
|
||||||
|
userBets: Bet[]
|
||||||
|
error: string | undefined
|
||||||
|
setError: (error: string | undefined) => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
inputClassName?: string
|
||||||
|
// Needed to focus the amount input
|
||||||
|
inputRef?: React.MutableRefObject<any>
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
contract,
|
||||||
|
amount,
|
||||||
|
onChange,
|
||||||
|
userBets,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
inputClassName,
|
||||||
|
inputRef,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
|
||||||
|
const [yesBets, noBets] = _.partition(
|
||||||
|
openUserBets,
|
||||||
|
(bet) => bet.outcome === 'YES'
|
||||||
|
)
|
||||||
|
const [yesShares, noShares] = [
|
||||||
|
_.sumBy(yesBets, (bet) => bet.shares),
|
||||||
|
_.sumBy(noBets, (bet) => bet.shares),
|
||||||
|
]
|
||||||
|
|
||||||
|
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
||||||
|
const shares = yesShares || noShares
|
||||||
|
|
||||||
|
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
|
||||||
|
|
||||||
|
const sharesSold = Math.min(amount ?? 0, yesShares || noShares)
|
||||||
|
const { saleValue } = calculateCpmmSale(contract, {
|
||||||
|
shares: sharesSold,
|
||||||
|
outcome: sellOutcome as 'YES' | 'NO',
|
||||||
|
})
|
||||||
|
|
||||||
|
const loanRepaid = Math.min(prevLoanAmount, saleValue)
|
||||||
|
|
||||||
|
const onAmountChange = (amount: number | undefined) => {
|
||||||
|
onChange(amount)
|
||||||
|
|
||||||
|
// Check for errors.
|
||||||
|
if (amount !== undefined) {
|
||||||
|
if (amount > shares) {
|
||||||
|
setError(`Maximum ${formatWithCommas(Math.floor(shares))} shares`)
|
||||||
|
} else {
|
||||||
|
setError(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AmountInput
|
||||||
|
amount={amount}
|
||||||
|
onChange={onAmountChange}
|
||||||
|
label="Qty"
|
||||||
|
error={error}
|
||||||
|
disabled={disabled}
|
||||||
|
className={className}
|
||||||
|
inputClassName={inputClassName}
|
||||||
|
inputRef={inputRef}
|
||||||
|
>
|
||||||
|
{user && (
|
||||||
|
<Col className="gap-3 text-sm">
|
||||||
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
|
Sale proceeds{' '}
|
||||||
|
<span className="text-neutral">{formatMoney(saleValue)}</span>
|
||||||
|
</Row>
|
||||||
|
{!!prevLoanAmount && (
|
||||||
|
<Row className="items-center justify-between gap-2 text-gray-500">
|
||||||
|
<Row className="items-center gap-2">
|
||||||
|
Loan repaid{' '}
|
||||||
|
<InfoTooltip
|
||||||
|
text={`Sold shares go toward paying off loans first.`}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<span className="text-neutral">{formatMoney(loanRepaid)}</span>{' '}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</AmountInput>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { XIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { Answer } from '../../../common/answer'
|
import { Answer } from '../../../common/answer'
|
||||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||||
import { AmountInput } from '../amount-input'
|
import { BuyAmountInput } from '../amount-input'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { placeBet } from '../../lib/firebase/api-call'
|
import { placeBet } from '../../lib/firebase/api-call'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
@ -114,7 +114,7 @@ export function AnswerBetPanel(props: {
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<div className="my-3 text-left text-sm text-gray-500">Amount </div>
|
<div className="my-3 text-left text-sm text-gray-500">Amount </div>
|
||||||
<AmountInput
|
<BuyAmountInput
|
||||||
inputClassName="w-full"
|
inputClassName="w-full"
|
||||||
amount={betAmount}
|
amount={betAmount}
|
||||||
onChange={setBetAmount}
|
onChange={setBetAmount}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useState } from 'react'
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
|
||||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||||
import { AmountInput } from '../amount-input'
|
import { BuyAmountInput } from '../amount-input'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { createAnswer } from '../../lib/firebase/api-call'
|
import { createAnswer } from '../../lib/firebase/api-call'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
@ -97,7 +97,7 @@ export function CreateAnswerPanel(props: {
|
||||||
<>
|
<>
|
||||||
<Col className="mt-1 gap-2">
|
<Col className="mt-1 gap-2">
|
||||||
<div className="text-sm text-gray-500">Buy amount</div>
|
<div className="text-sm text-gray-500">Buy amount</div>
|
||||||
<AmountInput
|
<BuyAmountInput
|
||||||
amount={betAmount}
|
amount={betAmount}
|
||||||
onChange={setBetAmount}
|
onChange={setBetAmount}
|
||||||
error={amountError}
|
error={amountError}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||||
|
@ -13,10 +14,10 @@ import {
|
||||||
formatWithCommas,
|
formatWithCommas,
|
||||||
} from '../../common/util/format'
|
} from '../../common/util/format'
|
||||||
import { Title } from './title'
|
import { Title } from './title'
|
||||||
import { firebaseLogin } from '../lib/firebase/users'
|
import { firebaseLogin, User } from '../lib/firebase/users'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { placeBet } from '../lib/firebase/api-call'
|
import { placeBet, sellShares } from '../lib/firebase/api-call'
|
||||||
import { AmountInput } from './amount-input'
|
import { BuyAmountInput, SellAmountInput } from './amount-input'
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { OutcomeLabel } from './outcome-label'
|
import { OutcomeLabel } from './outcome-label'
|
||||||
import {
|
import {
|
||||||
|
@ -26,30 +27,219 @@ import {
|
||||||
getOutcomeProbabilityAfterBet,
|
getOutcomeProbabilityAfterBet,
|
||||||
} from '../../common/calculate'
|
} from '../../common/calculate'
|
||||||
import { useFocus } from '../hooks/use-focus'
|
import { useFocus } from '../hooks/use-focus'
|
||||||
|
import { useUserContractBets } from '../hooks/use-user-bets'
|
||||||
|
import {
|
||||||
|
calculateCpmmSale,
|
||||||
|
getCpmmProbability,
|
||||||
|
} from '../../common/calculate-cpmm'
|
||||||
|
import { Modal } from './layout/modal'
|
||||||
|
|
||||||
export function BetPanel(props: {
|
export function BetPanel(props: {
|
||||||
contract: FullContract<DPM | CPMM, Binary>
|
contract: FullContract<DPM | CPMM, Binary>
|
||||||
className?: string
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, className } = props
|
||||||
|
const { mechanism } = contract
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
|
|
||||||
|
const [showSellModal, setShowSellModal] = useState(false)
|
||||||
|
|
||||||
|
const { yesShares, noShares } = useSaveShares(contract, userBets)
|
||||||
|
|
||||||
|
const shares = yesShares || noShares
|
||||||
|
const sharesOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className={className}>
|
||||||
|
{sharesOutcome && user && mechanism === 'cpmm-1' && (
|
||||||
|
<Col className="rounded-t-md bg-gray-100 px-6 py-6">
|
||||||
|
<Row className="items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
You have {formatWithCommas(Math.floor(shares))}{' '}
|
||||||
|
<OutcomeLabel outcome={sharesOutcome} /> shares
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '2px solid',
|
||||||
|
color: '#3D4451',
|
||||||
|
}}
|
||||||
|
onClick={() => setShowSellModal(true)}
|
||||||
|
>
|
||||||
|
Sell
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showSellModal && (
|
||||||
|
<SellSharesModal
|
||||||
|
contract={contract as FullContract<CPMM, Binary>}
|
||||||
|
user={user}
|
||||||
|
userBets={userBets ?? []}
|
||||||
|
shares={shares}
|
||||||
|
sharesOutcome={sharesOutcome}
|
||||||
|
setOpen={setShowSellModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Col
|
||||||
|
className={clsx(
|
||||||
|
'rounded-b-md bg-white px-8 py-6',
|
||||||
|
!sharesOutcome && 'rounded-t-md',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Title className={clsx('!mt-0')} text="Place a trade" />
|
||||||
|
|
||||||
|
<BuyPanel contract={contract} user={user} userBets={userBets ?? []} />
|
||||||
|
|
||||||
|
{user === null && (
|
||||||
|
<button
|
||||||
|
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||||
|
onClick={firebaseLogin}
|
||||||
|
>
|
||||||
|
Sign in to trade!
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BetPanelSwitcher(props: {
|
||||||
|
contract: FullContract<DPM | CPMM, Binary>
|
||||||
|
className?: string
|
||||||
title?: string // Set if BetPanel is on a feed modal
|
title?: string // Set if BetPanel is on a feed modal
|
||||||
selected?: 'YES' | 'NO'
|
selected?: 'YES' | 'NO'
|
||||||
onBetSuccess?: () => void
|
onBetSuccess?: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const { contract, className, title, selected, onBetSuccess } = props
|
||||||
|
|
||||||
|
const { mechanism } = contract
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
|
|
||||||
|
const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY')
|
||||||
|
|
||||||
|
const { yesShares, noShares } = useSaveShares(contract, userBets)
|
||||||
|
|
||||||
|
const shares = yesShares || noShares
|
||||||
|
const sharesOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Switch back to BUY if the user has sold all their shares.
|
||||||
|
if (tradeType === 'SELL' && sharesOutcome === undefined) {
|
||||||
|
setTradeType('BUY')
|
||||||
|
}
|
||||||
|
}, [tradeType, sharesOutcome])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className={className}>
|
||||||
|
{sharesOutcome && mechanism === 'cpmm-1' && (
|
||||||
|
<Col className="rounded-t-md bg-gray-100 px-6 py-6">
|
||||||
|
<Row className="items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
You have {formatWithCommas(Math.floor(shares))}{' '}
|
||||||
|
<OutcomeLabel outcome={sharesOutcome} /> shares
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '2px solid',
|
||||||
|
color: '#3D4451',
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
tradeType === 'BUY' ? setTradeType('SELL') : setTradeType('BUY')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tradeType === 'BUY' ? 'Sell' : 'Buy'}
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Col
|
||||||
|
className={clsx(
|
||||||
|
'rounded-b-md bg-white px-8 py-6',
|
||||||
|
!sharesOutcome && 'rounded-t-md',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Title
|
||||||
|
className={clsx(
|
||||||
|
'!mt-0',
|
||||||
|
tradeType === 'BUY' && title ? '!text-xl' : ''
|
||||||
|
)}
|
||||||
|
text={tradeType === 'BUY' ? title ?? 'Place a trade' : 'Sell shares'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{tradeType === 'SELL' && user && sharesOutcome && (
|
||||||
|
<SellPanel
|
||||||
|
contract={contract as FullContract<CPMM, Binary>}
|
||||||
|
shares={yesShares || noShares}
|
||||||
|
sharesOutcome={sharesOutcome}
|
||||||
|
user={user}
|
||||||
|
userBets={userBets ?? []}
|
||||||
|
onSellSuccess={onBetSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tradeType === 'BUY' && (
|
||||||
|
<BuyPanel
|
||||||
|
contract={contract}
|
||||||
|
user={user}
|
||||||
|
userBets={userBets ?? []}
|
||||||
|
selected={selected}
|
||||||
|
onBuySuccess={onBetSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{user === null && (
|
||||||
|
<button
|
||||||
|
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
||||||
|
onClick={firebaseLogin}
|
||||||
|
>
|
||||||
|
Sign in to trade!
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BuyPanel(props: {
|
||||||
|
contract: FullContract<DPM | CPMM, Binary>
|
||||||
|
user: User | null | undefined
|
||||||
|
userBets: Bet[]
|
||||||
|
selected?: 'YES' | 'NO'
|
||||||
|
onBuySuccess?: () => void
|
||||||
|
}) {
|
||||||
|
const { contract, user, userBets, selected, onBuySuccess } = props
|
||||||
|
|
||||||
|
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
||||||
|
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||||
|
const [error, setError] = useState<string | undefined>()
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [wasSubmitted, setWasSubmitted] = useState(false)
|
||||||
|
|
||||||
|
const [inputRef, focusAmountInput] = useFocus()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// warm up cloud function
|
// warm up cloud function
|
||||||
placeBet({}).catch()
|
placeBet({}).catch()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { contract, className, title, selected, onBetSuccess } = props
|
useEffect(() => {
|
||||||
|
if (selected) focusAmountInput()
|
||||||
const user = useUser()
|
}, [selected, focusAmountInput])
|
||||||
|
|
||||||
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
|
|
||||||
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
|
||||||
const [inputRef, focusAmountInput] = useFocus()
|
|
||||||
|
|
||||||
const [error, setError] = useState<string | undefined>()
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
const [wasSubmitted, setWasSubmitted] = useState(false)
|
|
||||||
|
|
||||||
function onBetChoice(choice: 'YES' | 'NO') {
|
function onBetChoice(choice: 'YES' | 'NO') {
|
||||||
setBetChoice(choice)
|
setBetChoice(choice)
|
||||||
|
@ -83,9 +273,9 @@ export function BetPanel(props: {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
setWasSubmitted(true)
|
setWasSubmitted(true)
|
||||||
setBetAmount(undefined)
|
setBetAmount(undefined)
|
||||||
if (onBetSuccess) onBetSuccess()
|
if (onBuySuccess) onBuySuccess()
|
||||||
} else {
|
} else {
|
||||||
setError(result?.error || 'Error placing bet')
|
setError(result?.message || 'Error placing bet')
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,11 +304,6 @@ export function BetPanel(props: {
|
||||||
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
||||||
const currentReturnPercent = formatPercent(currentReturn)
|
const currentReturnPercent = formatPercent(currentReturn)
|
||||||
|
|
||||||
const panelTitle = title ?? 'Place a trade'
|
|
||||||
if (title) {
|
|
||||||
focusAmountInput()
|
|
||||||
}
|
|
||||||
|
|
||||||
const dpmTooltip =
|
const dpmTooltip =
|
||||||
contract.mechanism === 'dpm-2'
|
contract.mechanism === 'dpm-2'
|
||||||
? `Current payout for ${formatWithCommas(shares)} / ${formatWithCommas(
|
? `Current payout for ${formatWithCommas(shares)} / ${formatWithCommas(
|
||||||
|
@ -129,25 +314,19 @@ export function BetPanel(props: {
|
||||||
: 0)
|
: 0)
|
||||||
)} ${betChoice ?? 'YES'} shares`
|
)} ${betChoice ?? 'YES'} shares`
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={clsx('rounded-md bg-white px-8 py-6', className)}>
|
<>
|
||||||
<Title
|
|
||||||
className={clsx('!mt-0', title ? '!text-xl' : '')}
|
|
||||||
text={panelTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<YesNoSelector
|
<YesNoSelector
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
selected={betChoice}
|
selected={betChoice}
|
||||||
onSelect={(choice) => onBetChoice(choice)}
|
onSelect={(choice) => onBetChoice(choice)}
|
||||||
/>
|
/>
|
||||||
|
<div className="my-3 text-left text-sm text-gray-500">Amount</div>
|
||||||
<div className="my-3 text-left text-sm text-gray-500">Amount </div>
|
<BuyAmountInput
|
||||||
<AmountInput
|
|
||||||
inputClassName="w-full"
|
inputClassName="w-full"
|
||||||
amount={betAmount}
|
amount={betAmount}
|
||||||
onChange={onBetChange}
|
onChange={onBetChange}
|
||||||
|
userBets={userBets}
|
||||||
error={error}
|
error={error}
|
||||||
setError={setError}
|
setError={setError}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
@ -206,19 +385,180 @@ export function BetPanel(props: {
|
||||||
)}
|
)}
|
||||||
onClick={betDisabled ? undefined : submitBet}
|
onClick={betDisabled ? undefined : submitBet}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Submitting...' : 'Submit trade'}
|
{isSubmitting ? 'Submitting...' : 'Submit Buy'}
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{user === null && (
|
|
||||||
<button
|
|
||||||
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
|
|
||||||
onClick={firebaseLogin}
|
|
||||||
>
|
|
||||||
Sign in to trade!
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{wasSubmitted && <div className="mt-4">Trade submitted!</div>}
|
{wasSubmitted && <div className="mt-4">Buy submitted!</div>}
|
||||||
</Col>
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SellPanel(props: {
|
||||||
|
contract: FullContract<CPMM, Binary>
|
||||||
|
userBets: Bet[]
|
||||||
|
shares: number
|
||||||
|
sharesOutcome: 'YES' | 'NO'
|
||||||
|
user: User
|
||||||
|
onSellSuccess?: () => void
|
||||||
|
}) {
|
||||||
|
const { contract, shares, sharesOutcome, userBets, user, onSellSuccess } =
|
||||||
|
props
|
||||||
|
|
||||||
|
const [amount, setAmount] = useState<number | undefined>(shares)
|
||||||
|
const [error, setError] = useState<string | undefined>()
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [wasSubmitted, setWasSubmitted] = useState(false)
|
||||||
|
|
||||||
|
const betDisabled = isSubmitting || !amount || error
|
||||||
|
|
||||||
|
async function submitSell() {
|
||||||
|
if (!user || !amount) return
|
||||||
|
|
||||||
|
setError(undefined)
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
// Sell all shares if remaining shares would be < 1
|
||||||
|
const sellAmount = amount === Math.floor(shares) ? shares : amount
|
||||||
|
|
||||||
|
const result = await sellShares({
|
||||||
|
shares: sellAmount,
|
||||||
|
outcome: sharesOutcome,
|
||||||
|
contractId: contract.id,
|
||||||
|
}).then((r) => r.data)
|
||||||
|
|
||||||
|
console.log('Sold shares. Result:', result)
|
||||||
|
|
||||||
|
if (result?.status === 'success') {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
setWasSubmitted(true)
|
||||||
|
setAmount(undefined)
|
||||||
|
if (onSellSuccess) onSellSuccess()
|
||||||
|
} else {
|
||||||
|
setError(result?.message || 'Error selling')
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialProb = getProbability(contract)
|
||||||
|
const { newPool } = calculateCpmmSale(contract, {
|
||||||
|
shares: Math.min(amount ?? 0, shares),
|
||||||
|
outcome: sharesOutcome,
|
||||||
|
})
|
||||||
|
const resultProb = getCpmmProbability(newPool, contract.p)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SellAmountInput
|
||||||
|
inputClassName="w-full"
|
||||||
|
contract={contract}
|
||||||
|
amount={amount ? Math.floor(amount) : undefined}
|
||||||
|
onChange={setAmount}
|
||||||
|
userBets={userBets}
|
||||||
|
error={error}
|
||||||
|
setError={setError}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Col className="mt-3 w-full gap-3">
|
||||||
|
<Row className="items-center justify-between text-sm">
|
||||||
|
<div className="text-gray-500">Probability</div>
|
||||||
|
<Row>
|
||||||
|
<div>{formatPercent(initialProb)}</div>
|
||||||
|
<div className="mx-2">→</div>
|
||||||
|
<div>{formatPercent(resultProb)}</div>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Spacer h={8} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'btn flex-1',
|
||||||
|
betDisabled
|
||||||
|
? 'btn-disabled'
|
||||||
|
: sharesOutcome === 'YES'
|
||||||
|
? 'btn-primary'
|
||||||
|
: 'border-none bg-red-400 hover:bg-red-500',
|
||||||
|
isSubmitting ? 'loading' : ''
|
||||||
|
)}
|
||||||
|
onClick={betDisabled ? undefined : submitSell}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Submitting...' : 'Submit sell'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{wasSubmitted && <div className="mt-4">Sell submitted!</div>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSaveShares = (
|
||||||
|
contract: FullContract<CPMM | DPM, Binary>,
|
||||||
|
userBets: Bet[] | undefined
|
||||||
|
) => {
|
||||||
|
const [savedShares, setSavedShares] = useState<
|
||||||
|
{ yesShares: number; noShares: number } | undefined
|
||||||
|
>()
|
||||||
|
|
||||||
|
const [yesBets, noBets] = _.partition(
|
||||||
|
userBets ?? [],
|
||||||
|
(bet) => bet.outcome === 'YES'
|
||||||
|
)
|
||||||
|
const [yesShares, noShares] = [
|
||||||
|
_.sumBy(yesBets, (bet) => bet.shares),
|
||||||
|
_.sumBy(noBets, (bet) => bet.shares),
|
||||||
|
]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Save yes and no shares to local storage.
|
||||||
|
const savedShares = localStorage.getItem(`${contract.id}-shares`)
|
||||||
|
if (!userBets && savedShares) {
|
||||||
|
setSavedShares(JSON.parse(savedShares))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userBets) {
|
||||||
|
const updatedShares = { yesShares, noShares }
|
||||||
|
localStorage.setItem(
|
||||||
|
`${contract.id}-shares`,
|
||||||
|
JSON.stringify(updatedShares)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [contract.id, userBets, noShares, yesShares])
|
||||||
|
|
||||||
|
if (userBets) return { yesShares, noShares }
|
||||||
|
return savedShares ?? { yesShares: 0, noShares: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function SellSharesModal(props: {
|
||||||
|
contract: FullContract<CPMM, Binary>
|
||||||
|
userBets: Bet[]
|
||||||
|
shares: number
|
||||||
|
sharesOutcome: 'YES' | 'NO'
|
||||||
|
user: User
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { contract, shares, sharesOutcome, userBets, user, setOpen } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={true} setOpen={setOpen}>
|
||||||
|
<Col className="rounded-md bg-white px-8 py-6">
|
||||||
|
<Title className="!mt-0" text={'Sell shares'} />
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
You have {formatWithCommas(Math.floor(shares))}{' '}
|
||||||
|
<OutcomeLabel outcome={sharesOutcome} /> shares
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SellPanel
|
||||||
|
contract={contract}
|
||||||
|
shares={shares}
|
||||||
|
sharesOutcome={sharesOutcome}
|
||||||
|
user={user}
|
||||||
|
userBets={userBets ?? []}
|
||||||
|
onSellSuccess={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Fragment, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
|
||||||
|
|
||||||
import { BetPanel } from './bet-panel'
|
import { BetPanelSwitcher } from './bet-panel'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { YesNoSelector } from './yes-no-selector'
|
import { YesNoSelector } from './yes-no-selector'
|
||||||
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||||
|
import { Modal } from './layout/modal'
|
||||||
|
|
||||||
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
||||||
export default function BetRow(props: {
|
export default function BetRow(props: {
|
||||||
|
@ -27,7 +27,7 @@ export default function BetRow(props: {
|
||||||
Place a trade
|
Place a trade
|
||||||
</div>
|
</div>
|
||||||
<YesNoSelector
|
<YesNoSelector
|
||||||
btnClassName="btn-sm w-20"
|
btnClassName="btn-sm w-24"
|
||||||
onSelect={(choice) => {
|
onSelect={(choice) => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setBetChoice(choice)
|
setBetChoice(choice)
|
||||||
|
@ -35,7 +35,7 @@ export default function BetRow(props: {
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Modal open={open} setOpen={setOpen}>
|
<Modal open={open} setOpen={setOpen}>
|
||||||
<BetPanel
|
<BetPanelSwitcher
|
||||||
contract={props.contract}
|
contract={props.contract}
|
||||||
title={props.contract.question}
|
title={props.contract.question}
|
||||||
selected={betChoice}
|
selected={betChoice}
|
||||||
|
@ -46,57 +46,3 @@ export default function BetRow(props: {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// From https://tailwindui.com/components/application-ui/overlays/modals
|
|
||||||
export function Modal(props: {
|
|
||||||
children: React.ReactNode
|
|
||||||
open: boolean
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
}) {
|
|
||||||
const { children, open, setOpen } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={open} as={Fragment}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
className="fixed inset-0 z-50 overflow-y-auto"
|
|
||||||
onClose={setOpen}
|
|
||||||
>
|
|
||||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
{/* This element is to trick the browser into centering the modal contents. */}
|
|
||||||
<span
|
|
||||||
className="hidden sm:inline-block sm:h-screen sm:align-middle"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
​
|
|
||||||
</span>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<div className="inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -472,6 +472,7 @@ export function ContractBetsTable(props: {
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="p-2">
|
<tr className="p-2">
|
||||||
<th></th>
|
<th></th>
|
||||||
|
{isCPMM && <th>Type</th>}
|
||||||
<th>Outcome</th>
|
<th>Outcome</th>
|
||||||
<th>Amount</th>
|
<th>Amount</th>
|
||||||
{!isCPMM && <th>{isResolved ? <>Payout</> : <>Sale price</>}</th>}
|
{!isCPMM && <th>{isResolved ? <>Payout</> : <>Sale price</>}</th>}
|
||||||
|
@ -541,13 +542,14 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
||||||
<SellButton contract={contract} bet={bet} />
|
<SellButton contract={contract} bet={bet} />
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
{isCPMM && <td>{shares >= 0 ? 'BUY' : 'SELL'}</td>}
|
||||||
<td>
|
<td>
|
||||||
<OutcomeLabel outcome={outcome} />
|
<OutcomeLabel outcome={outcome} />
|
||||||
</td>
|
</td>
|
||||||
<td>{formatMoney(amount)}</td>
|
<td>{formatMoney(Math.abs(amount))}</td>
|
||||||
{!isCPMM && <td>{saleDisplay}</td>}
|
{!isCPMM && <td>{saleDisplay}</td>}
|
||||||
{!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>}
|
{!isCPMM && !isResolved && <td>{payoutIfChosenDisplay}</td>}
|
||||||
<td>{formatWithCommas(shares)}</td>
|
<td>{formatWithCommas(Math.abs(shares))}</td>
|
||||||
<td>
|
<td>
|
||||||
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -36,7 +36,7 @@ import { DateTimeTooltip } from '../datetime-tooltip'
|
||||||
import { Bet } from '../../lib/firebase/bets'
|
import { Bet } from '../../lib/firebase/bets'
|
||||||
import { JoinSpans } from '../join-spans'
|
import { JoinSpans } from '../join-spans'
|
||||||
import { fromNow } from '../../lib/util/time'
|
import { fromNow } from '../../lib/util/time'
|
||||||
import BetRow, { Modal } from '../bet-row'
|
import BetRow from '../bet-row'
|
||||||
import { parseTags } from '../../../common/util/parse'
|
import { parseTags } from '../../../common/util/parse'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
import { useAdmin } from '../../hooks/use-admin'
|
import { useAdmin } from '../../hooks/use-admin'
|
||||||
|
@ -48,6 +48,7 @@ import { getDpmOutcomeProbability } from '../../../common/calculate-dpm'
|
||||||
import { AnswerBetPanel } from '../answers/answer-bet-panel'
|
import { AnswerBetPanel } from '../answers/answer-bet-panel'
|
||||||
import { useSaveSeenContract } from '../../hooks/use-seen-contracts'
|
import { useSaveSeenContract } from '../../hooks/use-seen-contracts'
|
||||||
import { User } from '../../../common/user'
|
import { User } from '../../../common/user'
|
||||||
|
import { Modal } from '../layout/modal'
|
||||||
|
|
||||||
export function FeedItems(props: {
|
export function FeedItems(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
|
56
web/components/layout/modal.tsx
Normal file
56
web/components/layout/modal.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
|
||||||
|
// From https://tailwindui.com/components/application-ui/overlays/modals
|
||||||
|
export function Modal(props: {
|
||||||
|
children: React.ReactNode
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { children, open, setOpen } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={open} as={Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
className="fixed inset-0 z-50 overflow-y-auto"
|
||||||
|
onClose={setOpen}
|
||||||
|
>
|
||||||
|
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
{/* This element is to trick the browser into centering the modal contents. */}
|
||||||
|
<span
|
||||||
|
className="hidden sm:inline-block sm:h-screen sm:align-middle"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
​
|
||||||
|
</span>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<div className="inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
|
@ -12,11 +12,15 @@ export function YesNoSelector(props: {
|
||||||
}) {
|
}) {
|
||||||
const { selected, onSelect, className, btnClassName } = props
|
const { selected, onSelect, className, btnClassName } = props
|
||||||
|
|
||||||
|
const commonClassNames =
|
||||||
|
'inline-flex flex-1 items-center justify-center rounded-3xl border-2 p-2'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('space-x-3', className)}>
|
<Row className={clsx('space-x-3', className)}>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'hover:bg-primary-focus border-primary hover:border-primary-focus inline-flex flex-1 items-center justify-center rounded-lg border-2 p-2 hover:text-white',
|
commonClassNames,
|
||||||
|
'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white',
|
||||||
selected == 'YES'
|
selected == 'YES'
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-white'
|
||||||
: 'text-primary bg-transparent',
|
: 'text-primary bg-transparent',
|
||||||
|
@ -28,7 +32,8 @@ export function YesNoSelector(props: {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'inline-flex flex-1 items-center justify-center rounded-lg border-2 border-red-400 p-2 hover:border-red-500 hover:bg-red-500 hover:text-white',
|
commonClassNames,
|
||||||
|
'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white',
|
||||||
selected == 'NO'
|
selected == 'NO'
|
||||||
? 'bg-red-400 text-white'
|
? 'bg-red-400 text-white'
|
||||||
: 'bg-transparent text-red-400',
|
: 'bg-transparent text-red-400',
|
||||||
|
@ -50,7 +55,7 @@ export function YesNoCancelSelector(props: {
|
||||||
}) {
|
}) {
|
||||||
const { selected, onSelect } = props
|
const { selected, onSelect } = props
|
||||||
|
|
||||||
const btnClassName = clsx('px-6 flex-1', props.btnClassName)
|
const btnClassName = clsx('px-6 flex-1 rounded-3xl', props.btnClassName)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
|
|
|
@ -18,6 +18,13 @@ export const createFold = cloudFunction<
|
||||||
|
|
||||||
export const placeBet = cloudFunction('placeBet')
|
export const placeBet = cloudFunction('placeBet')
|
||||||
|
|
||||||
|
export const sellBet = cloudFunction('sellBet')
|
||||||
|
|
||||||
|
export const sellShares = cloudFunction<
|
||||||
|
{ contractId: string; shares: number; outcome: 'YES' | 'NO' },
|
||||||
|
{ status: 'error' | 'success'; message?: string }
|
||||||
|
>('sellShares')
|
||||||
|
|
||||||
export const createAnswer = cloudFunction<
|
export const createAnswer = cloudFunction<
|
||||||
{ contractId: string; text: string; amount: number },
|
{ contractId: string; text: string; amount: number },
|
||||||
{
|
{
|
||||||
|
@ -38,8 +45,6 @@ export const resolveMarket = cloudFunction<
|
||||||
{ status: 'error' | 'success'; message?: string }
|
{ status: 'error' | 'success'; message?: string }
|
||||||
>('resolveMarket')
|
>('resolveMarket')
|
||||||
|
|
||||||
export const sellBet = cloudFunction('sellBet')
|
|
||||||
|
|
||||||
export const createUser: () => Promise<User | null> = () => {
|
export const createUser: () => Promise<User | null> = () => {
|
||||||
let deviceToken = window.localStorage.getItem('device-token')
|
let deviceToken = window.localStorage.getItem('device-token')
|
||||||
if (!deviceToken) {
|
if (!deviceToken) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { Spacer } from '../components/layout/spacer'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { Contract, contractPath } from '../lib/firebase/contracts'
|
import { Contract, contractPath } from '../lib/firebase/contracts'
|
||||||
import { createContract } from '../lib/firebase/api-call'
|
import { createContract } from '../lib/firebase/api-call'
|
||||||
import { AmountInput } from '../components/amount-input'
|
import { BuyAmountInput } from '../components/amount-input'
|
||||||
import { MINIMUM_ANTE } from '../../common/antes'
|
import { MINIMUM_ANTE } from '../../common/antes'
|
||||||
import { InfoTooltip } from '../components/info-tooltip'
|
import { InfoTooltip } from '../components/info-tooltip'
|
||||||
import { CREATOR_FEE } from '../../common/fees'
|
import { CREATOR_FEE } from '../../common/fees'
|
||||||
|
@ -241,7 +241,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
text={`Provide liquidity to encourage traders to participate.`}
|
text={`Provide liquidity to encourage traders to participate.`}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<AmountInput
|
<BuyAmountInput
|
||||||
amount={ante ?? undefined}
|
amount={ante ?? undefined}
|
||||||
minimumAmount={MINIMUM_ANTE}
|
minimumAmount={MINIMUM_ANTE}
|
||||||
onChange={setAnte}
|
onChange={setAnte}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Textarea from 'react-expanding-textarea'
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getProbability } from '../../common/calculate'
|
||||||
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||||
import { parseWordsAsTags } from '../../common/util/parse'
|
import { parseWordsAsTags } from '../../common/util/parse'
|
||||||
import { AmountInput } from '../components/amount-input'
|
import { BuyAmountInput } from '../components/amount-input'
|
||||||
import { InfoTooltip } from '../components/info-tooltip'
|
import { InfoTooltip } from '../components/info-tooltip'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
import { Row } from '../components/layout/row'
|
import { Row } from '../components/layout/row'
|
||||||
|
@ -241,7 +241,7 @@ ${TEST_VALUE}
|
||||||
You earn ${0.01 * 100}% of trading volume.`}
|
You earn ${0.01 * 100}% of trading volume.`}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<AmountInput
|
<BuyAmountInput
|
||||||
amount={ante}
|
amount={ante}
|
||||||
minimumAmount={10}
|
minimumAmount={10}
|
||||||
onChange={setAnte}
|
onChange={setAnte}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user