Create answer
This commit is contained in:
parent
f42f9d9a1a
commit
893016a7c3
27
common/calculate-multi.ts
Normal file
27
common/calculate-multi.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
|
export function getMultiProbability(
|
||||||
|
totalShares: {
|
||||||
|
[answerId: string]: number
|
||||||
|
},
|
||||||
|
answerId: string
|
||||||
|
) {
|
||||||
|
const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||||
|
const shares = totalShares[answerId] ?? 0
|
||||||
|
return shares ** 2 / squareSum
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateMultiShares(
|
||||||
|
totalShares: {
|
||||||
|
[answerId: string]: number
|
||||||
|
},
|
||||||
|
bet: number,
|
||||||
|
betChoice: string
|
||||||
|
) {
|
||||||
|
const squareSum = _.sumBy(Object.values(totalShares), (shares) => shares ** 2)
|
||||||
|
const shares = totalShares[betChoice] ?? 0
|
||||||
|
|
||||||
|
const c = 2 * bet * Math.sqrt(squareSum)
|
||||||
|
|
||||||
|
return Math.sqrt(bet ** 2 + shares ** 2 + c) - shares
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import { Bet } from './bet'
|
import { Bet } from './bet'
|
||||||
import { calculateShares, getProbability } from './calculate'
|
import { calculateShares, getProbability } from './calculate'
|
||||||
|
import { calculateMultiShares, getMultiProbability } from './calculate-multi'
|
||||||
import { Contract } from './contract'
|
import { Contract } from './contract'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
|
|
||||||
export const getNewBetInfo = (
|
export const getNewBinaryBetInfo = (
|
||||||
user: User,
|
user: User,
|
||||||
outcome: 'YES' | 'NO',
|
outcome: 'YES' | 'NO',
|
||||||
amount: number,
|
amount: number,
|
||||||
|
@ -52,3 +53,43 @@ export const getNewBetInfo = (
|
||||||
|
|
||||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getNewMultiBetInfo = (
|
||||||
|
user: User,
|
||||||
|
outcome: string,
|
||||||
|
amount: number,
|
||||||
|
contract: Contract<'MULTI'>,
|
||||||
|
newBetId: string
|
||||||
|
) => {
|
||||||
|
const { pool, totalShares, totalBets } = contract
|
||||||
|
|
||||||
|
const prevOutcomePool = pool[outcome] ?? 0
|
||||||
|
const newPool = { ...pool, outcome: prevOutcomePool + amount }
|
||||||
|
|
||||||
|
const shares = calculateMultiShares(contract.totalShares, amount, outcome)
|
||||||
|
|
||||||
|
const prevShares = totalShares[outcome] ?? 0
|
||||||
|
const newTotalShares = { ...totalShares, outcome: prevShares + shares }
|
||||||
|
|
||||||
|
const prevTotalBets = totalBets[outcome] ?? 0
|
||||||
|
const newTotalBets = { ...totalBets, outcome: prevTotalBets + amount }
|
||||||
|
|
||||||
|
const probBefore = getMultiProbability(totalShares, outcome)
|
||||||
|
const probAfter = getMultiProbability(newTotalShares, outcome)
|
||||||
|
|
||||||
|
const newBet: Bet<'MULTI'> = {
|
||||||
|
id: newBetId,
|
||||||
|
userId: user.id,
|
||||||
|
contractId: contract.id,
|
||||||
|
amount,
|
||||||
|
shares,
|
||||||
|
outcome,
|
||||||
|
probBefore,
|
||||||
|
probAfter,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBalance = user.balance - amount
|
||||||
|
|
||||||
|
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||||
|
}
|
||||||
|
|
|
@ -44,6 +44,14 @@ service cloud.firestore {
|
||||||
allow read;
|
allow read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match /contracts/{contractId}/answers/{answerId} {
|
||||||
|
allow read;
|
||||||
|
}
|
||||||
|
|
||||||
|
match /{somePath=**}/answers/{answerId} {
|
||||||
|
allow read;
|
||||||
|
}
|
||||||
|
|
||||||
match /folds/{foldId} {
|
match /folds/{foldId} {
|
||||||
allow read;
|
allow read;
|
||||||
allow update, delete: if request.auth.uid == resource.data.curatorId;
|
allow update, delete: if request.auth.uid == resource.data.curatorId;
|
||||||
|
|
98
functions/src/create-answer.ts
Normal file
98
functions/src/create-answer.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { getNewMultiBetInfo } from '../../common/new-bet'
|
||||||
|
import { Answer } from '../../common/answer'
|
||||||
|
|
||||||
|
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
|
async (
|
||||||
|
data: {
|
||||||
|
contractId: string
|
||||||
|
amount: number
|
||||||
|
text: string
|
||||||
|
},
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const userId = context?.auth?.uid
|
||||||
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
|
const { contractId, amount, text } = data
|
||||||
|
|
||||||
|
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
||||||
|
return { status: 'error', message: 'Invalid amount' }
|
||||||
|
|
||||||
|
if (!text || typeof text !== 'string' || text.length > 1000)
|
||||||
|
return { status: 'error', message: 'Invalid text' }
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
if (user.balance < amount)
|
||||||
|
return { status: 'error', message: 'Insufficient balance' }
|
||||||
|
|
||||||
|
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<'MULTI'>
|
||||||
|
|
||||||
|
if (
|
||||||
|
contract.outcomeType !== 'MULTI' ||
|
||||||
|
contract.outcomes !== 'FREE_ANSWER'
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Requires a multi, free answer contract',
|
||||||
|
}
|
||||||
|
|
||||||
|
const { closeTime } = contract
|
||||||
|
if (closeTime && Date.now() > closeTime)
|
||||||
|
return { status: 'error', message: 'Trading is closed' }
|
||||||
|
|
||||||
|
const newAnswerDoc = firestore
|
||||||
|
.collection(`contracts/${contractId}/answers`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
|
const answerId = newAnswerDoc.id
|
||||||
|
const { username, name, avatarUrl } = user
|
||||||
|
|
||||||
|
const answer: Answer = {
|
||||||
|
id: answerId,
|
||||||
|
contractId,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
userId: user.id,
|
||||||
|
username,
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
transaction.create(newAnswerDoc, answer)
|
||||||
|
|
||||||
|
const newBetDoc = firestore
|
||||||
|
.collection(`contracts/${contractId}/bets`)
|
||||||
|
.doc()
|
||||||
|
|
||||||
|
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||||
|
getNewMultiBetInfo(user, answerId, amount, contract, newBetDoc.id)
|
||||||
|
|
||||||
|
transaction.create(newBetDoc, newBet)
|
||||||
|
transaction.update(contractDoc, {
|
||||||
|
pool: newPool,
|
||||||
|
totalShares: newTotalShares,
|
||||||
|
totalBets: newTotalBets,
|
||||||
|
})
|
||||||
|
transaction.update(userDoc, { balance: newBalance })
|
||||||
|
|
||||||
|
return { status: 'success', answerId, betId: newBetDoc.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
|
@ -11,6 +11,7 @@ export * from './sell-bet'
|
||||||
export * from './create-contract'
|
export * from './create-contract'
|
||||||
export * from './create-user'
|
export * from './create-user'
|
||||||
export * from './create-fold'
|
export * from './create-fold'
|
||||||
|
export * from './create-answer'
|
||||||
export * from './on-fold-follow'
|
export * from './on-fold-follow'
|
||||||
export * from './on-fold-delete'
|
export * from './on-fold-delete'
|
||||||
export * from './unsubscribe'
|
export * from './unsubscribe'
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { getNewBetInfo } from '../../common/new-bet'
|
import { getNewBinaryBetInfo } from '../../common/new-bet'
|
||||||
|
|
||||||
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
@ -51,7 +51,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
.doc()
|
.doc()
|
||||||
|
|
||||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||||
getNewBetInfo(user, outcome, amount, contract, newBetDoc.id)
|
getNewBinaryBetInfo(user, outcome, amount, contract, newBetDoc.id)
|
||||||
|
|
||||||
transaction.create(newBetDoc, newBet)
|
transaction.create(newBetDoc, newBet)
|
||||||
transaction.update(contractDoc, {
|
transaction.update(contractDoc, {
|
||||||
|
|
95
web/components/answers-panel.tsx
Normal file
95
web/components/answers-panel.tsx
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
|
||||||
|
import { Answer } from '../../common/answer'
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { AmountInput } from './amount-input'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import { createAnswer } from '../lib/firebase/api-call'
|
||||||
|
|
||||||
|
export function AnswersPanel(props: {
|
||||||
|
contract: Contract<'MULTI'>
|
||||||
|
answers: Answer[]
|
||||||
|
}) {
|
||||||
|
const { contract, answers } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col>
|
||||||
|
<CreateAnswerInput contract={contract} />
|
||||||
|
{answers.map((answer) => (
|
||||||
|
<div>{answer.text}</div>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateAnswerInput(props: { contract: Contract<'MULTI'> }) {
|
||||||
|
const { contract } = props
|
||||||
|
const [text, setText] = useState('')
|
||||||
|
const [betAmount, setBetAmount] = useState<number | undefined>(10)
|
||||||
|
const [amountError, setAmountError] = useState<string | undefined>()
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const canSubmit = text && betAmount && !amountError && !isSubmitting
|
||||||
|
|
||||||
|
const submitAnswer = async () => {
|
||||||
|
if (canSubmit) {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
console.log('submitting', { text, betAmount })
|
||||||
|
const result = await createAnswer({
|
||||||
|
contractId: contract.id,
|
||||||
|
text,
|
||||||
|
amount: betAmount,
|
||||||
|
}).then((r) => r.data)
|
||||||
|
|
||||||
|
console.log('submit complte', result)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
setText('')
|
||||||
|
setBetAmount(10)
|
||||||
|
setAmountError(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className="gap-4">
|
||||||
|
<div className="text-xl text-indigo-700">Add your answer</div>
|
||||||
|
<Textarea
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
className="textarea textarea-bordered w-full"
|
||||||
|
placeholder="Type your answer..."
|
||||||
|
rows={1}
|
||||||
|
maxLength={10000}
|
||||||
|
/>
|
||||||
|
<Col className="gap-2 justify-between self-end sm:flex-row">
|
||||||
|
{text && (
|
||||||
|
<Col className="gap-2">
|
||||||
|
<div className="text-gray-500 text-sm">Bet amount</div>
|
||||||
|
<AmountInput
|
||||||
|
amount={betAmount}
|
||||||
|
onChange={setBetAmount}
|
||||||
|
error={amountError}
|
||||||
|
setError={setAmountError}
|
||||||
|
minimumAmount={10}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'btn btn-sm self-start',
|
||||||
|
canSubmit ? 'btn-outline' : 'btn-disabled'
|
||||||
|
)}
|
||||||
|
disabled={!canSubmit}
|
||||||
|
onClick={submitAnswer}
|
||||||
|
>
|
||||||
|
Submit answer
|
||||||
|
</button>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from '@heroicons/react/solid'
|
} from '@heroicons/react/solid'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
|
||||||
import { OutcomeLabel } from './outcome-label'
|
import { OutcomeLabel } from './outcome-label'
|
||||||
import {
|
import {
|
||||||
|
@ -37,7 +38,6 @@ import { useBetsWithoutAntes } from '../hooks/use-bets'
|
||||||
import { Bet } from '../lib/firebase/bets'
|
import { Bet } from '../lib/firebase/bets'
|
||||||
import { Comment, mapCommentsByBetId } from '../lib/firebase/comments'
|
import { Comment, mapCommentsByBetId } from '../lib/firebase/comments'
|
||||||
import { JoinSpans } from './join-spans'
|
import { JoinSpans } from './join-spans'
|
||||||
import Textarea from 'react-expanding-textarea'
|
|
||||||
import { outcome } from '../../common/contract'
|
import { outcome } from '../../common/contract'
|
||||||
import { fromNow } from '../lib/util/time'
|
import { fromNow } from '../lib/util/time'
|
||||||
import BetRow from './bet-row'
|
import BetRow from './bet-row'
|
||||||
|
@ -204,7 +204,7 @@ function EditContract(props: {
|
||||||
) : (
|
) : (
|
||||||
<Row>
|
<Row>
|
||||||
<button
|
<button
|
||||||
className="btn btn-neutral btn-outline btn-sm mt-4"
|
className="btn btn-neutral btn-outline btn-xs mt-4"
|
||||||
onClick={() => setEditing(true)}
|
onClick={() => setEditing(true)}
|
||||||
>
|
>
|
||||||
{props.buttonText}
|
{props.buttonText}
|
||||||
|
|
|
@ -18,6 +18,16 @@ export const createFold = cloudFunction<
|
||||||
|
|
||||||
export const placeBet = cloudFunction('placeBet')
|
export const placeBet = cloudFunction('placeBet')
|
||||||
|
|
||||||
|
export const createAnswer = cloudFunction<
|
||||||
|
{ contractId: string; text: string; amount: number },
|
||||||
|
{
|
||||||
|
status: 'error' | 'success'
|
||||||
|
message?: string
|
||||||
|
answerId?: string
|
||||||
|
betId?: string
|
||||||
|
}
|
||||||
|
>('createAnswer')
|
||||||
|
|
||||||
export const resolveMarket = cloudFunction('resolveMarket')
|
export const resolveMarket = cloudFunction('resolveMarket')
|
||||||
|
|
||||||
export const sellBet = cloudFunction('sellBet')
|
export const sellBet = cloudFunction('sellBet')
|
||||||
|
|
|
@ -49,7 +49,7 @@ export function getBinaryProbPercent(contract: Contract) {
|
||||||
return probPercent
|
return probPercent
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tradingAllowed(contract: Contract) {
|
export function tradingAllowed(contract: Contract<'BINARY' | 'MULTI'>) {
|
||||||
return (
|
return (
|
||||||
!contract.isResolved &&
|
!contract.isResolved &&
|
||||||
(!contract.closeTime || contract.closeTime > Date.now())
|
(!contract.closeTime || contract.closeTime > Date.now())
|
||||||
|
@ -84,7 +84,9 @@ export async function getContractFromSlug(slug: string) {
|
||||||
const q = query(contractCollection, where('slug', '==', slug))
|
const q = query(contractCollection, where('slug', '==', slug))
|
||||||
const snapshot = await getDocs(q)
|
const snapshot = await getDocs(q)
|
||||||
|
|
||||||
return snapshot.empty ? undefined : (snapshot.docs[0].data() as Contract)
|
return snapshot.empty
|
||||||
|
? undefined
|
||||||
|
: (snapshot.docs[0].data() as Contract<'BINARY' | 'MULTI'>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteContract(contractId: string) {
|
export async function deleteContract(contractId: string) {
|
||||||
|
|
|
@ -26,6 +26,9 @@ import Custom404 from '../404'
|
||||||
import { getFoldsByTags } from '../../lib/firebase/folds'
|
import { getFoldsByTags } from '../../lib/firebase/folds'
|
||||||
import { Fold } from '../../../common/fold'
|
import { Fold } from '../../../common/fold'
|
||||||
import { useFoldsWithTags } from '../../hooks/use-fold'
|
import { useFoldsWithTags } from '../../hooks/use-fold'
|
||||||
|
import { listAllAnswers } from '../../lib/firebase/answers'
|
||||||
|
import { Answer } from '../../../common/answer'
|
||||||
|
import { AnswersPanel } from '../../components/answers-panel'
|
||||||
|
|
||||||
export async function getStaticProps(props: {
|
export async function getStaticProps(props: {
|
||||||
params: { username: string; contractSlug: string }
|
params: { username: string; contractSlug: string }
|
||||||
|
@ -36,9 +39,12 @@ export async function getStaticProps(props: {
|
||||||
|
|
||||||
const foldsPromise = getFoldsByTags(contract?.tags ?? [])
|
const foldsPromise = getFoldsByTags(contract?.tags ?? [])
|
||||||
|
|
||||||
const [bets, comments] = await Promise.all([
|
const [bets, comments, answers] = await Promise.all([
|
||||||
contractId ? listAllBets(contractId) : [],
|
contractId ? listAllBets(contractId) : [],
|
||||||
contractId ? listAllComments(contractId) : [],
|
contractId ? listAllComments(contractId) : [],
|
||||||
|
contractId && contract.outcomes === 'FREE_ANSWER'
|
||||||
|
? listAllAnswers(contractId)
|
||||||
|
: [],
|
||||||
])
|
])
|
||||||
|
|
||||||
const folds = await foldsPromise
|
const folds = await foldsPromise
|
||||||
|
@ -50,6 +56,7 @@ export async function getStaticProps(props: {
|
||||||
slug: contractSlug,
|
slug: contractSlug,
|
||||||
bets,
|
bets,
|
||||||
comments,
|
comments,
|
||||||
|
answers,
|
||||||
folds,
|
folds,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -66,6 +73,7 @@ export default function ContractPage(props: {
|
||||||
username: string
|
username: string
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
|
answers: Answer[]
|
||||||
slug: string
|
slug: string
|
||||||
folds: Fold[]
|
folds: Fold[]
|
||||||
}) {
|
}) {
|
||||||
|
@ -112,6 +120,10 @@ export default function ContractPage(props: {
|
||||||
comments={comments ?? []}
|
comments={comments ?? []}
|
||||||
folds={folds}
|
folds={folds}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{contract.outcomes === 'FREE_ANSWER' && (
|
||||||
|
<AnswersPanel contract={contract as any} answers={props.answers} />
|
||||||
|
)}
|
||||||
<BetsSection contract={contract} user={user ?? null} bets={bets} />
|
<BetsSection contract={contract} user={user ?? null} bets={bets} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -140,6 +152,7 @@ function BetsSection(props: {
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
}) {
|
}) {
|
||||||
const { contract, user } = props
|
const { contract, user } = props
|
||||||
|
const isBinary = contract.outcomeType === 'BINARY'
|
||||||
const bets = useBets(contract.id) ?? props.bets
|
const bets = useBets(contract.id) ?? props.bets
|
||||||
|
|
||||||
// Decending creation time.
|
// Decending creation time.
|
||||||
|
@ -152,6 +165,8 @@ function BetsSection(props: {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title className="px-2" text="Your trades" />
|
<Title className="px-2" text="Your trades" />
|
||||||
|
{isBinary && (
|
||||||
|
<>
|
||||||
<MyBetsSummary
|
<MyBetsSummary
|
||||||
className="px-2"
|
className="px-2"
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -159,6 +174,8 @@ function BetsSection(props: {
|
||||||
showMKT
|
showMKT
|
||||||
/>
|
/>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<ContractBetsTable contract={contract} bets={userBets} />
|
<ContractBetsTable contract={contract} bets={userBets} />
|
||||||
<Spacer h={12} />
|
<Spacer h={12} />
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user