Merge branch 'main' into stripe
This commit is contained in:
commit
b4a22ba4a1
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"projects": {
|
||||
"default": "mantic-markets"
|
||||
"default": "mantic-markets",
|
||||
"prod": "mantic-markets",
|
||||
"dev": "dev-mantic-markets"
|
||||
}
|
||||
}
|
||||
}
|
34
firestore.rules
Normal file
34
firestore.rules
Normal file
|
@ -0,0 +1,34 @@
|
|||
rules_version = '2';
|
||||
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
|
||||
match /users/{userId} {
|
||||
allow read;
|
||||
allow create: if request.auth != null;
|
||||
}
|
||||
|
||||
match /contracts/{contractId} {
|
||||
allow read;
|
||||
allow delete: if resource.data.creatorId == request.auth.uid;
|
||||
}
|
||||
|
||||
match /contracts/{contractId}/bets/{betId} {
|
||||
allow read;
|
||||
}
|
||||
|
||||
match /{somePath=**}/bets/{betId} {
|
||||
allow read;
|
||||
}
|
||||
|
||||
match /contracts/{contractId}/comments/{commentId} {
|
||||
allow read;
|
||||
allow create: if request.auth != null;
|
||||
}
|
||||
|
||||
match /{somePath=**}/comments/{commentId} {
|
||||
allow read;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
"name": "functions",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
"serve": "yarn build && firebase emulators:start --only functions",
|
||||
"shell": "yarn build && firebase functions:shell",
|
||||
"start": "yarn shell",
|
||||
|
@ -17,9 +18,11 @@
|
|||
"firebase-admin": "10.0.0",
|
||||
"firebase-functions": "3.16.0",
|
||||
"lodash": "4.17.21",
|
||||
"mailgun-js": "0.22.0",
|
||||
"stripe": "8.194.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mailgun-js": "0.22.12",
|
||||
"firebase-functions-test": "0.3.3",
|
||||
"typescript": "4.5.3"
|
||||
},
|
||||
|
|
152
functions/src/create-contract.ts
Normal file
152
functions/src/create-contract.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { randomString } from './util/random-string'
|
||||
import { slugify } from './util/slugify'
|
||||
import { Contract } from './types/contract'
|
||||
import { getUser } from './utils'
|
||||
import { payUser } from '.'
|
||||
import { User } from './types/user'
|
||||
|
||||
export const createContract = functions
|
||||
.runWith({ minInstances: 1 })
|
||||
.https.onCall(
|
||||
async (
|
||||
data: {
|
||||
question: string
|
||||
description: string
|
||||
initialProb: number
|
||||
ante?: number
|
||||
closeTime?: number
|
||||
},
|
||||
context
|
||||
) => {
|
||||
const userId = context?.auth?.uid
|
||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||
|
||||
const creator = await getUser(userId)
|
||||
if (!creator) return { status: 'error', message: 'User not found' }
|
||||
|
||||
const { question, description, initialProb, ante, closeTime } = data
|
||||
|
||||
if (!question || !initialProb)
|
||||
return { status: 'error', message: 'Missing contract attributes' }
|
||||
|
||||
if (ante !== undefined && (ante < 0 || ante > creator.balance))
|
||||
return { status: 'error', message: 'Invalid ante' }
|
||||
|
||||
console.log(
|
||||
'creating contract for',
|
||||
creator.username,
|
||||
'on',
|
||||
question,
|
||||
'ante:',
|
||||
ante || 0
|
||||
)
|
||||
|
||||
const slug = await getSlug(question)
|
||||
|
||||
const contractRef = firestore.collection('contracts').doc()
|
||||
|
||||
const contract = getNewContract(
|
||||
contractRef.id,
|
||||
slug,
|
||||
creator,
|
||||
question,
|
||||
description,
|
||||
initialProb,
|
||||
ante,
|
||||
closeTime
|
||||
)
|
||||
|
||||
if (ante) await payUser([creator.id, -ante])
|
||||
|
||||
await contractRef.create(contract)
|
||||
return { status: 'success', contract }
|
||||
}
|
||||
)
|
||||
|
||||
const getSlug = async (question: string) => {
|
||||
const proposedSlug = slugify(question).substring(0, 35)
|
||||
|
||||
const preexistingContract = await getContractFromSlug(proposedSlug)
|
||||
|
||||
return preexistingContract
|
||||
? proposedSlug + '-' + randomString()
|
||||
: proposedSlug
|
||||
}
|
||||
|
||||
function getNewContract(
|
||||
id: string,
|
||||
slug: string,
|
||||
creator: User,
|
||||
question: string,
|
||||
description: string,
|
||||
initialProb: number,
|
||||
ante?: number,
|
||||
closeTime?: number
|
||||
) {
|
||||
const { startYes, startNo, poolYes, poolNo } = calcStartPool(
|
||||
initialProb,
|
||||
ante
|
||||
)
|
||||
|
||||
const contract: Contract = {
|
||||
id,
|
||||
slug,
|
||||
outcomeType: 'BINARY',
|
||||
|
||||
creatorId: creator.id,
|
||||
creatorName: creator.name,
|
||||
creatorUsername: creator.username,
|
||||
|
||||
question: question.trim(),
|
||||
description: description.trim(),
|
||||
|
||||
startPool: { YES: startYes, NO: startNo },
|
||||
pool: { YES: poolYes, NO: poolNo },
|
||||
totalShares: { YES: 0, NO: 0 },
|
||||
totalBets: { YES: 0, NO: 0 },
|
||||
isResolved: false,
|
||||
|
||||
createdTime: Date.now(),
|
||||
lastUpdatedTime: Date.now(),
|
||||
}
|
||||
|
||||
if (closeTime) contract.closeTime = closeTime
|
||||
|
||||
return contract
|
||||
}
|
||||
|
||||
const calcStartPool = (
|
||||
initialProbInt: number,
|
||||
ante?: number,
|
||||
phantomAnte = 200
|
||||
) => {
|
||||
const p = initialProbInt / 100.0
|
||||
const totalAnte = phantomAnte + (ante || 0)
|
||||
|
||||
const poolYes =
|
||||
p === 0.5
|
||||
? p * totalAnte
|
||||
: -(totalAnte * (-p + Math.sqrt((-1 + p) * -p))) / (-1 + 2 * p)
|
||||
|
||||
const poolNo = totalAnte - poolYes
|
||||
|
||||
const f = phantomAnte / totalAnte
|
||||
const startYes = f * poolYes
|
||||
const startNo = f * poolNo
|
||||
|
||||
return { startYes, startNo, poolYes, poolNo }
|
||||
}
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export async function getContractFromSlug(slug: string) {
|
||||
const snap = await firestore
|
||||
.collection('contracts')
|
||||
.where('slug', '==', slug)
|
||||
.get()
|
||||
|
||||
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
|
||||
}
|
34
functions/src/emails.ts
Normal file
34
functions/src/emails.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { sendEmail } from './send-email'
|
||||
import { Contract } from './types/contract'
|
||||
import { User } from './types/user'
|
||||
import { getUser } from './utils'
|
||||
|
||||
export const sendMarketResolutionEmail = async (
|
||||
userId: string,
|
||||
payout: number,
|
||||
creator: User,
|
||||
contract: Contract,
|
||||
resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
||||
) => {
|
||||
const user = await getUser(userId)
|
||||
if (!user) return
|
||||
|
||||
const subject = `Resolved ${toDisplayResolution[resolution]}: ${contract.question}`
|
||||
|
||||
const body = `Dear ${user.name},
|
||||
|
||||
A market you bet in has been resolved!
|
||||
|
||||
Creator: ${contract.creatorName}
|
||||
Question: ${contract.question}
|
||||
Resolution: ${toDisplayResolution[resolution]}
|
||||
|
||||
Your payout is M$ ${Math.round(payout)}
|
||||
|
||||
View the market here:
|
||||
https://manifold.markets/${creator.username}/${contract.slug}
|
||||
`
|
||||
await sendEmail(user.email, subject, body)
|
||||
}
|
||||
|
||||
const toDisplayResolution = { YES: 'YES', NO: 'NO', CANCEL: 'N/A', MKT: 'MKT' }
|
|
@ -2,7 +2,9 @@ import * as admin from 'firebase-admin'
|
|||
|
||||
admin.initializeApp()
|
||||
|
||||
export * from './keep-awake'
|
||||
// export * from './keep-awake'
|
||||
export * from './place-bet'
|
||||
export * from './resolve-market'
|
||||
export * from './stripe'
|
||||
export * from './sell-bet'
|
||||
export * from './create-contract'
|
||||
|
|
|
@ -8,6 +8,7 @@ export const keepAwake = functions.pubsub
|
|||
await Promise.all([
|
||||
callCloudFunction('placeBet'),
|
||||
callCloudFunction('resolveMarket'),
|
||||
callCloudFunction('sellBet'),
|
||||
])
|
||||
|
||||
await sleep(30)
|
||||
|
@ -15,6 +16,7 @@ export const keepAwake = functions.pubsub
|
|||
await Promise.all([
|
||||
callCloudFunction('placeBet'),
|
||||
callCloudFunction('resolveMarket'),
|
||||
callCloudFunction('sellBet'),
|
||||
])
|
||||
})
|
||||
|
||||
|
|
|
@ -43,22 +43,18 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
.collection(`contracts/${contractId}/bets`)
|
||||
.doc()
|
||||
|
||||
const { newBet, newPool, newDpmWeights, newBalance } = getNewBetInfo(
|
||||
user,
|
||||
outcome,
|
||||
amount,
|
||||
contract,
|
||||
newBetDoc.id
|
||||
)
|
||||
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
|
||||
getNewBetInfo(user, outcome, amount, contract, newBetDoc.id)
|
||||
|
||||
transaction.create(newBetDoc, newBet)
|
||||
transaction.update(contractDoc, {
|
||||
pool: newPool,
|
||||
dpmWeights: newDpmWeights,
|
||||
totalShares: newTotalShares,
|
||||
totalBets: newTotalBets,
|
||||
})
|
||||
transaction.update(userDoc, { balance: newBalance })
|
||||
|
||||
return { status: 'success' }
|
||||
return { status: 'success', betId: newBetDoc.id }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -79,29 +75,26 @@ const getNewBetInfo = (
|
|||
? { YES: yesPool + amount, NO: noPool }
|
||||
: { YES: yesPool, NO: noPool + amount }
|
||||
|
||||
const dpmWeight =
|
||||
const shares =
|
||||
outcome === 'YES'
|
||||
? (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool)
|
||||
: (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool)
|
||||
? amount + (amount * noPool ** 2) / (yesPool ** 2 + amount * yesPool)
|
||||
: amount + (amount * yesPool ** 2) / (noPool ** 2 + amount * noPool)
|
||||
|
||||
const { YES: yesWeight, NO: noWeight } = contract.dpmWeights || {
|
||||
YES: 0,
|
||||
NO: 0,
|
||||
} // only nesc for old contracts
|
||||
const { YES: yesShares, NO: noShares } = contract.totalShares
|
||||
|
||||
const newDpmWeights =
|
||||
const newTotalShares =
|
||||
outcome === 'YES'
|
||||
? { YES: yesWeight + dpmWeight, NO: noWeight }
|
||||
: { YES: yesWeight, NO: noWeight + dpmWeight }
|
||||
? { YES: yesShares + shares, NO: noShares }
|
||||
: { YES: yesShares, NO: noShares + shares }
|
||||
|
||||
const { YES: yesBets, NO: noBets } = contract.totalBets
|
||||
|
||||
const newTotalBets =
|
||||
outcome === 'YES'
|
||||
? { YES: yesBets + amount, NO: noBets }
|
||||
: { YES: yesBets, NO: noBets + amount }
|
||||
|
||||
const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2)
|
||||
|
||||
const probAverage =
|
||||
(amount +
|
||||
noPool * Math.atan(yesPool / noPool) -
|
||||
noPool * Math.atan((amount + yesPool) / noPool)) /
|
||||
amount
|
||||
|
||||
const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2)
|
||||
|
||||
const newBet: Bet = {
|
||||
|
@ -109,15 +102,14 @@ const getNewBetInfo = (
|
|||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
dpmWeight,
|
||||
shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
probAverage,
|
||||
probAfter,
|
||||
createdTime: Date.now(),
|
||||
}
|
||||
|
||||
const newBalance = user.balance - amount
|
||||
|
||||
return { newBet, newPool, newDpmWeights, newBalance }
|
||||
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import * as _ from 'lodash'
|
|||
import { Contract } from './types/contract'
|
||||
import { User } from './types/user'
|
||||
import { Bet } from './types/bet'
|
||||
import { getUser } from './utils'
|
||||
import { sendMarketResolutionEmail } from './emails'
|
||||
|
||||
export const PLATFORM_FEE = 0.01 // 1%
|
||||
export const CREATOR_FEE = 0.01 // 1%
|
||||
|
@ -14,7 +16,7 @@ export const resolveMarket = functions
|
|||
.https.onCall(
|
||||
async (
|
||||
data: {
|
||||
outcome: string
|
||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
||||
contractId: string
|
||||
},
|
||||
context
|
||||
|
@ -24,7 +26,7 @@ export const resolveMarket = functions
|
|||
|
||||
const { outcome, contractId } = data
|
||||
|
||||
if (!['YES', 'NO', 'CANCEL'].includes(outcome))
|
||||
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
|
||||
return { status: 'error', message: 'Invalid outcome' }
|
||||
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
|
@ -39,6 +41,9 @@ export const resolveMarket = functions
|
|||
if (contract.resolution)
|
||||
return { status: 'error', message: 'Contract already resolved' }
|
||||
|
||||
const creator = await getUser(contract.creatorId)
|
||||
if (!creator) return { status: 'error', message: 'Creator not found' }
|
||||
|
||||
await contractDoc.update({
|
||||
isResolved: true,
|
||||
resolution: outcome,
|
||||
|
@ -50,15 +55,19 @@ export const resolveMarket = functions
|
|||
const betsSnap = await firestore
|
||||
.collection(`contracts/${contractId}/bets`)
|
||||
.get()
|
||||
|
||||
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||
|
||||
const startPool = contract.startPool.YES + contract.startPool.NO
|
||||
const truePool = contract.pool.YES + contract.pool.NO - startPool
|
||||
|
||||
const payouts =
|
||||
outcome === 'CANCEL'
|
||||
? bets.map((bet) => ({
|
||||
userId: bet.userId,
|
||||
payout: bet.amount,
|
||||
}))
|
||||
: getPayouts(outcome, contract, bets)
|
||||
? getCancelPayouts(truePool, openBets)
|
||||
: outcome === 'MKT'
|
||||
? getMktPayouts(truePool, contract, openBets)
|
||||
: getStandardPayouts(outcome, truePool, contract, openBets)
|
||||
|
||||
console.log('payouts:', payouts)
|
||||
|
||||
|
@ -69,31 +78,90 @@ export const resolveMarket = functions
|
|||
|
||||
const payoutPromises = Object.entries(userPayouts).map(payUser)
|
||||
|
||||
return await Promise.all(payoutPromises)
|
||||
const result = await Promise.all(payoutPromises)
|
||||
.catch((e) => ({ status: 'error', message: e }))
|
||||
.then(() => ({ status: 'success' }))
|
||||
|
||||
await sendResolutionEmails(
|
||||
openBets,
|
||||
userPayouts,
|
||||
creator,
|
||||
contract,
|
||||
outcome
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
)
|
||||
|
||||
const sendResolutionEmails = async (
|
||||
openBets: Bet[],
|
||||
userPayouts: { [userId: string]: number },
|
||||
creator: User,
|
||||
contract: Contract,
|
||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
||||
) => {
|
||||
const nonWinners = _.difference(
|
||||
_.uniq(openBets.map(({ userId }) => userId)),
|
||||
Object.keys(userPayouts)
|
||||
)
|
||||
const emailPayouts = [
|
||||
...Object.entries(userPayouts),
|
||||
...nonWinners.map((userId) => [userId, 0] as const),
|
||||
]
|
||||
await Promise.all(
|
||||
emailPayouts.map(([userId, payout]) =>
|
||||
sendMarketResolutionEmail(userId, payout, creator, contract, outcome)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
const getPayouts = (outcome: string, contract: Contract, bets: Bet[]) => {
|
||||
const getCancelPayouts = (truePool: number, bets: Bet[]) => {
|
||||
console.log('resolved N/A, pool M$', truePool)
|
||||
|
||||
const betSum = _.sumBy(bets, (b) => b.amount)
|
||||
|
||||
return bets.map((bet) => ({
|
||||
userId: bet.userId,
|
||||
payout: (bet.amount / betSum) * truePool,
|
||||
}))
|
||||
}
|
||||
|
||||
const getStandardPayouts = (
|
||||
outcome: string,
|
||||
truePool: number,
|
||||
contract: Contract,
|
||||
bets: Bet[]
|
||||
) => {
|
||||
const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES')
|
||||
const winningBets = outcome === 'YES' ? yesBets : noBets
|
||||
|
||||
const [pool, winningBets] =
|
||||
outcome === 'YES'
|
||||
? [contract.pool.NO - contract.startPool.NO, yesBets]
|
||||
: [contract.pool.YES - contract.startPool.YES, noBets]
|
||||
const betSum = _.sumBy(winningBets, (b) => b.amount)
|
||||
|
||||
const finalPool = (1 - PLATFORM_FEE - CREATOR_FEE) * pool
|
||||
const creatorPayout = CREATOR_FEE * pool
|
||||
console.log('final pool:', finalPool, 'creator fee:', creatorPayout)
|
||||
if (betSum >= truePool) return getCancelPayouts(truePool, winningBets)
|
||||
|
||||
const sumWeights = _.sumBy(winningBets, (bet) => bet.dpmWeight)
|
||||
const creatorPayout = CREATOR_FEE * truePool
|
||||
console.log(
|
||||
'resolved',
|
||||
outcome,
|
||||
'pool: M$',
|
||||
truePool,
|
||||
'creator fee: M$',
|
||||
creatorPayout
|
||||
)
|
||||
|
||||
const shareDifferenceSum = _.sumBy(winningBets, (b) => b.shares - b.amount)
|
||||
|
||||
const winningsPool = truePool - betSum
|
||||
|
||||
const winnerPayouts = winningBets.map((bet) => ({
|
||||
userId: bet.userId,
|
||||
payout: bet.amount + (bet.dpmWeight / sumWeights) * finalPool,
|
||||
payout:
|
||||
(1 - fees) *
|
||||
(bet.amount +
|
||||
((bet.shares - bet.amount) / shareDifferenceSum) * winningsPool),
|
||||
}))
|
||||
|
||||
return winnerPayouts.concat([
|
||||
|
@ -101,6 +169,59 @@ const getPayouts = (outcome: string, contract: Contract, bets: Bet[]) => {
|
|||
]) // add creator fee
|
||||
}
|
||||
|
||||
const getMktPayouts = (truePool: number, contract: Contract, bets: Bet[]) => {
|
||||
const p =
|
||||
contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2)
|
||||
console.log('Resolved MKT at p=', p, 'pool: $M', truePool)
|
||||
|
||||
const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES')
|
||||
|
||||
const weightedBetTotal =
|
||||
p * _.sumBy(yesBets, (b) => b.amount) +
|
||||
(1 - p) * _.sumBy(noBets, (b) => b.amount)
|
||||
|
||||
if (weightedBetTotal >= truePool) {
|
||||
return bets.map((bet) => ({
|
||||
userId: bet.userId,
|
||||
payout:
|
||||
(((bet.outcome === 'YES' ? p : 1 - p) * bet.amount) /
|
||||
weightedBetTotal) *
|
||||
truePool,
|
||||
}))
|
||||
}
|
||||
|
||||
const winningsPool = truePool - weightedBetTotal
|
||||
|
||||
const weightedShareTotal =
|
||||
p * _.sumBy(yesBets, (b) => b.shares - b.amount) +
|
||||
(1 - p) * _.sumBy(noBets, (b) => b.shares - b.amount)
|
||||
|
||||
const yesPayouts = yesBets.map((bet) => ({
|
||||
userId: bet.userId,
|
||||
payout:
|
||||
(1 - fees) *
|
||||
(p * bet.amount +
|
||||
((p * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool),
|
||||
}))
|
||||
|
||||
const noPayouts = noBets.map((bet) => ({
|
||||
userId: bet.userId,
|
||||
payout:
|
||||
(1 - fees) *
|
||||
((1 - p) * bet.amount +
|
||||
(((1 - p) * (bet.shares - bet.amount)) / weightedShareTotal) *
|
||||
winningsPool),
|
||||
}))
|
||||
|
||||
const creatorPayout = CREATOR_FEE * truePool
|
||||
|
||||
return [
|
||||
...yesPayouts,
|
||||
...noPayouts,
|
||||
{ userId: contract.creatorId, payout: creatorPayout },
|
||||
]
|
||||
}
|
||||
|
||||
export const payUser = ([userId, payout]: [string, number]) => {
|
||||
return firestore.runTransaction(async (transaction) => {
|
||||
const userDoc = firestore.doc(`users/${userId}`)
|
||||
|
@ -112,3 +233,5 @@ export const payUser = ([userId, payout]: [string, number]) => {
|
|||
transaction.update(userDoc, { balance: newUserBalance })
|
||||
})
|
||||
}
|
||||
|
||||
const fees = PLATFORM_FEE + CREATOR_FEE
|
||||
|
|
58
functions/src/scripts/migrate-contract.ts
Normal file
58
functions/src/scripts/migrate-contract.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import * as _ from 'lodash'
|
||||
import { Bet } from '../types/bet'
|
||||
import { Contract } from '../types/contract'
|
||||
|
||||
type DocRef = admin.firestore.DocumentReference
|
||||
|
||||
// Generate your own private key, and set the path below:
|
||||
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
||||
const serviceAccount = require('../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json')
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
})
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function migrateBet(contractRef: DocRef, bet: Bet) {
|
||||
const { dpmWeight, amount, id } = bet as Bet & { dpmWeight: number }
|
||||
const shares = dpmWeight + amount
|
||||
|
||||
await contractRef.collection('bets').doc(id).update({ shares })
|
||||
}
|
||||
|
||||
async function migrateContract(contractRef: DocRef, contract: Contract) {
|
||||
const bets = await contractRef
|
||||
.collection('bets')
|
||||
.get()
|
||||
.then((snap) => snap.docs.map((bet) => bet.data() as Bet))
|
||||
|
||||
const totalShares = {
|
||||
YES: _.sumBy(bets, (bet) => (bet.outcome === 'YES' ? bet.shares : 0)),
|
||||
NO: _.sumBy(bets, (bet) => (bet.outcome === 'NO' ? bet.shares : 0)),
|
||||
}
|
||||
|
||||
await contractRef.update({ totalShares })
|
||||
}
|
||||
|
||||
async function migrateContracts() {
|
||||
console.log('Migrating contracts')
|
||||
|
||||
const snapshot = await firestore.collection('contracts').get()
|
||||
const contracts = snapshot.docs.map((doc) => doc.data() as Contract)
|
||||
|
||||
console.log('Loaded contracts', contracts.length)
|
||||
|
||||
for (const contract of contracts) {
|
||||
const contractRef = firestore.doc(`contracts/${contract.id}`)
|
||||
const betsSnapshot = await contractRef.collection('bets').get()
|
||||
const bets = betsSnapshot.docs.map((bet) => bet.data() as Bet)
|
||||
|
||||
console.log('contract', contract.question, 'bets', bets.length)
|
||||
|
||||
for (const bet of bets) await migrateBet(contractRef, bet)
|
||||
await migrateContract(contractRef, contract)
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) migrateContracts().then(() => process.exit())
|
62
functions/src/scripts/recalculate-contract-totals.ts
Normal file
62
functions/src/scripts/recalculate-contract-totals.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import * as _ from 'lodash'
|
||||
import { Bet } from '../types/bet'
|
||||
import { Contract } from '../types/contract'
|
||||
|
||||
type DocRef = admin.firestore.DocumentReference
|
||||
|
||||
// Generate your own private key, and set the path below:
|
||||
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
||||
const serviceAccount = require('../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
})
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function recalculateContract(contractRef: DocRef, contract: Contract) {
|
||||
const bets = await contractRef
|
||||
.collection('bets')
|
||||
.get()
|
||||
.then((snap) => snap.docs.map((bet) => bet.data() as Bet))
|
||||
|
||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||
|
||||
const totalShares = {
|
||||
YES: _.sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.shares : 0)),
|
||||
NO: _.sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.shares : 0)),
|
||||
}
|
||||
|
||||
const totalBets = {
|
||||
YES: _.sumBy(openBets, (bet) => (bet.outcome === 'YES' ? bet.amount : 0)),
|
||||
NO: _.sumBy(openBets, (bet) => (bet.outcome === 'NO' ? bet.amount : 0)),
|
||||
}
|
||||
|
||||
await contractRef.update({ totalShares, totalBets })
|
||||
|
||||
console.log(
|
||||
'calculating totals for "',
|
||||
contract.question,
|
||||
'" total bets:',
|
||||
totalBets
|
||||
)
|
||||
console.log()
|
||||
}
|
||||
|
||||
async function recalculateContractTotals() {
|
||||
console.log('Recalculating contract info')
|
||||
|
||||
const snapshot = await firestore.collection('contracts').get()
|
||||
const contracts = snapshot.docs.map((doc) => doc.data() as Contract)
|
||||
|
||||
console.log('Loaded', contracts.length, 'contracts')
|
||||
|
||||
for (const contract of contracts) {
|
||||
const contractRef = firestore.doc(`contracts/${contract.id}`)
|
||||
|
||||
await recalculateContract(contractRef, contract)
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module)
|
||||
recalculateContractTotals().then(() => process.exit())
|
46
functions/src/scripts/rename-user-contracts.ts
Normal file
46
functions/src/scripts/rename-user-contracts.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import * as _ from 'lodash'
|
||||
import { Contract } from '../types/contract'
|
||||
import { getValues } from '../utils'
|
||||
|
||||
// Generate your own private key, and set the path below:
|
||||
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
||||
// James:
|
||||
const serviceAccount = require('../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json')
|
||||
// Stephen:
|
||||
// const serviceAccount = require('../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
})
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function renameUserContracts(
|
||||
username: string,
|
||||
newNames: { name: string; username: string }
|
||||
) {
|
||||
console.log(`Renaming contracts of ${username} to`, newNames)
|
||||
|
||||
const contracts = await getValues<Contract>(
|
||||
firestore.collection('contracts').where('creatorUsername', '==', username)
|
||||
)
|
||||
|
||||
console.log('Loaded', contracts.length, 'contracts by', username)
|
||||
|
||||
for (const contract of contracts) {
|
||||
const contractRef = firestore.doc(`contracts/${contract.id}`)
|
||||
|
||||
console.log('Renaming', contract.slug)
|
||||
|
||||
await contractRef.update({
|
||||
creatorUsername: newNames.username,
|
||||
creatorName: newNames.name,
|
||||
} as Partial<Contract>)
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module)
|
||||
renameUserContracts('ManticMarkets', {
|
||||
username: 'ManifoldMarkets',
|
||||
name: 'Manifold Markets',
|
||||
}).then(() => process.exit())
|
182
functions/src/sell-bet.ts
Normal file
182
functions/src/sell-bet.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import * as functions from 'firebase-functions'
|
||||
|
||||
import { CREATOR_FEE, PLATFORM_FEE } from './resolve-market'
|
||||
import { Bet } from './types/bet'
|
||||
import { Contract } from './types/contract'
|
||||
import { User } from './types/user'
|
||||
|
||||
export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||
async (
|
||||
data: {
|
||||
contractId: string
|
||||
betId: string
|
||||
},
|
||||
context
|
||||
) => {
|
||||
const userId = context?.auth?.uid
|
||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||
|
||||
const { contractId, betId } = 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 Contract
|
||||
|
||||
const betDoc = firestore.doc(`contracts/${contractId}/bets/${betId}`)
|
||||
const betSnap = await transaction.get(betDoc)
|
||||
if (!betSnap.exists) return { status: 'error', message: 'Invalid bet' }
|
||||
const bet = betSnap.data() as Bet
|
||||
|
||||
if (bet.isSold) return { status: 'error', message: 'Bet already sold' }
|
||||
|
||||
const newBetDoc = firestore
|
||||
.collection(`contracts/${contractId}/bets`)
|
||||
.doc()
|
||||
|
||||
const {
|
||||
newBet,
|
||||
newPool,
|
||||
newTotalShares,
|
||||
newTotalBets,
|
||||
newBalance,
|
||||
creatorFee,
|
||||
} = getSellBetInfo(user, bet, contract, newBetDoc.id)
|
||||
|
||||
const creatorDoc = firestore.doc(`users/${contract.creatorId}`)
|
||||
const creatorSnap = await transaction.get(creatorDoc)
|
||||
if (creatorSnap.exists) {
|
||||
const creator = creatorSnap.data() as User
|
||||
const creatorNewBalance = creator.balance + creatorFee
|
||||
transaction.update(creatorDoc, { balance: creatorNewBalance })
|
||||
}
|
||||
|
||||
transaction.update(betDoc, { isSold: true })
|
||||
transaction.create(newBetDoc, newBet)
|
||||
transaction.update(contractDoc, {
|
||||
pool: newPool,
|
||||
totalShares: newTotalShares,
|
||||
totalBets: newTotalBets,
|
||||
})
|
||||
transaction.update(userDoc, { balance: newBalance })
|
||||
|
||||
return { status: 'success' }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
const getSellBetInfo = (
|
||||
user: User,
|
||||
bet: Bet,
|
||||
contract: Contract,
|
||||
newBetId: string
|
||||
) => {
|
||||
const { id: betId, amount, shares, outcome } = bet
|
||||
|
||||
const { YES: yesPool, NO: noPool } = contract.pool
|
||||
const { YES: yesStart, NO: noStart } = contract.startPool
|
||||
const { YES: yesShares, NO: noShares } = contract.totalShares
|
||||
const { YES: yesBets, NO: noBets } = contract.totalBets
|
||||
|
||||
const [y, n, s] = [yesPool, noPool, shares]
|
||||
|
||||
const shareValue =
|
||||
outcome === 'YES'
|
||||
? // https://www.wolframalpha.com/input/?i=b+%2B+%28b+n%5E2%29%2F%28y+%28-b+%2B+y%29%29+%3D+c+solve+b
|
||||
(n ** 2 +
|
||||
s * y +
|
||||
y ** 2 -
|
||||
Math.sqrt(
|
||||
n ** 4 + (s - y) ** 2 * y ** 2 + 2 * n ** 2 * y * (s + y)
|
||||
)) /
|
||||
(2 * y)
|
||||
: (y ** 2 +
|
||||
s * n +
|
||||
n ** 2 -
|
||||
Math.sqrt(
|
||||
y ** 4 + (s - n) ** 2 * n ** 2 + 2 * y ** 2 * n * (s + n)
|
||||
)) /
|
||||
(2 * n)
|
||||
|
||||
const startPool = yesStart + noStart
|
||||
const pool = yesPool + noPool - startPool
|
||||
|
||||
const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2)
|
||||
|
||||
const f = pool / (probBefore * yesShares + (1 - probBefore) * noShares)
|
||||
|
||||
const myPool = outcome === 'YES' ? yesPool - yesStart : noPool - noStart
|
||||
|
||||
const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool)
|
||||
|
||||
const newPool =
|
||||
outcome === 'YES'
|
||||
? { YES: yesPool - adjShareValue, NO: noPool }
|
||||
: { YES: yesPool, NO: noPool - adjShareValue }
|
||||
|
||||
const newTotalShares =
|
||||
outcome === 'YES'
|
||||
? { YES: yesShares - shares, NO: noShares }
|
||||
: { YES: yesShares, NO: noShares - shares }
|
||||
|
||||
const newTotalBets =
|
||||
outcome === 'YES'
|
||||
? { YES: yesBets - amount, NO: noBets }
|
||||
: { YES: yesBets, NO: noBets - amount }
|
||||
|
||||
const probAfter = newPool.YES ** 2 / (newPool.YES ** 2 + newPool.NO ** 2)
|
||||
|
||||
const creatorFee = CREATOR_FEE * adjShareValue
|
||||
const saleAmount = (1 - CREATOR_FEE - PLATFORM_FEE) * adjShareValue
|
||||
|
||||
console.log(
|
||||
'SELL M$',
|
||||
amount,
|
||||
outcome,
|
||||
'for M$',
|
||||
saleAmount,
|
||||
'M$/share:',
|
||||
f,
|
||||
'creator fee: M$',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
const newBet: Bet = {
|
||||
id: newBetId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount: -adjShareValue,
|
||||
shares: -shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
probAfter,
|
||||
createdTime: Date.now(),
|
||||
sale: {
|
||||
amount: saleAmount,
|
||||
betId,
|
||||
},
|
||||
}
|
||||
|
||||
const newBalance = user.balance + saleAmount
|
||||
|
||||
return {
|
||||
newBet,
|
||||
newPool,
|
||||
newTotalShares,
|
||||
newTotalBets,
|
||||
newBalance,
|
||||
creatorFee,
|
||||
}
|
||||
}
|
18
functions/src/send-email.ts
Normal file
18
functions/src/send-email.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import * as mailgun from 'mailgun-js'
|
||||
import * as functions from 'firebase-functions'
|
||||
|
||||
const DOMAIN = 'mg.manifold.markets'
|
||||
const mg = mailgun({ apiKey: functions.config().mailgun.key, domain: DOMAIN })
|
||||
|
||||
export const sendEmail = (to: string, subject: string, text: string) => {
|
||||
const data = {
|
||||
from: 'Manifold Markets <no-reply@manifold.markets>',
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
}
|
||||
|
||||
return mg.messages().send(data, (error, body) => {
|
||||
console.log('Sent email', error, body)
|
||||
})
|
||||
}
|
|
@ -2,11 +2,20 @@ export type Bet = {
|
|||
id: string
|
||||
userId: string
|
||||
contractId: string
|
||||
amount: number // Amount of bet
|
||||
outcome: 'YES' | 'NO' // Chosen outcome
|
||||
createdTime: number
|
||||
|
||||
amount: number // bet size; negative if SELL bet
|
||||
outcome: 'YES' | 'NO'
|
||||
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
||||
|
||||
probBefore: number
|
||||
probAverage: number
|
||||
probAfter: number
|
||||
dpmWeight: number // Dynamic Parimutuel weight
|
||||
}
|
||||
|
||||
sale?: {
|
||||
amount: number // amount user makes from sale
|
||||
betId: string // id of bet being sold
|
||||
}
|
||||
|
||||
isSold?: boolean // true if this BUY bet has been sold
|
||||
|
||||
createdTime: number
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ export type Contract = {
|
|||
|
||||
creatorId: string
|
||||
creatorName: string
|
||||
creatorUsername: string
|
||||
|
||||
question: string
|
||||
description: string // More info about what the contract is about
|
||||
|
@ -12,7 +13,8 @@ export type Contract = {
|
|||
|
||||
startPool: { YES: number; NO: number }
|
||||
pool: { YES: number; NO: number }
|
||||
dpmWeights: { YES: number; NO: number }
|
||||
totalShares: { YES: number; NO: number }
|
||||
totalBets: { YES: number; NO: number }
|
||||
|
||||
createdTime: number // Milliseconds since epoch
|
||||
lastUpdatedTime: number // If the question or description was changed
|
||||
|
|
|
@ -4,14 +4,14 @@ import { Contract } from './types/contract'
|
|||
import { User } from './types/user'
|
||||
|
||||
export const getValue = async <T>(collection: string, doc: string) => {
|
||||
const snap = await admin.firestore()
|
||||
.collection(collection)
|
||||
.doc(doc)
|
||||
.get()
|
||||
const snap = await admin.firestore().collection(collection).doc(doc).get()
|
||||
|
||||
return snap.exists
|
||||
? snap.data() as T
|
||||
: undefined
|
||||
return snap.exists ? (snap.data() as T) : undefined
|
||||
}
|
||||
|
||||
export const getValues = async <T>(query: admin.firestore.Query) => {
|
||||
const snap = await query.get()
|
||||
return snap.docs.map((doc) => doc.data() as T)
|
||||
}
|
||||
|
||||
export const getContract = (contractId: string) => {
|
||||
|
@ -20,4 +20,4 @@ export const getContract = (contractId: string) => {
|
|||
|
||||
export const getUser = (userId: string) => {
|
||||
return getValue<User>('users', userId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -301,6 +301,14 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
||||
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
|
||||
|
||||
"@types/mailgun-js@0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/mailgun-js/-/mailgun-js-0.22.12.tgz#b0dcb590b56ef3e599ab1f262882493d318e5510"
|
||||
integrity sha512-fTjuh2mOPoJF2BN0QAhE5iPOBls333KIJrmrrJHObMDuBCgEfaENLX2LH1FjKqhfuWfGotKOfPFZMuSok5Ow7g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^2.5.0"
|
||||
|
||||
"@types/mime@^1":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||
|
@ -344,6 +352,13 @@ accepts@~1.3.7:
|
|||
mime-types "~2.1.24"
|
||||
negotiator "0.6.2"
|
||||
|
||||
agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
|
||||
integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
|
||||
dependencies:
|
||||
es6-promisify "^5.0.0"
|
||||
|
||||
agent-base@6:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||
|
@ -351,6 +366,13 @@ agent-base@6:
|
|||
dependencies:
|
||||
debug "4"
|
||||
|
||||
agent-base@~4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
|
||||
integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
|
||||
dependencies:
|
||||
es6-promisify "^5.0.0"
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
|
@ -373,6 +395,13 @@ arrify@^2.0.0, arrify@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
|
||||
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
|
||||
|
||||
ast-types@0.x.x:
|
||||
version "0.14.2"
|
||||
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd"
|
||||
integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==
|
||||
dependencies:
|
||||
tslib "^2.0.1"
|
||||
|
||||
async-retry@^1.3.1, async-retry@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280"
|
||||
|
@ -380,6 +409,18 @@ async-retry@^1.3.1, async-retry@^1.3.3:
|
|||
dependencies:
|
||||
retry "0.13.1"
|
||||
|
||||
async@^2.6.1:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
|
||||
|
||||
base64-js@^1.3.0:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
|
@ -440,6 +481,11 @@ cliui@^7.0.2:
|
|||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
|
@ -452,6 +498,13 @@ color-name@~1.1.4:
|
|||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
combined-stream@^1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
compressible@^2.0.12:
|
||||
version "2.0.18"
|
||||
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
|
||||
|
@ -493,6 +546,11 @@ cookie@0.4.1:
|
|||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
|
||||
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||
|
||||
cors@^2.8.5:
|
||||
version "2.8.5"
|
||||
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
||||
|
@ -506,25 +564,63 @@ crypto-random-string@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
|
||||
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
|
||||
|
||||
data-uri-to-buffer@1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835"
|
||||
integrity sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==
|
||||
|
||||
date-and-time@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-2.0.1.tgz#bc8b72704980e8a0979bb186118d30d02059ef04"
|
||||
integrity sha512-O7Xe5dLaqvY/aF/MFWArsAM1J4j7w1CSZlPCX9uHgmb+6SbkPd8Q4YOvfvH/cZGvFlJFfHOZKxQtmMUOoZhc/w==
|
||||
|
||||
debug@2.6.9:
|
||||
debug@2, debug@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.1.1, debug@^4.3.2:
|
||||
debug@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
|
||||
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
deep-is@~0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||
|
||||
degenerator@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095"
|
||||
integrity sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=
|
||||
dependencies:
|
||||
ast-types "0.x.x"
|
||||
escodegen "1.x.x"
|
||||
esprima "3.x.x"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||
|
||||
depd@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
|
@ -600,6 +696,18 @@ ent@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
|
||||
integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
|
||||
|
||||
es6-promise@^4.0.3:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
es6-promisify@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
|
||||
integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
|
||||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
|
@ -610,6 +718,38 @@ escape-html@~1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||
|
||||
escodegen@1.x.x:
|
||||
version "1.14.3"
|
||||
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
|
||||
integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
|
||||
dependencies:
|
||||
esprima "^4.0.1"
|
||||
estraverse "^4.2.0"
|
||||
esutils "^2.0.2"
|
||||
optionator "^0.8.1"
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
esprima@3.x.x:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
|
||||
integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
|
||||
|
||||
esprima@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
estraverse@^4.2.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
|
||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
etag@~1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
|
@ -656,7 +796,7 @@ express@^4.17.1:
|
|||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
extend@^3.0.2:
|
||||
extend@^3.0.2, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
@ -666,6 +806,11 @@ fast-deep-equal@^3.1.1:
|
|||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-levenshtein@~2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53"
|
||||
|
@ -686,6 +831,11 @@ fetch@1.1.0:
|
|||
biskviit "1.0.1"
|
||||
encoding "0.1.12"
|
||||
|
||||
file-uri-to-path@1:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
|
||||
|
||||
finalhandler@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
|
||||
|
@ -734,6 +884,15 @@ firebase-functions@3.16.0:
|
|||
express "^4.17.1"
|
||||
lodash "^4.17.14"
|
||||
|
||||
form-data@^2.3.3, form-data@^2.5.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
|
||||
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
forwarded@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||
|
@ -748,6 +907,14 @@ function-bind@^1.1.1:
|
|||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
ftp@~0.3.10:
|
||||
version "0.3.10"
|
||||
resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d"
|
||||
integrity sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=
|
||||
dependencies:
|
||||
readable-stream "1.1.x"
|
||||
xregexp "2.0.0"
|
||||
|
||||
functional-red-black-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
@ -806,6 +973,18 @@ get-stream@^6.0.0:
|
|||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
|
||||
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
|
||||
|
||||
get-uri@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.4.tgz#d4937ab819e218d4cb5ae18e4f5962bef169cc6a"
|
||||
integrity sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==
|
||||
dependencies:
|
||||
data-uri-to-buffer "1"
|
||||
debug "2"
|
||||
extend "~3.0.2"
|
||||
file-uri-to-path "1"
|
||||
ftp "~0.3.10"
|
||||
readable-stream "2"
|
||||
|
||||
google-auth-library@^7.0.0, google-auth-library@^7.6.1, google-auth-library@^7.9.2:
|
||||
version "7.11.0"
|
||||
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.11.0.tgz#b63699c65037310a424128a854ba7e736704cbdb"
|
||||
|
@ -894,6 +1073,14 @@ http-parser-js@>=0.5.1:
|
|||
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.5.tgz#d7c30d5d3c90d865b4a2e870181f9d6f22ac7ac5"
|
||||
integrity sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA==
|
||||
|
||||
http-proxy-agent@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
|
||||
integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==
|
||||
dependencies:
|
||||
agent-base "4"
|
||||
debug "3.1.0"
|
||||
|
||||
http-proxy-agent@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
|
||||
|
@ -903,6 +1090,14 @@ http-proxy-agent@^5.0.0:
|
|||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
https-proxy-agent@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
|
||||
integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==
|
||||
dependencies:
|
||||
agent-base "^4.3.0"
|
||||
debug "^3.1.0"
|
||||
|
||||
https-proxy-agent@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
|
||||
|
@ -923,11 +1118,26 @@ imurmurhash@^0.1.4:
|
|||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
|
||||
|
||||
inherits@2.0.4, inherits@^2.0.3:
|
||||
inflection@~1.12.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416"
|
||||
integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=
|
||||
|
||||
inflection@~1.3.0:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.3.8.tgz#cbd160da9f75b14c3cc63578d4f396784bf3014e"
|
||||
integrity sha1-y9Fg2p91sUw8xjV41POWeEvzAU4=
|
||||
|
||||
inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ip@1.1.5, ip@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
|
||||
integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
|
@ -948,6 +1158,11 @@ is-stream-ended@^0.1.4:
|
|||
resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda"
|
||||
integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
|
||||
|
||||
is-stream@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
|
||||
|
@ -958,6 +1173,16 @@ is-typedarray@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
|
||||
|
||||
jose@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-2.0.5.tgz#29746a18d9fff7dcf9d5d2a6f62cb0c7cd27abd3"
|
||||
|
@ -1033,6 +1258,14 @@ jws@^4.0.0:
|
|||
jwa "^2.0.0"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
levn@~0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
|
||||
integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
|
||||
dependencies:
|
||||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
limiter@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2"
|
||||
|
@ -1093,6 +1326,13 @@ long@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
|
||||
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
|
||||
|
||||
lru-cache@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||
integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
|
@ -1116,6 +1356,21 @@ lru-memoizer@^2.1.4:
|
|||
lodash.clonedeep "^4.5.0"
|
||||
lru-cache "~4.0.0"
|
||||
|
||||
mailgun-js@0.22.0:
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/mailgun-js/-/mailgun-js-0.22.0.tgz#128942b5e47a364a470791608852bf68c96b3a05"
|
||||
integrity sha512-a2alg5nuTZA9Psa1pSEIEsbxr1Zrmqx4VkgGCQ30xVh0kIH7Bu57AYILo+0v8QLSdXtCyLaS+KVmdCrQo0uWFA==
|
||||
dependencies:
|
||||
async "^2.6.1"
|
||||
debug "^4.1.0"
|
||||
form-data "^2.3.3"
|
||||
inflection "~1.12.0"
|
||||
is-stream "^1.1.0"
|
||||
path-proxy "~1.0.0"
|
||||
promisify-call "^2.0.2"
|
||||
proxy-agent "^3.0.3"
|
||||
tsscmp "^1.0.6"
|
||||
|
||||
make-dir@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
|
||||
|
@ -1143,7 +1398,7 @@ mime-db@1.51.0, "mime-db@>= 1.43.0 < 2":
|
|||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
|
||||
integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==
|
||||
|
||||
mime-types@^2.0.8, mime-types@~2.1.24:
|
||||
mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.24:
|
||||
version "2.1.34"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24"
|
||||
integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==
|
||||
|
@ -1180,6 +1435,11 @@ negotiator@0.6.2:
|
|||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||
|
||||
netmask@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
|
||||
integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.6.6"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
|
||||
|
@ -1221,6 +1481,18 @@ once@^1.3.1, once@^1.4.0:
|
|||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
optionator@^0.8.1:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
||||
integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
|
||||
dependencies:
|
||||
deep-is "~0.1.3"
|
||||
fast-levenshtein "~2.0.6"
|
||||
levn "~0.3.0"
|
||||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
word-wrap "~1.2.3"
|
||||
|
||||
p-limit@^3.0.1:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
|
||||
|
@ -1228,16 +1500,65 @@ p-limit@^3.0.1:
|
|||
dependencies:
|
||||
yocto-queue "^0.1.0"
|
||||
|
||||
pac-proxy-agent@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz#115b1e58f92576cac2eba718593ca7b0e37de2ad"
|
||||
integrity sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==
|
||||
dependencies:
|
||||
agent-base "^4.2.0"
|
||||
debug "^4.1.1"
|
||||
get-uri "^2.0.0"
|
||||
http-proxy-agent "^2.1.0"
|
||||
https-proxy-agent "^3.0.0"
|
||||
pac-resolver "^3.0.0"
|
||||
raw-body "^2.2.0"
|
||||
socks-proxy-agent "^4.0.1"
|
||||
|
||||
pac-resolver@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-3.0.0.tgz#6aea30787db0a891704deb7800a722a7615a6f26"
|
||||
integrity sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
degenerator "^1.0.4"
|
||||
ip "^1.1.5"
|
||||
netmask "^1.0.6"
|
||||
thunkify "^2.1.2"
|
||||
|
||||
parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||
|
||||
path-proxy@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-proxy/-/path-proxy-1.0.0.tgz#18e8a36859fc9d2f1a53b48dee138543c020de5e"
|
||||
integrity sha1-GOijaFn8nS8aU7SN7hOFQ8Ag3l4=
|
||||
dependencies:
|
||||
inflection "~1.3.0"
|
||||
|
||||
path-to-regexp@0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
||||
|
||||
prelude-ls@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
||||
|
||||
promisify-call@^2.0.2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/promisify-call/-/promisify-call-2.0.4.tgz#d48c2d45652ccccd52801ddecbd533a6d4bd5fba"
|
||||
integrity sha1-1IwtRWUszM1SgB3ey9UzptS9X7o=
|
||||
dependencies:
|
||||
with-callback "^1.0.2"
|
||||
|
||||
proto3-json-serializer@^0.1.5:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-0.1.6.tgz#67cf3b8d5f4c8bebfc410698ad3b1ed64da39c7b"
|
||||
|
@ -1272,6 +1593,25 @@ proxy-addr@~2.0.7:
|
|||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
proxy-agent@^3.0.3:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-3.1.1.tgz#7e04e06bf36afa624a1540be247b47c970bd3014"
|
||||
integrity sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==
|
||||
dependencies:
|
||||
agent-base "^4.2.0"
|
||||
debug "4"
|
||||
http-proxy-agent "^2.1.0"
|
||||
https-proxy-agent "^3.0.0"
|
||||
lru-cache "^5.1.1"
|
||||
pac-proxy-agent "^3.0.1"
|
||||
proxy-from-env "^1.0.0"
|
||||
socks-proxy-agent "^4.0.1"
|
||||
|
||||
proxy-from-env@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
pseudomap@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
||||
|
@ -1316,7 +1656,7 @@ range-parser@~1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@2.4.2:
|
||||
raw-body@2.4.2, raw-body@^2.2.0:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32"
|
||||
integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==
|
||||
|
@ -1326,6 +1666,29 @@ raw-body@2.4.2:
|
|||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
readable-stream@1.1.x:
|
||||
version "1.1.14"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||
integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@2:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@^3.1.1:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
|
@ -1358,6 +1721,11 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
|||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
|
@ -1421,11 +1789,37 @@ signal-exit@^3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af"
|
||||
integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==
|
||||
|
||||
smart-buffer@^4.1.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
|
||||
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
|
||||
|
||||
snakeize@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d"
|
||||
integrity sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=
|
||||
|
||||
socks-proxy-agent@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386"
|
||||
integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==
|
||||
dependencies:
|
||||
agent-base "~4.2.1"
|
||||
socks "~2.3.2"
|
||||
|
||||
socks@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3"
|
||||
integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==
|
||||
dependencies:
|
||||
ip "1.1.5"
|
||||
smart-buffer "^4.1.0"
|
||||
|
||||
source-map@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
|
@ -1464,6 +1858,18 @@ string_decoder@^1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
string_decoder@~0.10.x:
|
||||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
|
@ -1495,6 +1901,11 @@ teeny-request@^7.0.0:
|
|||
stream-events "^1.0.5"
|
||||
uuid "^8.0.0"
|
||||
|
||||
thunkify@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d"
|
||||
integrity sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=
|
||||
|
||||
toidentifier@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
|
@ -1505,11 +1916,23 @@ tr46@~0.0.3:
|
|||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
|
||||
|
||||
tslib@^2.1.0:
|
||||
tslib@^2.0.1, tslib@^2.1.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||
tsscmp@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
|
||||
integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
|
||||
|
||||
type-check@~0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
|
||||
integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
|
||||
dependencies:
|
||||
prelude-ls "~1.1.2"
|
||||
|
||||
type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||
|
@ -1542,7 +1965,7 @@ unpipe@1.0.0, unpipe@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
|
||||
|
||||
util-deprecate@^1.0.1:
|
||||
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
@ -1589,6 +2012,16 @@ whatwg-url@^5.0.0:
|
|||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
with-callback@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/with-callback/-/with-callback-1.0.2.tgz#a09629b9a920028d721404fb435bdcff5c91bc21"
|
||||
integrity sha1-oJYpuakgAo1yFAT7Q1vc/1yRvCE=
|
||||
|
||||
word-wrap@~1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
|
@ -1618,6 +2051,11 @@ xdg-basedir@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
||||
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
|
||||
|
||||
xregexp@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
|
||||
integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
|
@ -1628,6 +2066,11 @@ yallist@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
|
||||
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
|
||||
|
||||
yallist@^3.0.2:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Mantic Markets web
|
||||
# Manifold Markets web
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ export function SEO(props: {
|
|||
|
||||
return (
|
||||
<Head>
|
||||
<title>{title} | Mantic Markets</title>
|
||||
<title>{title} | Manifold Markets</title>
|
||||
|
||||
<meta
|
||||
property="og:title"
|
||||
|
@ -29,7 +29,7 @@ export function SEO(props: {
|
|||
{url && (
|
||||
<meta
|
||||
property="og:url"
|
||||
content={'https://mantic.markets' + url}
|
||||
content={'https://manifold.markets' + url}
|
||||
key="url"
|
||||
/>
|
||||
)}
|
||||
|
|
38
web/components/advanced-panel.tsx
Normal file
38
web/components/advanced-panel.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import clsx from 'clsx'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function AdvancedPanel(props: { children: any }) {
|
||||
const { children } = props
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'relative collapse collapse-arrow',
|
||||
collapsed ? 'collapse-close' : 'collapse-open'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={() => setCollapsed((collapsed) => !collapsed)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="mt-4 mr-6 text-sm text-gray-400 text-right">
|
||||
Advanced
|
||||
</div>
|
||||
<div
|
||||
className="collapse-title p-0 absolute w-0 h-0 min-h-0"
|
||||
style={{
|
||||
top: -2,
|
||||
right: -15,
|
||||
color: '#9ca3af' /* gray-400 */,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="collapse-content !p-0 m-0 !bg-transparent">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { getFunctions, httpsCallable } from 'firebase/functions'
|
||||
import clsx from 'clsx'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { Contract } from '../lib/firebase/contracts'
|
||||
|
@ -8,17 +8,31 @@ import { Col } from './layout/col'
|
|||
import { Row } from './layout/row'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { YesNoSelector } from './yes-no-selector'
|
||||
import { formatMoney, formatPercent } from '../lib/util/format'
|
||||
import {
|
||||
formatMoney,
|
||||
formatPercent,
|
||||
formatWithCommas,
|
||||
} from '../lib/util/format'
|
||||
import { Title } from './title'
|
||||
import {
|
||||
getProbability,
|
||||
getDpmWeight,
|
||||
calculateShares,
|
||||
getProbabilityAfterBet,
|
||||
} from '../lib/calculation/contract'
|
||||
calculatePayoutAfterCorrectBet,
|
||||
} from '../lib/calculate'
|
||||
import { firebaseLogin } from '../lib/firebase/users'
|
||||
import { AddFundsButton } from './add-funds-button'
|
||||
import { OutcomeLabel } from './outcome-label'
|
||||
import { AdvancedPanel } from './advanced-panel'
|
||||
import { Bet } from '../lib/firebase/bets'
|
||||
import { placeBet } from '../lib/firebase/api-call'
|
||||
|
||||
export function BetPanel(props: { contract: Contract; className?: string }) {
|
||||
useEffect(() => {
|
||||
// warm up cloud function
|
||||
placeBet({}).catch()
|
||||
}, [])
|
||||
|
||||
const { contract, className } = props
|
||||
|
||||
const user = useUser()
|
||||
|
@ -85,9 +99,9 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
|||
betChoice,
|
||||
betAmount ?? 0
|
||||
)
|
||||
const dpmWeight = getDpmWeight(contract.pool, betAmount ?? 0, betChoice)
|
||||
const shares = calculateShares(contract.pool, betAmount ?? 0, betChoice)
|
||||
|
||||
const estimatedWinnings = Math.floor((betAmount ?? 0) + dpmWeight)
|
||||
const estimatedWinnings = Math.floor(shares)
|
||||
const estimatedReturn = betAmount
|
||||
? (estimatedWinnings - betAmount) / betAmount
|
||||
: 0
|
||||
|
@ -97,9 +111,9 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
|||
|
||||
return (
|
||||
<Col
|
||||
className={clsx('bg-gray-100 shadow-xl px-8 py-6 rounded-md', className)}
|
||||
className={clsx('bg-gray-100 shadow-md px-8 py-6 rounded-md', className)}
|
||||
>
|
||||
<Title className="!mt-0 whitespace-nowrap" text="Place a bet" />
|
||||
<Title className="!mt-0 whitespace-nowrap" text={`Buy ${betChoice}`} />
|
||||
|
||||
<div className="mt-2 mb-1 text-sm text-gray-400">Outcome</div>
|
||||
<YesNoSelector
|
||||
|
@ -108,7 +122,14 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
|||
onSelect={(choice) => onBetChoice(choice)}
|
||||
/>
|
||||
|
||||
<div className="mt-3 mb-1 text-sm text-gray-400">Bet amount</div>
|
||||
<div className="mt-3 mb-1 text-sm text-gray-400">
|
||||
Amount{' '}
|
||||
{user && (
|
||||
<span className="float-right">
|
||||
{formatMoney(remainingBalance > 0 ? remainingBalance : 0)} left
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Col className="my-2">
|
||||
<label className="input-group">
|
||||
<span className="text-sm bg-gray-200">M$</span>
|
||||
|
@ -153,12 +174,38 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
|||
</Row>
|
||||
|
||||
<div className="mt-2 mb-1 text-sm text-gray-400">
|
||||
Max payout (estimated)
|
||||
Estimated max payout
|
||||
</div>
|
||||
<div>
|
||||
{formatMoney(estimatedWinnings)} (+{estimatedReturnPercent})
|
||||
{formatMoney(estimatedWinnings)} {' '}
|
||||
{estimatedWinnings ? <span>(+{estimatedReturnPercent})</span> : null}
|
||||
</div>
|
||||
|
||||
<AdvancedPanel>
|
||||
<div className="mt-2 mb-1 text-sm text-gray-400">
|
||||
<OutcomeLabel outcome={betChoice} /> shares
|
||||
</div>
|
||||
<div>
|
||||
{formatWithCommas(shares)} of{' '}
|
||||
{formatWithCommas(shares + contract.totalShares[betChoice])}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 mb-1 text-sm text-gray-400">
|
||||
Current payout if <OutcomeLabel outcome={betChoice} />
|
||||
</div>
|
||||
<div>
|
||||
{formatMoney(
|
||||
betAmount
|
||||
? calculatePayoutAfterCorrectBet(contract, {
|
||||
outcome: betChoice,
|
||||
amount: betAmount,
|
||||
shares,
|
||||
} as Bet)
|
||||
: 0
|
||||
)}
|
||||
</div>
|
||||
</AdvancedPanel>
|
||||
|
||||
<Spacer h={6} />
|
||||
|
||||
{user ? (
|
||||
|
@ -174,21 +221,18 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
|
|||
)}
|
||||
onClick={betDisabled ? undefined : submitBet}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Place bet'}
|
||||
{isSubmitting ? 'Submitting...' : 'Submit trade'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn mt-4 border-none normal-case text-lg font-medium px-10 bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
|
||||
onClick={firebaseLogin}
|
||||
>
|
||||
Sign in to bet!
|
||||
Sign in to trade!
|
||||
</button>
|
||||
)}
|
||||
|
||||
{wasSubmitted && <div className="mt-4">Bet submitted!</div>}
|
||||
{wasSubmitted && <div className="mt-4">Trade submitted!</div>}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
const functions = getFunctions()
|
||||
export const placeBet = httpsCallable(functions, 'placeBet')
|
||||
|
|
|
@ -2,10 +2,16 @@ import Link from 'next/link'
|
|||
import _ from 'lodash'
|
||||
import dayjs from 'dayjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { useUserBets } from '../hooks/use-user-bets'
|
||||
import { Bet } from '../lib/firebase/bets'
|
||||
import { User } from '../lib/firebase/users'
|
||||
import { formatMoney, formatPercent } from '../lib/util/format'
|
||||
import {
|
||||
formatMoney,
|
||||
formatPercent,
|
||||
formatWithCommas,
|
||||
} from '../lib/util/format'
|
||||
import { Col } from './layout/col'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { Contract, getContractFromId, path } from '../lib/firebase/contracts'
|
||||
|
@ -13,10 +19,12 @@ import { Row } from './layout/row'
|
|||
import { UserLink } from './user-page'
|
||||
import {
|
||||
calculatePayout,
|
||||
currentValue,
|
||||
calculateSaleAmount,
|
||||
resolvedPayout,
|
||||
} from '../lib/calculation/contract'
|
||||
import clsx from 'clsx'
|
||||
} from '../lib/calculate'
|
||||
import { sellBet } from '../lib/firebase/api-call'
|
||||
import { ConfirmationButton } from './confirmation-button'
|
||||
import { OutcomeLabel, YesLabel, NoLabel, MarketLabel } from './outcome-label'
|
||||
|
||||
export function BetsList(props: { user: User }) {
|
||||
const { user } = props
|
||||
|
@ -65,19 +73,26 @@ export function BetsList(props: { user: User }) {
|
|||
contracts,
|
||||
(contract) => contract.isResolved
|
||||
)
|
||||
|
||||
const currentBets = _.sumBy(unresolved, (contract) =>
|
||||
_.sumBy(contractBets[contract.id], (bet) => bet.amount)
|
||||
_.sumBy(contractBets[contract.id], (bet) => {
|
||||
if (bet.isSold || bet.sale) return 0
|
||||
return bet.amount
|
||||
})
|
||||
)
|
||||
|
||||
const currentBetsValue = _.sumBy(unresolved, (contract) =>
|
||||
_.sumBy(contractBets[contract.id], (bet) => currentValue(contract, bet))
|
||||
_.sumBy(contractBets[contract.id], (bet) => {
|
||||
if (bet.isSold || bet.sale) return 0
|
||||
return calculatePayout(contract, bet, 'MKT')
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<Col className="mt-6 gap-6">
|
||||
<Row className="gap-8">
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500">Active bets</div>
|
||||
<div className="text-sm text-gray-500">Currently invested</div>
|
||||
<div>{formatMoney(currentBets)}</div>
|
||||
</Col>
|
||||
<Col>
|
||||
|
@ -168,41 +183,49 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
|
|||
export function MyBetsSummary(props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
showMKT?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const { bets, contract, className } = props
|
||||
const { bets, contract, showMKT, className } = props
|
||||
const { resolution } = contract
|
||||
|
||||
const betsTotal = _.sumBy(bets, (bet) => bet.amount)
|
||||
const excludeSales = bets.filter((b) => !b.isSold && !b.sale)
|
||||
const betsTotal = _.sumBy(excludeSales, (bet) => bet.amount)
|
||||
|
||||
const betsPayout = resolution
|
||||
? _.sumBy(bets, (bet) => resolvedPayout(contract, bet))
|
||||
? _.sumBy(excludeSales, (bet) => resolvedPayout(contract, bet))
|
||||
: 0
|
||||
|
||||
const yesWinnings = _.sumBy(bets, (bet) =>
|
||||
const yesWinnings = _.sumBy(excludeSales, (bet) =>
|
||||
calculatePayout(contract, bet, 'YES')
|
||||
)
|
||||
const noWinnings = _.sumBy(bets, (bet) =>
|
||||
const noWinnings = _.sumBy(excludeSales, (bet) =>
|
||||
calculatePayout(contract, bet, 'NO')
|
||||
)
|
||||
|
||||
const marketWinnings = _.sumBy(excludeSales, (bet) =>
|
||||
calculatePayout(contract, bet, 'MKT')
|
||||
)
|
||||
|
||||
return (
|
||||
<Row className={clsx('gap-4 sm:gap-6', className)}>
|
||||
<Row
|
||||
className={clsx(
|
||||
'gap-4 sm:gap-6',
|
||||
showMKT && 'flex-wrap sm:flex-nowrap',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500 whitespace-nowrap">
|
||||
Total bets
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 whitespace-nowrap">Invested</div>
|
||||
<div className="whitespace-nowrap">{formatMoney(betsTotal)}</div>
|
||||
</Col>
|
||||
{resolution ? (
|
||||
<>
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500">Payout</div>
|
||||
<div className="whitespace-nowrap">{formatMoney(betsPayout)}</div>
|
||||
</Col>
|
||||
</>
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500">Payout</div>
|
||||
<div className="whitespace-nowrap">{formatMoney(betsPayout)}</div>
|
||||
</Col>
|
||||
) : (
|
||||
<>
|
||||
<Row className="gap-4 sm:gap-6">
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500 whitespace-nowrap">
|
||||
Payout if <YesLabel />
|
||||
|
@ -215,7 +238,17 @@ export function MyBetsSummary(props: {
|
|||
</div>
|
||||
<div className="whitespace-nowrap">{formatMoney(noWinnings)}</div>
|
||||
</Col>
|
||||
</>
|
||||
{showMKT && (
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500 whitespace-nowrap">
|
||||
Payout if <MarketLabel />
|
||||
</div>
|
||||
<div className="whitespace-nowrap">
|
||||
{formatMoney(marketWinnings)}
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
|
@ -228,6 +261,11 @@ export function ContractBetsTable(props: {
|
|||
}) {
|
||||
const { contract, bets, className } = props
|
||||
|
||||
const [sales, buys] = _.partition(bets, (bet) => bet.sale)
|
||||
const salesDict = _.fromPairs(
|
||||
sales.map((sale) => [sale.sale?.betId ?? '', sale])
|
||||
)
|
||||
|
||||
const { isResolved } = contract
|
||||
|
||||
return (
|
||||
|
@ -237,15 +275,21 @@ export function ContractBetsTable(props: {
|
|||
<tr className="p-2">
|
||||
<th>Date</th>
|
||||
<th>Outcome</th>
|
||||
<th>Bet</th>
|
||||
<th>Amount</th>
|
||||
<th>Probability</th>
|
||||
{!isResolved && <th>Est. max payout</th>}
|
||||
<th>{isResolved ? <>Payout</> : <>Current value</>}</th>
|
||||
<th>Shares</th>
|
||||
<th>{isResolved ? <>Payout</> : <>Sale price</>}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bets.map((bet) => (
|
||||
<BetRow key={bet.id} bet={bet} contract={contract} />
|
||||
{buys.map((bet) => (
|
||||
<BetRow
|
||||
key={bet.id}
|
||||
bet={bet}
|
||||
sale={salesDict[bet.id]}
|
||||
contract={contract}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -253,14 +297,22 @@ export function ContractBetsTable(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function BetRow(props: { bet: Bet; contract: Contract }) {
|
||||
const { bet, contract } = props
|
||||
const { amount, outcome, createdTime, probBefore, probAfter, dpmWeight } = bet
|
||||
function BetRow(props: { bet: Bet; contract: Contract; sale?: Bet }) {
|
||||
const { bet, sale, contract } = props
|
||||
const {
|
||||
amount,
|
||||
outcome,
|
||||
createdTime,
|
||||
probBefore,
|
||||
probAfter,
|
||||
shares,
|
||||
isSold,
|
||||
} = bet
|
||||
const { isResolved } = contract
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{dayjs(createdTime).format('MMM D, H:mma')}</td>
|
||||
<td>{dayjs(createdTime).format('MMM D, h:mma')}</td>
|
||||
<td>
|
||||
<OutcomeLabel outcome={outcome} />
|
||||
</td>
|
||||
|
@ -268,34 +320,59 @@ function BetRow(props: { bet: Bet; contract: Contract }) {
|
|||
<td>
|
||||
{formatPercent(probBefore)} → {formatPercent(probAfter)}
|
||||
</td>
|
||||
{!isResolved && <td>{formatMoney(amount + dpmWeight)}</td>}
|
||||
<td>{formatWithCommas(shares)}</td>
|
||||
<td>
|
||||
{formatMoney(
|
||||
isResolved
|
||||
? resolvedPayout(contract, bet)
|
||||
: currentValue(contract, bet)
|
||||
{sale ? (
|
||||
<>{formatMoney(Math.abs(sale.amount))} (sold)</>
|
||||
) : (
|
||||
formatMoney(
|
||||
isResolved
|
||||
? resolvedPayout(contract, bet)
|
||||
: calculateSaleAmount(contract, bet)
|
||||
)
|
||||
)}
|
||||
</td>
|
||||
|
||||
{!isResolved && !isSold && (
|
||||
<td className="text-neutral">
|
||||
<SellButton contract={contract} bet={bet} />
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function OutcomeLabel(props: { outcome: 'YES' | 'NO' | 'CANCEL' }) {
|
||||
const { outcome } = props
|
||||
function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||
useEffect(() => {
|
||||
// warm up cloud function
|
||||
sellBet({}).catch()
|
||||
}, [])
|
||||
|
||||
if (outcome === 'YES') return <YesLabel />
|
||||
if (outcome === 'NO') return <NoLabel />
|
||||
return <CancelLabel />
|
||||
}
|
||||
const { contract, bet } = props
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
function YesLabel() {
|
||||
return <span className="text-primary">YES</span>
|
||||
}
|
||||
|
||||
function NoLabel() {
|
||||
return <span className="text-red-400">NO</span>
|
||||
}
|
||||
|
||||
function CancelLabel() {
|
||||
return <span className="text-yellow-400">N/A</span>
|
||||
return (
|
||||
<ConfirmationButton
|
||||
id={`sell-${bet.id}`}
|
||||
openModelBtn={{
|
||||
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
||||
label: 'Sell',
|
||||
}}
|
||||
submitBtn={{ className: 'btn-primary' }}
|
||||
onSubmit={async () => {
|
||||
setIsSubmitting(true)
|
||||
await sellBet({ contractId: contract.id, betId: bet.id })
|
||||
setIsSubmitting(false)
|
||||
}}
|
||||
>
|
||||
<div className="text-2xl mb-4">
|
||||
Sell <OutcomeLabel outcome={bet.outcome} />
|
||||
</div>
|
||||
<div>
|
||||
Do you want to sell {formatWithCommas(bet.shares)} shares of{' '}
|
||||
<OutcomeLabel outcome={bet.outcome} /> for{' '}
|
||||
{formatMoney(calculateSaleAmount(contract, bet))}?
|
||||
</div>
|
||||
</ConfirmationButton>
|
||||
)
|
||||
}
|
||||
|
|
151
web/components/contract-card.tsx
Normal file
151
web/components/contract-card.tsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import { Row } from '../components/layout/row'
|
||||
import { formatMoney } from '../lib/util/format'
|
||||
import { UserLink } from './user-page'
|
||||
import { Linkify } from './linkify'
|
||||
import { Contract, compute, path } from '../lib/firebase/contracts'
|
||||
import { Col } from './layout/col'
|
||||
import { parseTags } from '../lib/util/parse'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export function ContractCard(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const { question, resolution } = contract
|
||||
const { probPercent } = compute(contract)
|
||||
|
||||
return (
|
||||
<Link href={path(contract)}>
|
||||
<a>
|
||||
<li className="col-span-1 bg-white hover:bg-gray-100 shadow-md rounded-lg divide-y divide-gray-200">
|
||||
<div className="card">
|
||||
<div className="card-body p-6">
|
||||
<Row className="justify-between gap-4 mb-2">
|
||||
<p className="font-medium text-indigo-700">{question}</p>
|
||||
<ResolutionOrChance
|
||||
className="items-center"
|
||||
resolution={resolution}
|
||||
probPercent={probPercent}
|
||||
/>
|
||||
</Row>
|
||||
<AbbrContractDetails contract={contract} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function ResolutionOrChance(props: {
|
||||
resolution?: 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||
probPercent: string
|
||||
large?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const { resolution, probPercent, large, className } = props
|
||||
|
||||
const resolutionColor = {
|
||||
YES: 'text-primary',
|
||||
NO: 'text-red-400',
|
||||
MKT: 'text-blue-400',
|
||||
CANCEL: 'text-yellow-400',
|
||||
'': '', // Empty if unresolved
|
||||
}[resolution || '']
|
||||
|
||||
const resolutionText = {
|
||||
YES: 'YES',
|
||||
NO: 'NO',
|
||||
MKT: 'MKT',
|
||||
CANCEL: 'N/A',
|
||||
'': '',
|
||||
}[resolution || '']
|
||||
|
||||
return (
|
||||
<Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}>
|
||||
{resolution ? (
|
||||
<>
|
||||
<div
|
||||
className={clsx('text-gray-500', large ? 'text-xl' : 'text-base')}
|
||||
>
|
||||
Resolved
|
||||
</div>
|
||||
<div className={resolutionColor}>{resolutionText}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-primary">{probPercent}</div>
|
||||
<div
|
||||
className={clsx('text-primary', large ? 'text-xl' : 'text-base')}
|
||||
>
|
||||
chance
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export function AbbrContractDetails(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const { truePool } = compute(contract)
|
||||
|
||||
return (
|
||||
<Col className={clsx('text-sm text-gray-500 gap-2')}>
|
||||
<Row className="gap-2 flex-wrap">
|
||||
<div className="whitespace-nowrap">
|
||||
<UserLink username={contract.creatorUsername} />
|
||||
</div>
|
||||
<div className="">•</div>
|
||||
<div className="whitespace-nowrap">{formatMoney(truePool)} pool</div>
|
||||
</Row>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContractDetails(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const { question, description, closeTime } = contract
|
||||
const { truePool, createdDate, resolvedDate } = compute(contract)
|
||||
|
||||
const tags = parseTags(`${question} ${description}`).map((tag) => `#${tag}`)
|
||||
|
||||
return (
|
||||
<Col className="text-sm text-gray-500 gap-2 sm:flex-row sm:flex-wrap">
|
||||
<Row className="gap-2 flex-wrap">
|
||||
<div className="whitespace-nowrap">
|
||||
<UserLink username={contract.creatorUsername} />
|
||||
</div>
|
||||
<div className="">•</div>
|
||||
<div className="whitespace-nowrap">
|
||||
{resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}
|
||||
</div>
|
||||
{!resolvedDate && closeTime && (
|
||||
<>
|
||||
<div className="">•</div>
|
||||
<div className="whitespace-nowrap">
|
||||
{closeTime > Date.now() ? 'Closes' : 'Closed'}{' '}
|
||||
{dayjs(closeTime).format('MMM D, h:mma')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="">•</div>
|
||||
<div className="whitespace-nowrap">{formatMoney(truePool)} pool</div>
|
||||
</Row>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<>
|
||||
<div className="hidden sm:block">•</div>
|
||||
|
||||
<Row className="gap-2 flex-wrap">
|
||||
{tags.map((tag) => (
|
||||
<div key={tag} className="bg-gray-100 px-1">
|
||||
<Linkify text={tag} gray />
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
471
web/components/contract-feed.tsx
Normal file
471
web/components/contract-feed.tsx
Normal file
|
@ -0,0 +1,471 @@
|
|||
// From https://tailwindui.com/components/application-ui/lists/feeds
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
BanIcon,
|
||||
ChatAltIcon,
|
||||
CheckIcon,
|
||||
LockClosedIcon,
|
||||
StarIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
XIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import { useBets } from '../hooks/use-bets'
|
||||
import { Bet } from '../lib/firebase/bets'
|
||||
import { Comment, mapCommentsByBetId } from '../lib/firebase/comments'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { OutcomeLabel } from './outcome-label'
|
||||
import { Contract, setContract } from '../lib/firebase/contracts'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { Linkify } from './linkify'
|
||||
import { Row } from './layout/row'
|
||||
import { createComment } from '../lib/firebase/comments'
|
||||
import { useComments } from '../hooks/use-comments'
|
||||
import { formatMoney } from '../lib/util/format'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
function FeedComment(props: { activityItem: any }) {
|
||||
const { activityItem } = props
|
||||
const { person, text, amount, outcome, createdTime } = activityItem
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<img
|
||||
className="h-10 w-10 rounded-full bg-gray-400 flex items-center justify-center ring-8 ring-gray-50"
|
||||
src={person.avatarUrl}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<span className="absolute -bottom-3 -right-2 bg-gray-50 rounded-tl px-0.5 py-px">
|
||||
<ChatAltIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<p className="mt-0.5 text-sm text-gray-500">
|
||||
<a href={person.href} className="font-medium text-gray-900">
|
||||
{person.name}
|
||||
</a>{' '}
|
||||
placed M$ {amount} on <OutcomeLabel outcome={outcome} />{' '}
|
||||
<Timestamp time={createdTime} />
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 text-gray-700">
|
||||
<p className="whitespace-pre-wrap">
|
||||
<Linkify text={text} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Timestamp(props: { time: number }) {
|
||||
const { time } = props
|
||||
return (
|
||||
<span
|
||||
className="whitespace-nowrap text-gray-300 ml-1"
|
||||
title={dayjs(time).format('MMM D, h:mma')}
|
||||
>
|
||||
{dayjs(time).fromNow()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedBet(props: { activityItem: any }) {
|
||||
const { activityItem } = props
|
||||
const { id, contractId, amount, outcome, createdTime } = activityItem
|
||||
const user = useUser()
|
||||
const isCreator = user?.id == activityItem.userId
|
||||
|
||||
const [comment, setComment] = useState('')
|
||||
async function submitComment() {
|
||||
if (!user || !comment) return
|
||||
await createComment(contractId, id, comment, user)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1">
|
||||
<div className="h-8 w-8 bg-gray-200 rounded-full ring-8 ring-gray-50 flex items-center justify-center">
|
||||
<UserIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-1.5">
|
||||
<div className="text-sm text-gray-500">
|
||||
<span className="text-gray-900">
|
||||
{isCreator ? 'You' : 'A trader'}
|
||||
</span>{' '}
|
||||
placed {formatMoney(amount)} on <OutcomeLabel outcome={outcome} />{' '}
|
||||
<Timestamp time={createdTime} />
|
||||
{isCreator && (
|
||||
// Allow user to comment in an textarea if they are the creator
|
||||
<div className="mt-2">
|
||||
<textarea
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
className="textarea textarea-bordered w-full"
|
||||
placeholder="Add a comment..."
|
||||
/>
|
||||
<button
|
||||
className="btn btn-outline btn-sm mt-1"
|
||||
onClick={submitComment}
|
||||
>
|
||||
Comment
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContractDescription(props: {
|
||||
contract: Contract
|
||||
isCreator: boolean
|
||||
}) {
|
||||
const { contract, isCreator } = props
|
||||
const [editing, setEditing] = useState(false)
|
||||
const editStatement = () => `${dayjs().format('MMM D, h:mma')}: `
|
||||
const [description, setDescription] = useState(editStatement())
|
||||
|
||||
// Append the new description (after a newline)
|
||||
async function saveDescription(e: any) {
|
||||
e.preventDefault()
|
||||
setEditing(false)
|
||||
contract.description = `${contract.description}\n${description}`.trim()
|
||||
await setContract(contract)
|
||||
setDescription(editStatement())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="whitespace-pre-line break-words mt-2 text-gray-700">
|
||||
<Linkify text={contract.description} />
|
||||
<br />
|
||||
{isCreator &&
|
||||
!contract.resolution &&
|
||||
(editing ? (
|
||||
<form className="mt-4">
|
||||
<textarea
|
||||
className="textarea h-24 textarea-bordered w-full mb-1"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value || '')}
|
||||
autoFocus
|
||||
onFocus={(e) =>
|
||||
// Focus starts at end of description.
|
||||
e.target.setSelectionRange(
|
||||
description.length,
|
||||
description.length
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Row className="gap-2">
|
||||
<button
|
||||
className="btn btn-neutral btn-outline btn-sm"
|
||||
onClick={saveDescription}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-error btn-outline btn-sm"
|
||||
onClick={() => setEditing(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Row>
|
||||
</form>
|
||||
) : (
|
||||
<Row>
|
||||
<button
|
||||
className="btn btn-neutral btn-outline btn-sm mt-4"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
Add to description
|
||||
</button>
|
||||
</Row>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedStart(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const user = useUser()
|
||||
const isCreator = user?.id === contract.creatorId
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1">
|
||||
<div className="h-8 w-8 bg-gray-200 rounded-full ring-8 ring-gray-50 flex items-center justify-center">
|
||||
<StarIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-1.5">
|
||||
<div className="text-sm text-gray-500">
|
||||
<span className="text-gray-900">{contract.creatorName}</span> created
|
||||
this market <Timestamp time={contract.createdTime} />
|
||||
</div>
|
||||
<ContractDescription contract={contract} isCreator={isCreator} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function OutcomeIcon(props: { outcome?: 'YES' | 'NO' | 'CANCEL' }) {
|
||||
const { outcome } = props
|
||||
switch (outcome) {
|
||||
case 'YES':
|
||||
return <CheckIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||
case 'NO':
|
||||
return <XIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||
case 'CANCEL':
|
||||
default:
|
||||
return <BanIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||
}
|
||||
}
|
||||
|
||||
function FeedResolve(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const resolution = contract.resolution || 'CANCEL'
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1">
|
||||
<div className="h-8 w-8 bg-gray-200 rounded-full ring-8 ring-gray-50 flex items-center justify-center">
|
||||
<OutcomeIcon outcome={resolution} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-1.5">
|
||||
<div className="text-sm text-gray-500">
|
||||
<span className="text-gray-900">{contract.creatorName}</span> resolved
|
||||
this market to <OutcomeLabel outcome={resolution} />{' '}
|
||||
<Timestamp time={contract.resolutionTime || 0} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedClose(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1">
|
||||
<div className="h-8 w-8 bg-gray-200 rounded-full ring-8 ring-gray-50 flex items-center justify-center">
|
||||
<LockClosedIcon
|
||||
className="h-5 w-5 text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-1.5">
|
||||
<div className="text-sm text-gray-500">
|
||||
Trading closed in this market{' '}
|
||||
<Timestamp time={contract.closeTime || 0} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function toFeedBet(bet: Bet) {
|
||||
return {
|
||||
id: bet.id,
|
||||
contractId: bet.contractId,
|
||||
userId: bet.userId,
|
||||
type: 'bet',
|
||||
amount: bet.amount,
|
||||
outcome: bet.outcome,
|
||||
createdTime: bet.createdTime,
|
||||
date: dayjs(bet.createdTime).fromNow(),
|
||||
}
|
||||
}
|
||||
|
||||
function toFeedComment(bet: Bet, comment: Comment) {
|
||||
return {
|
||||
id: bet.id,
|
||||
contractId: bet.contractId,
|
||||
userId: bet.userId,
|
||||
type: 'comment',
|
||||
amount: bet.amount,
|
||||
outcome: bet.outcome,
|
||||
createdTime: bet.createdTime,
|
||||
date: dayjs(bet.createdTime).fromNow(),
|
||||
|
||||
// Invariant: bet.comment exists
|
||||
text: comment.text,
|
||||
person: {
|
||||
href: `/${comment.userUsername}`,
|
||||
name: comment.userName,
|
||||
avatarUrl: comment.userAvatarUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Group together bets that are:
|
||||
// - Within 24h of the first in the group
|
||||
// - Do not have a comment
|
||||
// - Were not created by this user
|
||||
// Return a list of ActivityItems
|
||||
function group(bets: Bet[], comments: Comment[], userId?: string) {
|
||||
const commentsMap = mapCommentsByBetId(comments)
|
||||
const items: any[] = []
|
||||
let group: Bet[] = []
|
||||
|
||||
// Turn the current group into an ActivityItem
|
||||
function pushGroup() {
|
||||
if (group.length == 1) {
|
||||
items.push(toActivityItem(group[0]))
|
||||
} else if (group.length > 1) {
|
||||
items.push({ type: 'betgroup', bets: [...group], id: group[0].id })
|
||||
}
|
||||
group = []
|
||||
}
|
||||
|
||||
function toActivityItem(bet: Bet) {
|
||||
const comment = commentsMap[bet.id]
|
||||
return comment ? toFeedComment(bet, comment) : toFeedBet(bet)
|
||||
}
|
||||
|
||||
for (const bet of bets) {
|
||||
const isCreator = userId === bet.userId
|
||||
|
||||
if (commentsMap[bet.id] || isCreator) {
|
||||
pushGroup()
|
||||
// Create a single item for this
|
||||
items.push(toActivityItem(bet))
|
||||
} else {
|
||||
if (
|
||||
group.length > 0 &&
|
||||
dayjs(bet.createdTime).diff(dayjs(group[0].createdTime), 'hour') > 24
|
||||
) {
|
||||
// More than 24h has passed; start a new group
|
||||
pushGroup()
|
||||
}
|
||||
group.push(bet)
|
||||
}
|
||||
}
|
||||
if (group.length > 0) {
|
||||
pushGroup()
|
||||
}
|
||||
return items as ActivityItem[]
|
||||
}
|
||||
|
||||
// TODO: Make this expandable to show all grouped bets?
|
||||
function FeedBetGroup(props: { activityItem: any }) {
|
||||
const { activityItem } = props
|
||||
const bets: Bet[] = activityItem.bets
|
||||
|
||||
const yesAmount = bets
|
||||
.filter((b) => b.outcome == 'YES')
|
||||
.reduce((acc, bet) => acc + bet.amount, 0)
|
||||
const yesSpan = yesAmount ? (
|
||||
<span>
|
||||
{formatMoney(yesAmount)} on <OutcomeLabel outcome={'YES'} />
|
||||
</span>
|
||||
) : null
|
||||
const noAmount = bets
|
||||
.filter((b) => b.outcome == 'NO')
|
||||
.reduce((acc, bet) => acc + bet.amount, 0)
|
||||
const noSpan = noAmount ? (
|
||||
<span>
|
||||
{formatMoney(noAmount)} on <OutcomeLabel outcome={'NO'} />
|
||||
</span>
|
||||
) : null
|
||||
const traderCount = bets.length
|
||||
const createdTime = bets[0].createdTime
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1">
|
||||
<div className="h-8 w-8 bg-gray-200 rounded-full ring-8 ring-gray-50 flex items-center justify-center">
|
||||
<UsersIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-1.5">
|
||||
<div className="text-sm text-gray-500">
|
||||
<span className="text-gray-900">{traderCount} traders</span> placed{' '}
|
||||
{yesSpan}
|
||||
{yesAmount && noAmount ? ' and ' : ''}
|
||||
{noSpan} <Timestamp time={createdTime} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Missing feed items:
|
||||
// - Bet sold?
|
||||
type ActivityItem = {
|
||||
id: string
|
||||
type: 'bet' | 'comment' | 'start' | 'betgroup' | 'close' | 'resolve'
|
||||
}
|
||||
|
||||
export function ContractFeed(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const { id } = contract
|
||||
const user = useUser()
|
||||
|
||||
let bets = useBets(id)
|
||||
if (bets === 'loading') bets = []
|
||||
|
||||
let comments = useComments(id)
|
||||
if (comments === 'loading') comments = []
|
||||
|
||||
const allItems = [
|
||||
{ type: 'start', id: 0 },
|
||||
...group(bets, comments, user?.id),
|
||||
]
|
||||
if (contract.closeTime && contract.closeTime <= Date.now()) {
|
||||
allItems.push({ type: 'close', id: `${contract.closeTime}` })
|
||||
}
|
||||
if (contract.resolution) {
|
||||
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
<ul role="list" className="-mb-8">
|
||||
{allItems.map((activityItem, activityItemIdx) => (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-8">
|
||||
{activityItemIdx !== allItems.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-gray-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex items-start space-x-3">
|
||||
{activityItem.type === 'start' ? (
|
||||
<FeedStart contract={contract} />
|
||||
) : activityItem.type === 'comment' ? (
|
||||
<FeedComment activityItem={activityItem} />
|
||||
) : activityItem.type === 'bet' ? (
|
||||
<FeedBet activityItem={activityItem} />
|
||||
) : activityItem.type === 'betgroup' ? (
|
||||
<FeedBetGroup activityItem={activityItem} />
|
||||
) : activityItem.type === 'close' ? (
|
||||
<FeedClose contract={contract} />
|
||||
) : activityItem.type === 'resolve' ? (
|
||||
<FeedResolve contract={contract} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,132 +1,72 @@
|
|||
import { useState } from 'react'
|
||||
import {
|
||||
compute,
|
||||
Contract,
|
||||
deleteContract,
|
||||
setContract,
|
||||
path,
|
||||
} from '../lib/firebase/contracts'
|
||||
import { Col } from './layout/col'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { ContractProbGraph } from './contract-prob-graph'
|
||||
import { ContractDetails } from './contracts-list'
|
||||
import router from 'next/router'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { Row } from './layout/row'
|
||||
import dayjs from 'dayjs'
|
||||
import { Linkify } from './linkify'
|
||||
import clsx from 'clsx'
|
||||
|
||||
function ContractDescription(props: {
|
||||
contract: Contract
|
||||
isCreator: boolean
|
||||
}) {
|
||||
const { contract, isCreator } = props
|
||||
const [editing, setEditing] = useState(false)
|
||||
const editStatement = () => `${dayjs().format('MMM D, h:mma')}: `
|
||||
const [description, setDescription] = useState(editStatement())
|
||||
|
||||
// Append the new description (after a newline)
|
||||
async function saveDescription(e: any) {
|
||||
e.preventDefault()
|
||||
setEditing(false)
|
||||
contract.description = `${contract.description}\n${description}`.trim()
|
||||
await setContract(contract)
|
||||
setDescription(editStatement())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="whitespace-pre-line">
|
||||
<Linkify text={contract.description} />
|
||||
<br />
|
||||
{isCreator &&
|
||||
!contract.resolution &&
|
||||
(editing ? (
|
||||
<form className="mt-4">
|
||||
<textarea
|
||||
className="textarea h-24 textarea-bordered w-full mb-2"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value || '')}
|
||||
autoFocus
|
||||
onFocus={(e) =>
|
||||
// Focus starts at end of description.
|
||||
e.target.setSelectionRange(
|
||||
description.length,
|
||||
description.length
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Row className="gap-4 justify-end">
|
||||
<button
|
||||
className="btn btn-error btn-outline btn-sm mt-2"
|
||||
onClick={() => setEditing(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-neutral btn-outline btn-sm mt-2"
|
||||
onClick={saveDescription}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</Row>
|
||||
</form>
|
||||
) : (
|
||||
<Row className="justify-end">
|
||||
<button
|
||||
className="btn btn-neutral btn-outline btn-sm mt-4"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
Add to description
|
||||
</button>
|
||||
</Row>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { ContractDetails, ResolutionOrChance } from './contract-card'
|
||||
import { ContractFeed } from './contract-feed'
|
||||
import { TweetButton } from './tweet-button'
|
||||
|
||||
export const ContractOverview = (props: {
|
||||
contract: Contract
|
||||
className?: string
|
||||
}) => {
|
||||
const { contract, className } = props
|
||||
const { resolution, creatorId } = contract
|
||||
const { probPercent, volume } = compute(contract)
|
||||
const { resolution, creatorId, creatorName } = contract
|
||||
const { probPercent, truePool } = compute(contract)
|
||||
|
||||
const user = useUser()
|
||||
const isCreator = user?.id === creatorId
|
||||
|
||||
const resolutionColor = {
|
||||
YES: 'text-primary',
|
||||
NO: 'text-red-400',
|
||||
CANCEL: 'text-yellow-400',
|
||||
'': '', // Empty if unresolved
|
||||
}[contract.resolution || '']
|
||||
const tweetQuestion = isCreator
|
||||
? contract.question
|
||||
: `${creatorName}: ${contract.question}`
|
||||
const tweetDescription = resolution
|
||||
? isCreator
|
||||
? `Resolved ${resolution}!`
|
||||
: `Resolved ${resolution} by ${creatorName}:`
|
||||
: `Currently ${probPercent} chance, place your bets here:`
|
||||
const url = `https://manifold.markets${path(contract)}`
|
||||
const tweetText = `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
|
||||
|
||||
return (
|
||||
<Col className={clsx('mb-6', className)}>
|
||||
<Col className="justify-between md:flex-row">
|
||||
<Col>
|
||||
<div className="text-3xl text-indigo-700 mb-4">
|
||||
<Row className="justify-between gap-4">
|
||||
<Col className="gap-4">
|
||||
<div className="text-2xl md:text-3xl text-indigo-700">
|
||||
<Linkify text={contract.question} />
|
||||
</div>
|
||||
|
||||
<ResolutionOrChance
|
||||
className="md:hidden"
|
||||
resolution={resolution}
|
||||
probPercent={probPercent}
|
||||
large
|
||||
/>
|
||||
|
||||
<ContractDetails contract={contract} />
|
||||
<TweetButton className="self-end md:hidden" tweetText={tweetText} />
|
||||
</Col>
|
||||
|
||||
{resolution ? (
|
||||
<Col className="text-4xl mt-8 md:mt-0 md:ml-4 md:mr-6 items-end self-center md:self-start">
|
||||
<div className="text-xl text-gray-500">Resolved</div>
|
||||
<div className={resolutionColor}>
|
||||
{resolution === 'CANCEL' ? 'N/A' : resolution}
|
||||
</div>
|
||||
</Col>
|
||||
) : (
|
||||
<Col className="text-4xl mt-8 md:mt-0 md:ml-4 md:mr-6 text-primary items-end self-center md:self-start">
|
||||
{probPercent}
|
||||
<div className="text-xl">chance</div>
|
||||
</Col>
|
||||
)}
|
||||
</Col>
|
||||
<Col className="hidden md:flex justify-between items-end">
|
||||
<ResolutionOrChance
|
||||
className="items-end"
|
||||
resolution={resolution}
|
||||
probPercent={probPercent}
|
||||
large
|
||||
/>
|
||||
<TweetButton className="mt-6" tweetText={tweetText} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
|
@ -134,13 +74,8 @@ export const ContractOverview = (props: {
|
|||
|
||||
<Spacer h={12} />
|
||||
|
||||
{((isCreator && !contract.resolution) || contract.description) && (
|
||||
<label className="text-gray-500 mb-2 text-sm">Description</label>
|
||||
)}
|
||||
<ContractDescription contract={contract} isCreator={isCreator} />
|
||||
|
||||
{/* Show a delete button for contracts without any trading */}
|
||||
{isCreator && volume === 0 && (
|
||||
{isCreator && truePool === 0 && (
|
||||
<>
|
||||
<Spacer h={8} />
|
||||
<button
|
||||
|
@ -155,6 +90,8 @@ export const ContractOverview = (props: {
|
|||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ContractFeed contract={contract} />
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,84 +3,15 @@ import Link from 'next/link'
|
|||
import clsx from 'clsx'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Row } from '../components/layout/row'
|
||||
import {
|
||||
compute,
|
||||
Contract,
|
||||
listContracts,
|
||||
path,
|
||||
} from '../lib/firebase/contracts'
|
||||
import { formatMoney } from '../lib/util/format'
|
||||
import { compute, Contract, listContracts } from '../lib/firebase/contracts'
|
||||
import { User } from '../lib/firebase/users'
|
||||
import { UserLink } from './user-page'
|
||||
import { Linkify } from './linkify'
|
||||
import { Col } from './layout/col'
|
||||
import { SiteLink } from './site-link'
|
||||
import { parseTags } from '../lib/util/parse'
|
||||
import { ContractCard } from './contract-card'
|
||||
import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
||||
|
||||
export function ContractDetails(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const { volume, createdDate, resolvedDate } = compute(contract)
|
||||
|
||||
return (
|
||||
<Row className="flex-wrap text-sm text-gray-500">
|
||||
<div className="whitespace-nowrap">
|
||||
<UserLink username={contract.creatorUsername} />
|
||||
</div>
|
||||
<div className="mx-2">•</div>
|
||||
<div className="whitespace-nowrap">
|
||||
{resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}
|
||||
</div>
|
||||
<div className="mx-2">•</div>
|
||||
<div className="whitespace-nowrap">{formatMoney(volume)} volume</div>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
function ContractCard(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
const { probPercent } = compute(contract)
|
||||
|
||||
const resolutionColor = {
|
||||
YES: 'text-primary',
|
||||
NO: 'text-red-400',
|
||||
CANCEL: 'text-yellow-400',
|
||||
'': '', // Empty if unresolved
|
||||
}[contract.resolution || '']
|
||||
|
||||
const resolutionText = {
|
||||
YES: 'YES',
|
||||
NO: 'NO',
|
||||
CANCEL: 'N/A',
|
||||
'': '',
|
||||
}[contract.resolution || '']
|
||||
|
||||
return (
|
||||
<Link href={path(contract)}>
|
||||
<a>
|
||||
<li className="col-span-1 bg-white hover:bg-gray-100 shadow-xl rounded-lg divide-y divide-gray-200">
|
||||
<div className="card">
|
||||
<div className="card-body p-6">
|
||||
<Row className="justify-between gap-4 mb-2">
|
||||
<p className="font-medium text-indigo-700">
|
||||
<Linkify text={contract.question} />
|
||||
</p>
|
||||
<div className={clsx('text-4xl', resolutionColor)}>
|
||||
{resolutionText || (
|
||||
<div className="text-primary">
|
||||
{probPercent}
|
||||
<div className="text-lg">chance</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
<ContractDetails contract={contract} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function ContractsGrid(props: { contracts: Contract[] }) {
|
||||
export function ContractsGrid(props: { contracts: Contract[] }) {
|
||||
const [resolvedContracts, activeContracts] = _.partition(
|
||||
props.contracts,
|
||||
(c) => c.isResolved
|
||||
|
@ -110,14 +41,122 @@ function ContractsGrid(props: { contracts: Contract[] }) {
|
|||
)
|
||||
}
|
||||
|
||||
type Sort = 'createdTime' | 'volume' | 'resolved' | 'all'
|
||||
const MAX_GROUPED_CONTRACTS_DISPLAYED = 6
|
||||
|
||||
function CreatorContractsGrid(props: { contracts: Contract[] }) {
|
||||
const { contracts } = props
|
||||
|
||||
const byCreator = _.groupBy(contracts, (contract) => contract.creatorId)
|
||||
const creatorIds = _.sortBy(Object.keys(byCreator), (creatorId) =>
|
||||
_.sumBy(byCreator[creatorId], (contract) => -1 * compute(contract).truePool)
|
||||
)
|
||||
|
||||
return (
|
||||
<Col className="gap-6">
|
||||
{creatorIds.map((creatorId) => {
|
||||
const { creatorUsername, creatorName } = byCreator[creatorId][0]
|
||||
|
||||
return (
|
||||
<Col className="gap-4" key={creatorUsername}>
|
||||
<SiteLink className="text-lg" href={`/${creatorUsername}`}>
|
||||
{creatorName}
|
||||
</SiteLink>
|
||||
|
||||
<ul role="list" className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{byCreator[creatorId]
|
||||
.slice(0, MAX_GROUPED_CONTRACTS_DISPLAYED)
|
||||
.map((contract) => (
|
||||
<ContractCard contract={contract} key={contract.id} />
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{byCreator[creatorId].length > MAX_GROUPED_CONTRACTS_DISPLAYED ? (
|
||||
<Link href={`/${creatorUsername}`}>
|
||||
<a
|
||||
className={clsx(
|
||||
'self-end hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
See all
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
function TagContractsGrid(props: { contracts: Contract[] }) {
|
||||
const { contracts } = props
|
||||
|
||||
const contractTags = _.flatMap(contracts, (contract) =>
|
||||
parseTags(contract.question + ' ' + contract.description).map((tag) => ({
|
||||
tag,
|
||||
contract,
|
||||
}))
|
||||
)
|
||||
const groupedByTag = _.groupBy(contractTags, ({ tag }) => tag)
|
||||
const byTag = _.mapValues(groupedByTag, (contractTags) =>
|
||||
contractTags.map(({ contract }) => contract)
|
||||
)
|
||||
const tags = _.sortBy(Object.keys(byTag), (tag) =>
|
||||
_.sumBy(byTag[tag], (contract) => -1 * compute(contract).truePool)
|
||||
)
|
||||
|
||||
return (
|
||||
<Col className="gap-6">
|
||||
{tags.map((tag) => {
|
||||
return (
|
||||
<Col className="gap-4" key={tag}>
|
||||
<SiteLink className="text-lg" href={`/tag/${tag}`}>
|
||||
#{tag}
|
||||
</SiteLink>
|
||||
|
||||
<ul role="list" className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{byTag[tag]
|
||||
.slice(0, MAX_GROUPED_CONTRACTS_DISPLAYED)
|
||||
.map((contract) => (
|
||||
<ContractCard contract={contract} key={contract.id} />
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{byTag[tag].length > MAX_GROUPED_CONTRACTS_DISPLAYED ? (
|
||||
<Link href={`/tag/${tag}`}>
|
||||
<a
|
||||
className={clsx(
|
||||
'self-end hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
See all
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
const MAX_CONTRACTS_DISPLAYED = 99
|
||||
|
||||
export function SearchableGrid(props: {
|
||||
contracts: Contract[]
|
||||
defaultSort?: Sort
|
||||
query: string
|
||||
setQuery: (query: string) => void
|
||||
sort: Sort
|
||||
setSort: (sort: Sort) => void
|
||||
byOneCreator?: boolean
|
||||
}) {
|
||||
const { contracts, defaultSort } = props
|
||||
const [query, setQuery] = useState('')
|
||||
const [sort, setSort] = useState(defaultSort || 'volume')
|
||||
const { contracts, query, setQuery, sort, setSort, byOneCreator } = props
|
||||
|
||||
function check(corpus: String) {
|
||||
return corpus.toLowerCase().includes(query.toLowerCase())
|
||||
|
@ -130,10 +169,10 @@ export function SearchableGrid(props: {
|
|||
check(c.creatorUsername)
|
||||
)
|
||||
|
||||
if (sort === 'createdTime' || sort === 'resolved' || sort === 'all') {
|
||||
if (sort === 'newest' || sort === 'resolved' || sort === 'all') {
|
||||
matches.sort((a, b) => b.createdTime - a.createdTime)
|
||||
} else if (sort === 'volume') {
|
||||
matches.sort((a, b) => compute(b).volume - compute(a).volume)
|
||||
} else if (sort === 'most-traded' || sort === 'creator' || sort === 'tag') {
|
||||
matches.sort((a, b) => compute(b).truePool - compute(a).truePool)
|
||||
}
|
||||
|
||||
if (sort !== 'all') {
|
||||
|
@ -143,6 +182,9 @@ export function SearchableGrid(props: {
|
|||
)
|
||||
}
|
||||
|
||||
if (matches.length > MAX_CONTRACTS_DISPLAYED)
|
||||
matches = _.slice(matches, 0, MAX_CONTRACTS_DISPLAYED)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Show a search input next to a sort dropdown */}
|
||||
|
@ -159,22 +201,37 @@ export function SearchableGrid(props: {
|
|||
value={sort}
|
||||
onChange={(e) => setSort(e.target.value as Sort)}
|
||||
>
|
||||
<option value="volume">Most traded</option>
|
||||
<option value="createdTime">Newest first</option>
|
||||
{byOneCreator ? (
|
||||
<option value="all">All markets</option>
|
||||
) : (
|
||||
<option value="creator">By creator</option>
|
||||
)}
|
||||
<option value="tag">By tag</option>
|
||||
<option value="most-traded">Most traded</option>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="all">All markets</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ContractsGrid contracts={matches} />
|
||||
{sort === 'tag' ? (
|
||||
<TagContractsGrid contracts={matches} />
|
||||
) : !byOneCreator && (sort === 'creator' || sort === 'resolved') ? (
|
||||
<CreatorContractsGrid contracts={matches} />
|
||||
) : (
|
||||
<ContractsGrid contracts={matches} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContractsList(props: { creator: User }) {
|
||||
export function CreatorContractsList(props: { creator: User }) {
|
||||
const { creator } = props
|
||||
const [contracts, setContracts] = useState<Contract[] | 'loading'>('loading')
|
||||
|
||||
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
||||
defaultSort: 'all',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (creator?.id) {
|
||||
// TODO: stream changes from firestore
|
||||
|
@ -184,5 +241,14 @@ export function ContractsList(props: { creator: User }) {
|
|||
|
||||
if (contracts === 'loading') return <></>
|
||||
|
||||
return <SearchableGrid contracts={contracts} defaultSort="all" />
|
||||
return (
|
||||
<SearchableGrid
|
||||
contracts={contracts}
|
||||
byOneCreator
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
sort={sort}
|
||||
setSort={setSort}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Link from 'next/link'
|
||||
import { Fragment } from 'react'
|
||||
import { SiteLink } from './site-link'
|
||||
|
||||
// Return a JSX span, linkifying @username, #hashtags, and https://...
|
||||
export function Linkify(props: { text: string }) {
|
||||
const { text } = props
|
||||
export function Linkify(props: { text: string; gray?: boolean }) {
|
||||
const { text, gray } = props
|
||||
const regex = /(?:^|\s)(?:[@#][a-z0-9_]+|https?:\/\/\S+)/gi
|
||||
const matches = text.match(regex) || []
|
||||
const links = matches.map((match) => {
|
||||
|
@ -15,17 +15,18 @@ export function Linkify(props: { text: string }) {
|
|||
{
|
||||
'@': `/${tag}`,
|
||||
'#': `/tag/${tag}`,
|
||||
}[symbol] ?? match
|
||||
}[symbol] ?? match.trim()
|
||||
|
||||
return (
|
||||
<>
|
||||
{whitespace}
|
||||
<Link href={href}>
|
||||
<a className="text-indigo-700 hover:underline hover:decoration-2">
|
||||
{symbol}
|
||||
{tag}
|
||||
</a>
|
||||
</Link>
|
||||
<SiteLink
|
||||
className={gray ? 'text-gray-500' : 'text-indigo-700'}
|
||||
href={href}
|
||||
>
|
||||
{symbol}
|
||||
{tag}
|
||||
</SiteLink>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -14,12 +14,11 @@ export function ManticLogo(props: { darkBackground?: boolean }) {
|
|||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'font-major-mono lowercase mt-1 sm:text-2xl md:whitespace-nowrap',
|
||||
'hidden sm:flex font-major-mono lowercase mt-1 sm:text-2xl md:whitespace-nowrap',
|
||||
darkBackground && 'text-white'
|
||||
)}
|
||||
style={{ fontFamily: 'Major Mono Display,monospace' }}
|
||||
>
|
||||
Mantic Markets
|
||||
Manifold Markets
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
|
|
|
@ -34,6 +34,7 @@ export function MenuButton(props: {
|
|||
{({ active }) => (
|
||||
<a
|
||||
href={item.href}
|
||||
target={item.href.startsWith('http') ? '_blank' : undefined}
|
||||
onClick={item.onClick}
|
||||
className={clsx(
|
||||
active ? 'bg-gray-100' : '',
|
||||
|
|
|
@ -22,14 +22,7 @@ export function NavBar(props: {
|
|||
const themeClasses = clsx(darkBackground && 'text-white', hoverClasses)
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={clsx(
|
||||
'w-full p-4 mb-4 shadow-sm',
|
||||
!darkBackground && 'bg-white',
|
||||
className
|
||||
)}
|
||||
aria-label="Global"
|
||||
>
|
||||
<nav className={clsx('w-full p-4 mb-4', className)} aria-label="Global">
|
||||
<Row
|
||||
className={clsx(
|
||||
'justify-between items-center mx-auto sm:px-4',
|
||||
|
|
26
web/components/outcome-label.tsx
Normal file
26
web/components/outcome-label.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
export function OutcomeLabel(props: {
|
||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
||||
}) {
|
||||
const { outcome } = props
|
||||
|
||||
if (outcome === 'YES') return <YesLabel />
|
||||
if (outcome === 'NO') return <NoLabel />
|
||||
if (outcome === 'MKT') return <MarketLabel />
|
||||
return <CancelLabel />
|
||||
}
|
||||
|
||||
export function YesLabel() {
|
||||
return <span className="text-primary">YES</span>
|
||||
}
|
||||
|
||||
export function NoLabel() {
|
||||
return <span className="text-red-400">NO</span>
|
||||
}
|
||||
|
||||
export function CancelLabel() {
|
||||
return <span className="text-yellow-400">N/A</span>
|
||||
}
|
||||
|
||||
export function MarketLabel() {
|
||||
return <span className="text-blue-400">MKT</span>
|
||||
}
|
|
@ -7,6 +7,7 @@ export function Page(props: { wide?: boolean; children?: any }) {
|
|||
return (
|
||||
<div>
|
||||
<NavBar wide={wide} />
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'w-full px-4 pb-8 mx-auto',
|
||||
|
|
|
@ -41,8 +41,8 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) {
|
|||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Your bets',
|
||||
href: '/bets',
|
||||
name: 'Your trades',
|
||||
href: '/trades',
|
||||
},
|
||||
{
|
||||
name: 'Your markets',
|
||||
|
@ -57,6 +57,10 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) {
|
|||
href: '#',
|
||||
onClick: () => firebaseLogout(),
|
||||
},
|
||||
{
|
||||
name: 'Discord',
|
||||
href: 'https://discord.gg/eHQBNBqXuh',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -67,7 +71,7 @@ function ProfileSummary(props: { user: User }) {
|
|||
<div className="rounded-full w-10 h-10 mr-4">
|
||||
<Image src={user.avatarUrl} width={40} height={40} />
|
||||
</div>
|
||||
<div className="truncate" style={{ maxWidth: 175 }}>
|
||||
<div className="truncate text-left" style={{ maxWidth: 170 }}>
|
||||
{user.name}
|
||||
<div className="text-gray-700 text-sm">{formatMoney(user.balance)}</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from 'clsx'
|
||||
import React, { useState } from 'react'
|
||||
import { getFunctions, httpsCallable } from 'firebase/functions'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { Contract } from '../lib/firebase/contracts'
|
||||
import { Col } from './layout/col'
|
||||
|
@ -9,18 +8,23 @@ import { User } from '../lib/firebase/users'
|
|||
import { YesNoCancelSelector } from './yes-no-selector'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { ConfirmationButton as ConfirmationButton } from './confirmation-button'
|
||||
|
||||
const functions = getFunctions()
|
||||
export const resolveMarket = httpsCallable(functions, 'resolveMarket')
|
||||
import { resolveMarket } from '../lib/firebase/api-call'
|
||||
|
||||
export function ResolutionPanel(props: {
|
||||
creator: User
|
||||
contract: Contract
|
||||
className?: string
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// warm up cloud function
|
||||
resolveMarket({}).catch()
|
||||
}, [])
|
||||
|
||||
const { contract, className } = props
|
||||
|
||||
const [outcome, setOutcome] = useState<'YES' | 'NO' | 'CANCEL' | undefined>()
|
||||
const [outcome, setOutcome] = useState<
|
||||
'YES' | 'NO' | 'MKT' | 'CANCEL' | undefined
|
||||
>()
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | undefined>(undefined)
|
||||
|
@ -48,11 +52,13 @@ export function ResolutionPanel(props: {
|
|||
? 'bg-red-400 hover:bg-red-500'
|
||||
: outcome === 'CANCEL'
|
||||
? 'bg-yellow-400 hover:bg-yellow-500'
|
||||
: outcome === 'MKT'
|
||||
? 'bg-blue-400 hover:bg-blue-500'
|
||||
: 'btn-disabled'
|
||||
|
||||
return (
|
||||
<Col
|
||||
className={clsx('bg-gray-100 shadow-xl px-8 py-6 rounded-md', className)}
|
||||
className={clsx('bg-gray-100 shadow-md px-8 py-6 rounded-md', className)}
|
||||
>
|
||||
<Title className="mt-0" text="Your market" />
|
||||
|
||||
|
@ -70,18 +76,19 @@ export function ResolutionPanel(props: {
|
|||
<div>
|
||||
{outcome === 'YES' ? (
|
||||
<>
|
||||
Winnings will be paid out to YES bettors. You earn 1% of the NO
|
||||
bets.
|
||||
Winnings will be paid out to YES bettors. You earn 1% of the pool.
|
||||
</>
|
||||
) : outcome === 'NO' ? (
|
||||
<>
|
||||
Winnings will be paid out to NO bettors. You earn 1% of the YES
|
||||
bets.
|
||||
</>
|
||||
<>Winnings will be paid out to NO bettors. You earn 1% of the pool.</>
|
||||
) : outcome === 'CANCEL' ? (
|
||||
<>All bets will be returned with no fees.</>
|
||||
<>The pool will be returned to traders with no fees.</>
|
||||
) : outcome === 'MKT' ? (
|
||||
<>
|
||||
Traders will be paid out at the current implied probability. You
|
||||
earn 1% of the pool.
|
||||
</>
|
||||
) : (
|
||||
<>Resolving this market will immediately pay out bettors.</>
|
||||
<>Resolving this market will immediately pay out traders.</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
35
web/components/site-link.tsx
Normal file
35
web/components/site-link.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const SiteLink = (props: {
|
||||
href: string
|
||||
children: any
|
||||
className?: string
|
||||
}) => {
|
||||
const { href, children, className } = props
|
||||
|
||||
return href.startsWith('http') ? (
|
||||
<a
|
||||
href={href}
|
||||
className={clsx(
|
||||
'hover:underline hover:decoration-indigo-400 hover:decoration-2',
|
||||
className
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={href}>
|
||||
<a
|
||||
className={clsx(
|
||||
'hover:underline hover:decoration-indigo-400 hover:decoration-2',
|
||||
className
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
|
@ -4,10 +4,7 @@ export function Title(props: { text: string; className?: string }) {
|
|||
const { text, className } = props
|
||||
return (
|
||||
<h1
|
||||
className={clsx(
|
||||
'text-3xl font-major-mono text-indigo-700 inline-block my-6',
|
||||
className
|
||||
)}
|
||||
className={clsx('text-3xl text-indigo-700 inline-block my-6', className)}
|
||||
>
|
||||
{text}
|
||||
</h1>
|
||||
|
|
24
web/components/tweet-button.tsx
Normal file
24
web/components/tweet-button.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import clsx from 'clsx'
|
||||
|
||||
export function TweetButton(props: { className?: string; tweetText?: string }) {
|
||||
const { tweetText, className } = props
|
||||
|
||||
return (
|
||||
<a
|
||||
className={clsx('btn btn-xs normal-case border-none', className)}
|
||||
style={{ backgroundColor: '#1da1f2', width: 75 }}
|
||||
href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
||||
tweetText ?? ''
|
||||
)}`}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="mr-2"
|
||||
src={'/twitter-icon-white.svg'}
|
||||
width={15}
|
||||
height={15}
|
||||
/>
|
||||
Tweet
|
||||
</a>
|
||||
)
|
||||
}
|
|
@ -1,28 +1,19 @@
|
|||
import { firebaseLogout, User } from '../lib/firebase/users'
|
||||
import { ContractsList } from './contracts-list'
|
||||
import { CreatorContractsList } from './contracts-list'
|
||||
import { Title } from './title'
|
||||
import { Row } from './layout/row'
|
||||
import { formatMoney } from '../lib/util/format'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
import { SEO } from './SEO'
|
||||
import { Page } from './page'
|
||||
import { SiteLink } from './site-link'
|
||||
|
||||
export function UserLink(props: { username: string; className?: string }) {
|
||||
const { username, className } = props
|
||||
|
||||
return (
|
||||
<Link href={`/${username}`}>
|
||||
<a
|
||||
className={clsx(
|
||||
'hover:underline hover:decoration-indigo-400 hover:decoration-2',
|
||||
className
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@{username}
|
||||
</a>
|
||||
</Link>
|
||||
<SiteLink href={`/${username}`} className={className}>
|
||||
@{username}
|
||||
</SiteLink>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -67,7 +58,7 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
|||
|
||||
const isCurrentUser = user.id === currentUser?.id
|
||||
|
||||
const possesive = isCurrentUser ? 'Your ' : `${user.username}'s `
|
||||
const possesive = isCurrentUser ? 'Your ' : `${user.name}'s `
|
||||
|
||||
return (
|
||||
<Page>
|
||||
|
@ -81,7 +72,7 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
|||
|
||||
<Title text={possesive + 'markets'} />
|
||||
|
||||
<ContractsList creator={user} />
|
||||
<CreatorContractsList creator={user} />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import { formatMoney } from '../lib/util/format'
|
||||
import { Col } from './layout/col'
|
||||
import { Row } from './layout/row'
|
||||
|
||||
export function YesNoSelector(props: {
|
||||
|
@ -30,41 +31,53 @@ export function YesNoSelector(props: {
|
|||
}
|
||||
|
||||
export function YesNoCancelSelector(props: {
|
||||
selected: 'YES' | 'NO' | 'CANCEL' | undefined
|
||||
onSelect: (selected: 'YES' | 'NO' | 'CANCEL') => void
|
||||
selected: 'YES' | 'NO' | 'MKT' | 'CANCEL' | undefined
|
||||
onSelect: (selected: 'YES' | 'NO' | 'MKT' | 'CANCEL') => void
|
||||
className?: string
|
||||
btnClassName?: string
|
||||
}) {
|
||||
const { selected, onSelect, className } = props
|
||||
|
||||
const btnClassName = clsx('px-6', props.btnClassName)
|
||||
const btnClassName = clsx('px-6 flex-1', props.btnClassName)
|
||||
|
||||
return (
|
||||
<Row className={clsx('space-x-3', className)}>
|
||||
<Button
|
||||
color={selected === 'YES' ? 'green' : 'gray'}
|
||||
onClick={() => onSelect('YES')}
|
||||
className={btnClassName}
|
||||
>
|
||||
YES
|
||||
</Button>
|
||||
<Col>
|
||||
<Row className={clsx('space-x-3 w-full', className)}>
|
||||
<Button
|
||||
color={selected === 'YES' ? 'green' : 'gray'}
|
||||
onClick={() => onSelect('YES')}
|
||||
className={btnClassName}
|
||||
>
|
||||
YES
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color={selected === 'NO' ? 'red' : 'gray'}
|
||||
onClick={() => onSelect('NO')}
|
||||
className={btnClassName}
|
||||
>
|
||||
NO
|
||||
</Button>
|
||||
<Button
|
||||
color={selected === 'NO' ? 'red' : 'gray'}
|
||||
onClick={() => onSelect('NO')}
|
||||
className={btnClassName}
|
||||
>
|
||||
NO
|
||||
</Button>
|
||||
</Row>
|
||||
|
||||
<Button
|
||||
color={selected === 'CANCEL' ? 'yellow' : 'gray'}
|
||||
onClick={() => onSelect('CANCEL')}
|
||||
className={btnClassName}
|
||||
>
|
||||
N/A
|
||||
</Button>
|
||||
</Row>
|
||||
<Row className={clsx('space-x-3 w-full', className)}>
|
||||
<Button
|
||||
color={selected === 'MKT' ? 'blue' : 'gray'}
|
||||
onClick={() => onSelect('MKT')}
|
||||
className={clsx(btnClassName, 'btn-sm')}
|
||||
>
|
||||
MKT
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color={selected === 'CANCEL' ? 'yellow' : 'gray'}
|
||||
onClick={() => onSelect('CANCEL')}
|
||||
className={clsx(btnClassName, 'btn-sm')}
|
||||
>
|
||||
N/A
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -98,7 +111,7 @@ export function FundsSelector(props: {
|
|||
function Button(props: {
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
color: 'green' | 'purple' | 'red' | 'yellow' | 'gray'
|
||||
color: 'green' | 'red' | 'blue' | 'yellow' | 'gray'
|
||||
children?: any
|
||||
}) {
|
||||
const { className, onClick, children, color } = props
|
||||
|
@ -109,9 +122,9 @@ function Button(props: {
|
|||
className={clsx(
|
||||
'flex-1 inline-flex justify-center items-center px-8 py-3 border border-transparent rounded-md shadow-sm text-sm font-medium text-white',
|
||||
color === 'green' && 'btn-primary',
|
||||
color === 'purple' && 'btn-secondary',
|
||||
color === 'red' && 'bg-red-400 hover:bg-red-500',
|
||||
color === 'yellow' && 'bg-yellow-400 hover:bg-yellow-500',
|
||||
color === 'blue' && 'bg-blue-400 hover:bg-blue-500',
|
||||
color === 'gray' && 'text-gray-700 bg-gray-300 hover:bg-gray-400',
|
||||
className
|
||||
)}
|
||||
|
|
12
web/hooks/use-comments.ts
Normal file
12
web/hooks/use-comments.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Comment, listenForComments } from '../lib/firebase/comments'
|
||||
|
||||
export const useComments = (contractId: string) => {
|
||||
const [comments, setComments] = useState<Comment[] | 'loading'>('loading')
|
||||
|
||||
useEffect(() => {
|
||||
if (contractId) return listenForComments(contractId, setComments)
|
||||
}, [contractId])
|
||||
|
||||
return comments
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
import _ from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Contract, listenForContracts } from '../lib/firebase/contracts'
|
||||
import { Bet, listenForRecentBets } from '../lib/firebase/bets'
|
||||
import {
|
||||
computeHotContracts,
|
||||
Contract,
|
||||
listenForContracts,
|
||||
} from '../lib/firebase/contracts'
|
||||
|
||||
export const useContracts = () => {
|
||||
const [contracts, setContracts] = useState<Contract[] | 'loading'>('loading')
|
||||
|
@ -10,3 +16,16 @@ export const useContracts = () => {
|
|||
|
||||
return contracts
|
||||
}
|
||||
|
||||
export const useHotContracts = () => {
|
||||
const [recentBets, setRecentBets] = useState<Bet[] | 'loading'>('loading')
|
||||
|
||||
useEffect(() => {
|
||||
const oneDay = 1000 * 60 * 60 * 24
|
||||
return listenForRecentBets(oneDay, setRecentBets)
|
||||
}, [])
|
||||
|
||||
if (recentBets === 'loading') return 'loading'
|
||||
|
||||
return computeHotContracts(recentBets)
|
||||
}
|
||||
|
|
40
web/hooks/use-sort-and-query-params.tsx
Normal file
40
web/hooks/use-sort-and-query-params.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { useRouter } from 'next/router'
|
||||
|
||||
export type Sort =
|
||||
| 'creator'
|
||||
| 'tag'
|
||||
| 'newest'
|
||||
| 'most-traded'
|
||||
| 'resolved'
|
||||
| 'all'
|
||||
|
||||
export function useQueryAndSortParams(options?: { defaultSort: Sort }) {
|
||||
const router = useRouter()
|
||||
|
||||
const { s: sort, q: query } = router.query as {
|
||||
q?: string
|
||||
s?: Sort
|
||||
}
|
||||
|
||||
const setSort = (sort: Sort | undefined) => {
|
||||
router.query.s = sort
|
||||
router.push(router, undefined, { shallow: true })
|
||||
}
|
||||
|
||||
const setQuery = (query: string | undefined) => {
|
||||
if (query) {
|
||||
router.query.q = query
|
||||
} else {
|
||||
delete router.query.q
|
||||
}
|
||||
|
||||
router.push(router, undefined, { shallow: true })
|
||||
}
|
||||
|
||||
return {
|
||||
sort: sort ?? options?.defaultSort ?? 'creator',
|
||||
query: query ?? '',
|
||||
setSort,
|
||||
setQuery,
|
||||
}
|
||||
}
|
165
web/lib/calculate.ts
Normal file
165
web/lib/calculate.ts
Normal file
|
@ -0,0 +1,165 @@
|
|||
import { Bet } from './firebase/bets'
|
||||
import { Contract } from './firebase/contracts'
|
||||
|
||||
const fees = 0.02
|
||||
|
||||
export function getProbability(pool: { YES: number; NO: number }) {
|
||||
const [yesPool, noPool] = [pool.YES, pool.NO]
|
||||
const numerator = Math.pow(yesPool, 2)
|
||||
const denominator = Math.pow(yesPool, 2) + Math.pow(noPool, 2)
|
||||
return numerator / denominator
|
||||
}
|
||||
|
||||
export function getProbabilityAfterBet(
|
||||
pool: { YES: number; NO: number },
|
||||
outcome: 'YES' | 'NO',
|
||||
bet: number
|
||||
) {
|
||||
const [YES, NO] = [
|
||||
pool.YES + (outcome === 'YES' ? bet : 0),
|
||||
pool.NO + (outcome === 'NO' ? bet : 0),
|
||||
]
|
||||
return getProbability({ YES, NO })
|
||||
}
|
||||
|
||||
export function calculateShares(
|
||||
pool: { YES: number; NO: number },
|
||||
bet: number,
|
||||
betChoice: 'YES' | 'NO'
|
||||
) {
|
||||
const [yesPool, noPool] = [pool.YES, pool.NO]
|
||||
|
||||
return betChoice === 'YES'
|
||||
? bet + (bet * noPool ** 2) / (yesPool ** 2 + bet * yesPool)
|
||||
: bet + (bet * yesPool ** 2) / (noPool ** 2 + bet * noPool)
|
||||
}
|
||||
|
||||
export function calculatePayout(
|
||||
contract: Contract,
|
||||
bet: Bet,
|
||||
outcome: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
||||
) {
|
||||
const { amount, outcome: betOutcome, shares } = bet
|
||||
|
||||
if (outcome === 'CANCEL') return amount
|
||||
if (outcome === 'MKT') return calculateMktPayout(contract, bet)
|
||||
|
||||
if (betOutcome !== outcome) return 0
|
||||
|
||||
const { totalShares, totalBets } = contract
|
||||
|
||||
if (totalShares[outcome] === 0) return 0
|
||||
|
||||
const startPool = contract.startPool.YES + contract.startPool.NO
|
||||
const truePool = contract.pool.YES + contract.pool.NO - startPool
|
||||
|
||||
if (totalBets[outcome] >= truePool)
|
||||
return (amount / totalBets[outcome]) * truePool
|
||||
|
||||
const total = totalShares[outcome] - totalBets[outcome]
|
||||
const winningsPool = truePool - totalBets[outcome]
|
||||
|
||||
return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool)
|
||||
}
|
||||
|
||||
export function calculatePayoutAfterCorrectBet(contract: Contract, bet: Bet) {
|
||||
const { amount, outcome, shares } = bet
|
||||
const { totalShares, totalBets } = contract
|
||||
|
||||
const startPool = contract.startPool.YES + contract.startPool.NO
|
||||
const truePool = amount + contract.pool.YES + contract.pool.NO - startPool
|
||||
|
||||
const totalBetsOutcome = totalBets[outcome] + amount
|
||||
const totalSharesOutcome = totalShares[outcome] + shares
|
||||
|
||||
if (totalBetsOutcome >= truePool)
|
||||
return (amount / totalBetsOutcome) * truePool
|
||||
|
||||
const total = totalSharesOutcome - totalBetsOutcome
|
||||
const winningsPool = truePool - totalBetsOutcome
|
||||
|
||||
return (1 - fees) * (amount + ((shares - amount) / total) * winningsPool)
|
||||
}
|
||||
|
||||
function calculateMktPayout(contract: Contract, bet: Bet) {
|
||||
const p =
|
||||
contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2)
|
||||
const weightedTotal =
|
||||
p * contract.totalBets.YES + (1 - p) * contract.totalBets.NO
|
||||
|
||||
const startPool = contract.startPool.YES + contract.startPool.NO
|
||||
const truePool = contract.pool.YES + contract.pool.NO - startPool
|
||||
|
||||
const betP = bet.outcome === 'YES' ? p : 1 - p
|
||||
|
||||
if (weightedTotal >= truePool) {
|
||||
return ((betP * bet.amount) / weightedTotal) * truePool
|
||||
}
|
||||
|
||||
const winningsPool = truePool - weightedTotal
|
||||
|
||||
const weightedShareTotal =
|
||||
p * (contract.totalShares.YES - contract.totalBets.YES) +
|
||||
(1 - p) * (contract.totalShares.NO - contract.totalBets.NO)
|
||||
|
||||
return (
|
||||
(1 - fees) *
|
||||
(betP * bet.amount +
|
||||
((betP * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool)
|
||||
)
|
||||
}
|
||||
|
||||
export function resolvedPayout(contract: Contract, bet: Bet) {
|
||||
if (contract.resolution)
|
||||
return calculatePayout(contract, bet, contract.resolution)
|
||||
throw new Error('Contract was not resolved')
|
||||
}
|
||||
|
||||
export function currentValue(contract: Contract, bet: Bet) {
|
||||
const prob = getProbability(contract.pool)
|
||||
const yesPayout = calculatePayout(contract, bet, 'YES')
|
||||
const noPayout = calculatePayout(contract, bet, 'NO')
|
||||
|
||||
return prob * yesPayout + (1 - prob) * noPayout
|
||||
}
|
||||
|
||||
export function calculateSaleAmount(contract: Contract, bet: Bet) {
|
||||
const { shares, outcome } = bet
|
||||
|
||||
const { YES: yesPool, NO: noPool } = contract.pool
|
||||
const { YES: yesStart, NO: noStart } = contract.startPool
|
||||
const { YES: yesShares, NO: noShares } = contract.totalShares
|
||||
|
||||
const [y, n, s] = [yesPool, noPool, shares]
|
||||
|
||||
const shareValue =
|
||||
outcome === 'YES'
|
||||
? // https://www.wolframalpha.com/input/?i=b+%2B+%28b+n%5E2%29%2F%28y+%28-b+%2B+y%29%29+%3D+c+solve+b
|
||||
(n ** 2 +
|
||||
s * y +
|
||||
y ** 2 -
|
||||
Math.sqrt(
|
||||
n ** 4 + (s - y) ** 2 * y ** 2 + 2 * n ** 2 * y * (s + y)
|
||||
)) /
|
||||
(2 * y)
|
||||
: (y ** 2 +
|
||||
s * n +
|
||||
n ** 2 -
|
||||
Math.sqrt(
|
||||
y ** 4 + (s - n) ** 2 * n ** 2 + 2 * y ** 2 * n * (s + n)
|
||||
)) /
|
||||
(2 * n)
|
||||
|
||||
const startPool = yesStart + noStart
|
||||
const pool = yesPool + noPool - startPool
|
||||
|
||||
const probBefore = yesPool ** 2 / (yesPool ** 2 + noPool ** 2)
|
||||
const f = pool / (probBefore * yesShares + (1 - probBefore) * noShares)
|
||||
|
||||
const myPool = outcome === 'YES' ? yesPool - yesStart : noPool - noStart
|
||||
|
||||
const adjShareValue = Math.min(Math.min(1, f) * shareValue, myPool)
|
||||
|
||||
const saleAmount = (1 - fees) * adjShareValue
|
||||
return saleAmount
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
import { Bet } from '../firebase/bets'
|
||||
import { Contract } from '../firebase/contracts'
|
||||
|
||||
const fees = 0.02
|
||||
|
||||
export function getProbability(pool: { YES: number; NO: number }) {
|
||||
const [yesPool, noPool] = [pool.YES, pool.NO]
|
||||
const numerator = Math.pow(yesPool, 2)
|
||||
const denominator = Math.pow(yesPool, 2) + Math.pow(noPool, 2)
|
||||
return numerator / denominator
|
||||
}
|
||||
|
||||
export function getProbabilityAfterBet(
|
||||
pool: { YES: number; NO: number },
|
||||
outcome: 'YES' | 'NO',
|
||||
bet: number
|
||||
) {
|
||||
const [YES, NO] = [
|
||||
pool.YES + (outcome === 'YES' ? bet : 0),
|
||||
pool.NO + (outcome === 'NO' ? bet : 0),
|
||||
]
|
||||
return getProbability({ YES, NO })
|
||||
}
|
||||
|
||||
export function getDpmWeight(
|
||||
pool: { YES: number; NO: number },
|
||||
bet: number,
|
||||
betChoice: 'YES' | 'NO'
|
||||
) {
|
||||
const [yesPool, noPool] = [pool.YES, pool.NO]
|
||||
|
||||
return betChoice === 'YES'
|
||||
? (bet * Math.pow(noPool, 2)) / (Math.pow(yesPool, 2) + bet * yesPool)
|
||||
: (bet * Math.pow(yesPool, 2)) / (Math.pow(noPool, 2) + bet * noPool)
|
||||
}
|
||||
|
||||
export function calculatePayout(
|
||||
contract: Contract,
|
||||
bet: Bet,
|
||||
outcome: 'YES' | 'NO' | 'CANCEL'
|
||||
) {
|
||||
const { amount, outcome: betOutcome, dpmWeight } = bet
|
||||
|
||||
if (outcome === 'CANCEL') return amount
|
||||
if (betOutcome !== outcome) return 0
|
||||
|
||||
let { dpmWeights, pool, startPool } = contract
|
||||
|
||||
// Fake data if not set.
|
||||
if (!dpmWeights) dpmWeights = { YES: 100, NO: 100 }
|
||||
|
||||
// Fake data if not set.
|
||||
if (!pool) pool = { YES: 100, NO: 100 }
|
||||
|
||||
const otherOutcome = outcome === 'YES' ? 'NO' : 'YES'
|
||||
const poolSize = pool[otherOutcome] - startPool[otherOutcome]
|
||||
|
||||
return (1 - fees) * (dpmWeight / dpmWeights[outcome]) * poolSize + amount
|
||||
}
|
||||
export function resolvedPayout(contract: Contract, bet: Bet) {
|
||||
if (contract.resolution)
|
||||
return calculatePayout(contract, bet, contract.resolution)
|
||||
throw new Error('Contract was not resolved')
|
||||
}
|
||||
|
||||
export function currentValue(contract: Contract, bet: Bet) {
|
||||
const prob = getProbability(contract.pool)
|
||||
const yesPayout = calculatePayout(contract, bet, 'YES')
|
||||
const noPayout = calculatePayout(contract, bet, 'NO')
|
||||
|
||||
return prob * yesPayout + (1 - prob) * noPayout
|
||||
}
|
13
web/lib/firebase/api-call.ts
Normal file
13
web/lib/firebase/api-call.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { getFunctions, httpsCallable } from 'firebase/functions'
|
||||
|
||||
const functions = getFunctions()
|
||||
|
||||
export const cloudFunction = (name: string) => httpsCallable(functions, name)
|
||||
|
||||
export const createContract = cloudFunction('createContract')
|
||||
|
||||
export const placeBet = cloudFunction('placeBet')
|
||||
|
||||
export const resolveMarket = cloudFunction('resolveMarket')
|
||||
|
||||
export const sellBet = cloudFunction('sellBet')
|
|
@ -4,19 +4,31 @@ import {
|
|||
query,
|
||||
onSnapshot,
|
||||
where,
|
||||
getDocs,
|
||||
} from 'firebase/firestore'
|
||||
import _ from 'lodash'
|
||||
import { db } from './init'
|
||||
|
||||
export type Bet = {
|
||||
id: string
|
||||
userId: string
|
||||
contractId: string
|
||||
amount: number // Amount of bet
|
||||
outcome: 'YES' | 'NO' // Chosen outcome
|
||||
dpmWeight: number // Dynamic Parimutuel weight
|
||||
|
||||
amount: number // bet size; negative if SELL bet
|
||||
outcome: 'YES' | 'NO'
|
||||
shares: number // dynamic parimutuel pool weight; negative if SELL bet
|
||||
|
||||
probBefore: number
|
||||
probAverage: number
|
||||
probAfter: number
|
||||
|
||||
sale?: {
|
||||
amount: number // amount user makes from sale
|
||||
betId: string // id of bet being sold
|
||||
// TODO: add sale time?
|
||||
}
|
||||
|
||||
isSold?: boolean // true if this BUY bet has been sold
|
||||
|
||||
createdTime: number
|
||||
}
|
||||
|
||||
|
@ -52,3 +64,34 @@ export function listenForUserBets(
|
|||
setBets(bets)
|
||||
})
|
||||
}
|
||||
|
||||
export function listenForRecentBets(
|
||||
timePeriodMs: number,
|
||||
setBets: (bets: Bet[]) => void
|
||||
) {
|
||||
const recentQuery = query(
|
||||
collectionGroup(db, 'bets'),
|
||||
where('createdTime', '>', Date.now() - timePeriodMs)
|
||||
)
|
||||
return onSnapshot(recentQuery, (snap) => {
|
||||
const bets = snap.docs.map((doc) => doc.data() as Bet)
|
||||
|
||||
bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime)
|
||||
|
||||
setBets(bets)
|
||||
})
|
||||
}
|
||||
|
||||
export async function getRecentBets(timePeriodMs: number) {
|
||||
const recentQuery = query(
|
||||
collectionGroup(db, 'bets'),
|
||||
where('createdTime', '>', Date.now() - timePeriodMs)
|
||||
)
|
||||
|
||||
const snapshot = await getDocs(recentQuery)
|
||||
const bets = snapshot.docs.map((doc) => doc.data() as Bet)
|
||||
|
||||
bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime)
|
||||
|
||||
return bets
|
||||
}
|
||||
|
|
60
web/lib/firebase/comments.ts
Normal file
60
web/lib/firebase/comments.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { doc, collection, onSnapshot, setDoc } from 'firebase/firestore'
|
||||
import { db } from './init'
|
||||
import { User } from './users'
|
||||
|
||||
// Currently, comments are created after the bet, not atomically with the bet.
|
||||
// They're uniquely identified by the pair contractId/betId.
|
||||
export type Comment = {
|
||||
contractId: string
|
||||
betId: string
|
||||
text: string
|
||||
createdTime: number
|
||||
// Denormalized, for rendering comments
|
||||
userName?: string
|
||||
userUsername?: string
|
||||
userAvatarUrl?: string
|
||||
}
|
||||
|
||||
export async function createComment(
|
||||
contractId: string,
|
||||
betId: string,
|
||||
text: string,
|
||||
commenter: User
|
||||
) {
|
||||
const ref = doc(getCommentsCollection(contractId), betId)
|
||||
return await setDoc(ref, {
|
||||
contractId,
|
||||
betId,
|
||||
text,
|
||||
createdTime: Date.now(),
|
||||
userName: commenter.name,
|
||||
userUsername: commenter.username,
|
||||
userAvatarUrl: commenter.avatarUrl,
|
||||
})
|
||||
}
|
||||
|
||||
function getCommentsCollection(contractId: string) {
|
||||
return collection(db, 'contracts', contractId, 'comments')
|
||||
}
|
||||
|
||||
export function listenForComments(
|
||||
contractId: string,
|
||||
setComments: (comments: Comment[]) => void
|
||||
) {
|
||||
return onSnapshot(getCommentsCollection(contractId), (snap) => {
|
||||
const comments = snap.docs.map((doc) => doc.data() as Comment)
|
||||
|
||||
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||
|
||||
setComments(comments)
|
||||
})
|
||||
}
|
||||
|
||||
// Return a map of betId -> comment
|
||||
export function mapCommentsByBetId(comments: Comment[]) {
|
||||
const map: Record<string, Comment> = {}
|
||||
for (const comment of comments) {
|
||||
map[comment.betId] = comment
|
||||
}
|
||||
return map
|
||||
}
|
|
@ -11,9 +11,10 @@ import {
|
|||
onSnapshot,
|
||||
orderBy,
|
||||
getDoc,
|
||||
limit,
|
||||
} from 'firebase/firestore'
|
||||
import dayjs from 'dayjs'
|
||||
import { Bet, getRecentBets } from './bets'
|
||||
import _ from 'lodash'
|
||||
|
||||
export type Contract = {
|
||||
id: string
|
||||
|
@ -30,7 +31,8 @@ export type Contract = {
|
|||
|
||||
startPool: { YES: number; NO: number }
|
||||
pool: { YES: number; NO: number }
|
||||
dpmWeights: { YES: number; NO: number }
|
||||
totalShares: { YES: number; NO: number }
|
||||
totalBets: { YES: number; NO: number }
|
||||
|
||||
createdTime: number // Milliseconds since epoch
|
||||
lastUpdatedTime: number // If the question or description was changed
|
||||
|
@ -48,14 +50,16 @@ export function path(contract: Contract) {
|
|||
|
||||
export function compute(contract: Contract) {
|
||||
const { pool, startPool, createdTime, resolutionTime, isResolved } = contract
|
||||
const volume = pool.YES + pool.NO - startPool.YES - startPool.NO
|
||||
const truePool = pool.YES + pool.NO - startPool.YES - startPool.NO
|
||||
const prob = pool.YES ** 2 / (pool.YES ** 2 + pool.NO ** 2)
|
||||
const probPercent = Math.round(prob * 100) + '%'
|
||||
const startProb =
|
||||
startPool.YES ** 2 / (startPool.YES ** 2 + startPool.NO ** 2)
|
||||
const createdDate = dayjs(createdTime).format('MMM D')
|
||||
const resolvedDate = isResolved
|
||||
? dayjs(resolutionTime).format('MMM D')
|
||||
: undefined
|
||||
return { volume, probPercent, createdDate, resolvedDate }
|
||||
return { truePool, probPercent, startProb, createdDate, resolvedDate }
|
||||
}
|
||||
|
||||
const db = getFirestore(app)
|
||||
|
@ -105,7 +109,7 @@ export async function listContracts(creatorId: string): Promise<Contract[]> {
|
|||
}
|
||||
|
||||
export async function listAllContracts(): Promise<Contract[]> {
|
||||
const q = query(contractCollection, orderBy('createdTime', 'desc'), limit(25))
|
||||
const q = query(contractCollection, orderBy('createdTime', 'desc'))
|
||||
const snapshot = await getDocs(q)
|
||||
return snapshot.docs.map((doc) => doc.data() as Contract)
|
||||
}
|
||||
|
@ -113,7 +117,7 @@ export async function listAllContracts(): Promise<Contract[]> {
|
|||
export function listenForContracts(
|
||||
setContracts: (contracts: Contract[]) => void
|
||||
) {
|
||||
const q = query(contractCollection, orderBy('createdTime', 'desc'), limit(25))
|
||||
const q = query(contractCollection, orderBy('createdTime', 'desc'))
|
||||
return onSnapshot(q, (snap) => {
|
||||
setContracts(snap.docs.map((doc) => doc.data() as Contract))
|
||||
})
|
||||
|
@ -128,3 +132,17 @@ export function listenForContract(
|
|||
setContract((contractSnap.data() ?? null) as Contract | null)
|
||||
})
|
||||
}
|
||||
|
||||
export function computeHotContracts(recentBets: Bet[]) {
|
||||
const contractBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||
const hotContractIds = _.sortBy(Object.keys(contractBets), (contractId) =>
|
||||
_.sumBy(contractBets[contractId], (bet) => -1 * bet.amount)
|
||||
).slice(0, 4)
|
||||
return hotContractIds
|
||||
}
|
||||
|
||||
export async function getHotContracts() {
|
||||
const oneDay = 1000 * 60 * 60 * 24
|
||||
const recentBets = await getRecentBets(oneDay)
|
||||
return computeHotContracts(recentBets)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,30 @@
|
|||
import { getFirestore } from '@firebase/firestore'
|
||||
import { initializeApp } from 'firebase/app'
|
||||
const firebaseConfig = {
|
||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||
authDomain: 'mantic-markets.firebaseapp.com',
|
||||
projectId: 'mantic-markets',
|
||||
storageBucket: 'mantic-markets.appspot.com',
|
||||
messagingSenderId: '128925704902',
|
||||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||
measurementId: 'G-SSFK1Q138D',
|
||||
}
|
||||
|
||||
// TODO: Reenable this when we have a way to set the Firebase db in dev
|
||||
// export const isProd = process.env.NODE_ENV === 'production'
|
||||
export const isProd = true
|
||||
|
||||
const firebaseConfig = isProd
|
||||
? {
|
||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||
authDomain: 'mantic-markets.firebaseapp.com',
|
||||
projectId: 'mantic-markets',
|
||||
storageBucket: 'mantic-markets.appspot.com',
|
||||
messagingSenderId: '128925704902',
|
||||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||
measurementId: 'G-SSFK1Q138D',
|
||||
}
|
||||
: {
|
||||
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
|
||||
authDomain: 'dev-mantic-markets.firebaseapp.com',
|
||||
projectId: 'dev-mantic-markets',
|
||||
storageBucket: 'dev-mantic-markets.appspot.com',
|
||||
messagingSenderId: '134303100058',
|
||||
appId: '1:134303100058:web:27f9ea8b83347251f80323',
|
||||
measurementId: 'G-YJC9E37P37',
|
||||
}
|
||||
|
||||
// Initialize Firebase
|
||||
export const app = initializeApp(firebaseConfig)
|
||||
export const db = getFirestore(app)
|
||||
|
||||
// try {
|
||||
// // Note: this is still throwing a console error atm...
|
||||
// import('firebase/analytics').then((analytics) => {
|
||||
// analytics.getAnalytics(app)
|
||||
// })
|
||||
// } catch (e) {
|
||||
// console.warn('Analytics were blocked')
|
||||
// }
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
signInWithPopup,
|
||||
} from 'firebase/auth'
|
||||
|
||||
export const STARTING_BALANCE = 100
|
||||
export const STARTING_BALANCE = 1000
|
||||
|
||||
export type User = {
|
||||
id: string
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
import {
|
||||
Contract,
|
||||
getContractFromSlug,
|
||||
pushNewContract,
|
||||
setContract,
|
||||
} from '../firebase/contracts'
|
||||
import { User } from '../firebase/users'
|
||||
import { randomString } from '../util/random-string'
|
||||
import { slugify } from '../util/slugify'
|
||||
|
||||
// consider moving to cloud function for security
|
||||
export async function createContract(
|
||||
question: string,
|
||||
description: string,
|
||||
initialProb: number,
|
||||
creator: User
|
||||
) {
|
||||
const proposedSlug = slugify(question).substring(0, 35)
|
||||
|
||||
const preexistingContract = await getContractFromSlug(proposedSlug)
|
||||
|
||||
const slug = preexistingContract
|
||||
? proposedSlug + '-' + randomString()
|
||||
: proposedSlug
|
||||
|
||||
const { startYes, startNo } = calcStartPool(initialProb)
|
||||
|
||||
const contract: Omit<Contract, 'id'> = {
|
||||
slug,
|
||||
outcomeType: 'BINARY',
|
||||
|
||||
creatorId: creator.id,
|
||||
creatorName: creator.name,
|
||||
creatorUsername: creator.username,
|
||||
|
||||
question: question.trim(),
|
||||
description: description.trim(),
|
||||
|
||||
startPool: { YES: startYes, NO: startNo },
|
||||
pool: { YES: startYes, NO: startNo },
|
||||
dpmWeights: { YES: 0, NO: 0 },
|
||||
isResolved: false,
|
||||
|
||||
// TODO: Set create time to Firestore timestamp
|
||||
createdTime: Date.now(),
|
||||
lastUpdatedTime: Date.now(),
|
||||
}
|
||||
|
||||
return await pushNewContract(contract)
|
||||
}
|
||||
|
||||
export function calcStartPool(initialProb: number, initialCapital = 200) {
|
||||
const p = initialProb / 100.0
|
||||
|
||||
const startYes =
|
||||
p === 0.5
|
||||
? p * initialCapital
|
||||
: -(initialCapital * (-p + Math.sqrt((-1 + p) * -p))) / (-1 + 2 * p)
|
||||
|
||||
const startNo = initialCapital - startYes
|
||||
|
||||
return { startYes, startNo }
|
||||
}
|
|
@ -21,10 +21,13 @@ function makeWeights(bids: Bid[]) {
|
|||
// First pass: calculate all the weights
|
||||
for (const { yesBid, noBid } of bids) {
|
||||
const yesWeight =
|
||||
(yesBid * Math.pow(noPot, 2)) / (Math.pow(yesPot, 2) + yesBid * yesPot) ||
|
||||
0
|
||||
yesBid +
|
||||
(yesBid * Math.pow(noPot, 2)) /
|
||||
(Math.pow(yesPot, 2) + yesBid * yesPot) || 0
|
||||
const noWeight =
|
||||
(noBid * Math.pow(yesPot, 2)) / (Math.pow(noPot, 2) + noBid * noPot) || 0
|
||||
noBid +
|
||||
(noBid * Math.pow(yesPot, 2)) / (Math.pow(noPot, 2) + noBid * noPot) ||
|
||||
0
|
||||
|
||||
// Note: Need to calculate weights BEFORE updating pot
|
||||
yesPot += yesBid
|
||||
|
@ -53,15 +56,15 @@ export function makeEntries(bids: Bid[]): Entry[] {
|
|||
const yesWeightsSum = weights.reduce((sum, entry) => sum + entry.yesWeight, 0)
|
||||
const noWeightsSum = weights.reduce((sum, entry) => sum + entry.noWeight, 0)
|
||||
|
||||
const potSize = yesPot + noPot - YES_SEED - NO_SEED
|
||||
|
||||
// Second pass: calculate all the payouts
|
||||
const entries: Entry[] = []
|
||||
|
||||
for (const weight of weights) {
|
||||
const { yesBid, noBid, yesWeight, noWeight } = weight
|
||||
// Payout: You get your initial bid back, as well as your share of the
|
||||
// (noPot - seed) according to your yesWeight
|
||||
const yesPayout = yesBid + (yesWeight / yesWeightsSum) * (noPot - NO_SEED)
|
||||
const noPayout = noBid + (noWeight / noWeightsSum) * (yesPot - YES_SEED)
|
||||
const yesPayout = (yesWeight / yesWeightsSum) * potSize
|
||||
const noPayout = (noWeight / noWeightsSum) * potSize
|
||||
const yesReturn = (yesPayout - yesBid) / yesBid
|
||||
const noReturn = (noPayout - noBid) / noBid
|
||||
entries.push({ ...weight, yesPayout, noPayout, yesReturn, noReturn })
|
||||
|
|
|
@ -6,11 +6,11 @@ const formatter = new Intl.NumberFormat('en-US', {
|
|||
})
|
||||
|
||||
export function formatMoney(amount: number) {
|
||||
return 'M$ ' + formatter.format(amount).substring(1)
|
||||
return 'M$ ' + formatter.format(amount).replace('$', '')
|
||||
}
|
||||
|
||||
export function formatWithCommas(amount: number) {
|
||||
return formatter.format(amount).substring(1)
|
||||
return formatter.format(amount).replace('$', '')
|
||||
}
|
||||
|
||||
export function formatPercent(zeroToOne: number) {
|
||||
|
|
9
web/lib/util/parse.ts
Normal file
9
web/lib/util/parse.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
export function parseTags(text: string) {
|
||||
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
|
||||
const matches = (text.match(regex) || []).map((match) =>
|
||||
match.trim().substring(1)
|
||||
)
|
||||
return _.uniqBy(matches, (tag) => tag.toLowerCase())
|
||||
}
|
|
@ -22,7 +22,8 @@
|
|||
"lodash": "4.17.21",
|
||||
"next": "12.0.7",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
"react-dom": "17.0.2",
|
||||
"react-expanding-textarea": "^2.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "0.4.0",
|
||||
|
|
|
@ -11,7 +11,11 @@ import { useBets } from '../../hooks/use-bets'
|
|||
import { Title } from '../../components/title'
|
||||
import { Spacer } from '../../components/layout/spacer'
|
||||
import { User } from '../../lib/firebase/users'
|
||||
import { Contract, getContractFromSlug } from '../../lib/firebase/contracts'
|
||||
import {
|
||||
compute,
|
||||
Contract,
|
||||
getContractFromSlug,
|
||||
} from '../../lib/firebase/contracts'
|
||||
import { SEO } from '../../components/SEO'
|
||||
import { Page } from '../../components/page'
|
||||
|
||||
|
@ -47,14 +51,23 @@ export default function ContractPage(props: {
|
|||
return <div>Contract not found...</div>
|
||||
}
|
||||
|
||||
const { creatorId, isResolved } = contract
|
||||
const { creatorId, isResolved, resolution, question } = contract
|
||||
const isCreator = user?.id === creatorId
|
||||
const allowTrade =
|
||||
!isResolved && (!contract.closeTime || contract.closeTime > Date.now())
|
||||
const allowResolve = !isResolved && isCreator && user
|
||||
|
||||
const { probPercent } = compute(contract)
|
||||
|
||||
const description = resolution
|
||||
? `Resolved ${resolution}. ${contract.description}`
|
||||
: `${probPercent} chance. ${contract.description}`
|
||||
|
||||
return (
|
||||
<Page wide={!isResolved}>
|
||||
<Page wide={allowTrade}>
|
||||
<SEO
|
||||
title={contract.question}
|
||||
description={contract.description}
|
||||
title={question}
|
||||
description={description}
|
||||
url={`/${props.username}/${props.slug}`}
|
||||
/>
|
||||
|
||||
|
@ -64,14 +77,13 @@ export default function ContractPage(props: {
|
|||
<BetsSection contract={contract} user={user ?? null} />
|
||||
</div>
|
||||
|
||||
{!isResolved && (
|
||||
{(allowTrade || allowResolve) && (
|
||||
<>
|
||||
<div className="md:ml-8" />
|
||||
|
||||
<Col className="flex-1">
|
||||
<BetPanel contract={contract} />
|
||||
|
||||
{isCreator && user && (
|
||||
{allowTrade && <BetPanel contract={contract} />}
|
||||
{allowResolve && (
|
||||
<ResolutionPanel creator={user} contract={contract} />
|
||||
)}
|
||||
</Col>
|
||||
|
@ -97,8 +109,8 @@ function BetsSection(props: { contract: Contract; user: User | null }) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Title text="Your bets" />
|
||||
<MyBetsSummary contract={contract} bets={userBets} />
|
||||
<Title text="Your trades" />
|
||||
<MyBetsSummary contract={contract} bets={userBets} showMKT />
|
||||
<Spacer h={6} />
|
||||
<ContractBetsTable contract={contract} bets={userBets} />
|
||||
<Spacer h={12} />
|
||||
|
|
|
@ -6,32 +6,35 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Mantic Markets</title>
|
||||
<title>Manifold Markets</title>
|
||||
|
||||
<meta
|
||||
property="og:title"
|
||||
name="twitter:title"
|
||||
content="Mantic Markets"
|
||||
content="Manifold Markets"
|
||||
key="title"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Mantic Markets is creating better forecasting through user-created prediction markets."
|
||||
content="Manifold Markets is creating better forecasting through user-created prediction markets."
|
||||
key="description1"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
name="twitter:description"
|
||||
content="Mantic Markets is creating better forecasting through user-created prediction markets."
|
||||
content="Manifold Markets is creating better forecasting through user-created prediction markets."
|
||||
key="description2"
|
||||
/>
|
||||
<meta property="og:url" content="https://mantic.markets" key="url" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@manticmarkets" />
|
||||
<meta property="og:url" content="https://manifold.markets" key="url" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@manifoldmarkets" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://manifold.markets/logo-cover.png"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://mantic.markets/logo-cover.png"
|
||||
content="https://manifold.markets/logo-bg.png"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ function Contents() {
|
|||
<h1 id="about">About</h1>
|
||||
<hr />
|
||||
<p>
|
||||
Mantic Markets is creating better forecasting through user-created
|
||||
Manifold Markets is creating better forecasting through user-created
|
||||
prediction markets.
|
||||
</p>
|
||||
<p>
|
||||
|
@ -77,49 +77,51 @@ function Contents() {
|
|||
</a>
|
||||
. This is the power of prediction markets!
|
||||
</p>
|
||||
<h3 id="how-does-mantic-markets-work-">How does Mantic Markets work?</h3>
|
||||
<h3 id="how-does-manifold-markets-work-">
|
||||
How does Manifold Markets work?
|
||||
</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>
|
||||
Anyone can create a market for any yes-or-no question.
|
||||
</strong>
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
You can ask questions about the future like "Will Taiwan remove its
|
||||
14-day COVID quarantine by Jun 01, 2022?" Then use the information
|
||||
to plan your trip.
|
||||
</p>
|
||||
<p>
|
||||
You can also ask subjective, personal questions like "Will I enjoy
|
||||
my 2022 Taiwan trip?". Then share the market with your family and
|
||||
friends.
|
||||
</p>
|
||||
<ol>
|
||||
<p>
|
||||
You can ask questions about the future like "Will Taiwan remove
|
||||
its 14-day COVID quarantine by Jun 01, 2022?" Then use the
|
||||
information to plan your trip.
|
||||
</p>
|
||||
<p>
|
||||
You can also ask subjective, personal questions like "Will I
|
||||
enjoy my 2022 Taiwan trip?". Then share the market with your
|
||||
family and friends.
|
||||
</p>
|
||||
<li>
|
||||
<strong>
|
||||
Anyone can bet on a market using Mantic Dollars (M$), our platform
|
||||
Anyone can bet on a market using Manifold Dollars (M$), our platform
|
||||
currency.
|
||||
</strong>
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
You get M$ 100 just for signing up, so you can start betting
|
||||
You get M$ 1,000 just for signing up, so you can start betting
|
||||
immediately! When a market creator decides an outcome in your favor,
|
||||
you'll win money from people who bet against you.
|
||||
you'll win Manifold Dollars from people who bet against you.
|
||||
</p>
|
||||
<p>
|
||||
{/* <p>
|
||||
If you run out of money, you can purchase more at a rate of $1 USD to M$
|
||||
100. (Note that Mantic Dollars are not convertible to cash and can only
|
||||
100. (Note that Manifold Dollars are not convertible to cash and can only
|
||||
be used within our platform.)
|
||||
</p>
|
||||
</p> */}
|
||||
<aside>
|
||||
💡 We're still in Open Beta; we'll tweak this model and
|
||||
periodically reset balances before our official launch. If you purchase
|
||||
any M$ during the beta, we promise to honor that when we launch!
|
||||
💡 We're still in Open Beta; we'll tweak the amounts of Manifold
|
||||
Dollars given out and periodically reset balances before our official
|
||||
launch.
|
||||
{/* If you purchase
|
||||
any M$ during the beta, we promise to honor that when we launch! */}
|
||||
</aside>
|
||||
|
||||
<h3 id="why-do-i-want-to-bet-with-play-money-">
|
||||
{/* <h3 id="why-do-i-want-to-bet-with-play-money-">
|
||||
Why do I want to bet with play-money?
|
||||
</h3>
|
||||
<p>
|
||||
|
@ -130,7 +132,7 @@ function Contents() {
|
|||
</p>
|
||||
<p>By buying M$, you support:</p>
|
||||
<ul>
|
||||
<li>The continued development of Mantic Markets</li>
|
||||
<li>The continued development of Manifold Markets</li>
|
||||
<li>Cash payouts to market creators (TBD)</li>
|
||||
<li>Forecasting tournaments for bettors (TBD)</li>
|
||||
</ul>
|
||||
|
@ -138,7 +140,7 @@ function Contents() {
|
|||
We also have some thoughts on how to reward bettors: physical swag,
|
||||
exclusive conversations with market creators, NFTs...? If you have
|
||||
ideas, let us know!
|
||||
</p>
|
||||
</p> */}
|
||||
<h3 id="can-prediction-markets-work-without-real-money-">
|
||||
Can prediction markets work without real money?
|
||||
</h3>
|
||||
|
@ -162,8 +164,8 @@ function Contents() {
|
|||
</p>
|
||||
<h3 id="how-are-markets-resolved-">How are markets resolved?</h3>
|
||||
<p>
|
||||
The creator of the prediction market decides the outcome and earns 0.5%
|
||||
of the trade volume for their effort.
|
||||
The creator of the prediction market decides the outcome and earns 1% of
|
||||
the betting pool for their effort.
|
||||
</p>
|
||||
<p>
|
||||
This simple resolution mechanism has surprising benefits in allowing a
|
||||
|
@ -200,34 +202,52 @@ function Contents() {
|
|||
<h3 id="how-is-this-different-from-metaculus-or-hypermind-">
|
||||
How is this different from Metaculus or Hypermind?
|
||||
</h3>
|
||||
<p>
|
||||
{/* <p>
|
||||
We believe that in order to get the best results, you have to have skin
|
||||
in the game. We require that people use real money to buy the currency
|
||||
they use on our platform.
|
||||
</p>
|
||||
<p>
|
||||
With Mantic Dollars being a scarce resource, people will bet more
|
||||
With Manifold Dollars being a scarce resource, people will bet more
|
||||
carefully and can't rig the outcome by creating multiple accounts.
|
||||
The result is more accurate predictions.
|
||||
</p>
|
||||
</p> */}
|
||||
<p>
|
||||
Mantic Markets is also focused on accessibility and allowing anyone to
|
||||
Manifold Markets is focused on accessibility and allowing anyone to
|
||||
quickly create and judge a prediction market. When we all have the power
|
||||
to create and share prediction markets in seconds and apply our own
|
||||
judgment on the outcome, it leads to a qualitative shift in the number,
|
||||
variety, and usefulness of prediction markets.
|
||||
</p>
|
||||
<h3 id="how-does-betting-in-a-market-work-on-a-technical-level-">
|
||||
How does betting in a market work on a technical level?
|
||||
</h3>
|
||||
|
||||
<h3 id="how-does-betting-work">How does betting work?</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Markets are structured around a question with a binary outcome (either
|
||||
YES or NO)
|
||||
</li>
|
||||
<li>
|
||||
Traders can place a bet on either YES or NO. The bet amount is added
|
||||
to the corresponding bet pool for the outcome. The trader receives
|
||||
some shares of the final pool. The number of shares depends on the
|
||||
current implied probability.
|
||||
</li>
|
||||
<li>
|
||||
When the market is resolved, the traders who bet on the correct
|
||||
outcome are paid out of the total pool (YES pool + NO pool) in
|
||||
proportion to the amount of shares they own, minus any fees.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="type-of-market-maker">What kind of betting system do you use?</h3>
|
||||
<p>
|
||||
Mantic Markets uses a special type of automated market marker based on a
|
||||
dynamic pari-mutuel (DPM) betting system.
|
||||
Manifold Markets uses a special type of automated market marker based on
|
||||
a dynamic pari-mutuel (DPM) betting system.
|
||||
</p>
|
||||
<p>
|
||||
Like traditional pari-mutuel systems, your payoff is not known at the
|
||||
time you place your bet (it's dependent on the size of the pot when
|
||||
the event ends).
|
||||
time you place your bet (it's dependent on the size of the pool when
|
||||
the event is resolved).
|
||||
</p>
|
||||
<p>
|
||||
Unlike traditional pari-mutuel systems, the price or probability that
|
||||
|
@ -238,8 +258,9 @@ function Contents() {
|
|||
The result is a market that can function well when trading volume is low
|
||||
without any risk to the market creator.
|
||||
</p>
|
||||
|
||||
<h3 id="who-are-we-">Who are we?</h3>
|
||||
<p>Mantic Markets is currently a team of three:</p>
|
||||
<p>Manifold Markets is currently a team of three:</p>
|
||||
<ul>
|
||||
<li>James Grugett</li>
|
||||
<li>Stephen Grugett</li>
|
||||
|
@ -259,7 +280,7 @@ function Contents() {
|
|||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Email: <code>info@mantic.markets</code>
|
||||
Email: <code>info@manifold.markets</code>
|
||||
</li>
|
||||
<li>
|
||||
Office hours:{' '}
|
||||
|
@ -269,12 +290,19 @@ function Contents() {
|
|||
</ul>
|
||||
<p>
|
||||
<a href="https://discord.gg/eHQBNBqXuh">
|
||||
Join the Mantic Markets Discord Server!
|
||||
Join the Manifold Markets Discord Server!
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 id="further-reading">Further Reading</h1>
|
||||
<hr />
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://manifoldmarkets.notion.site/Technical-Overview-b9b48a09ea1f45b88d991231171730c5">
|
||||
Technical Overview of Manifold Markets
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://en.wikipedia.org/wiki/Prediction_market">
|
||||
Wikipedia: Prediction markets
|
||||
|
@ -296,7 +324,7 @@ function Contents() {
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://mantic.markets/simulator">
|
||||
<a href="https://manifold.markets/simulator">
|
||||
Dynamic parimutuel market simulator
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import router from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
|
||||
import { ContractsList } from '../components/contracts-list'
|
||||
import { CreatorContractsList } from '../components/contracts-list'
|
||||
import { Spacer } from '../components/layout/spacer'
|
||||
import { Title } from '../components/title'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { path } from '../lib/firebase/contracts'
|
||||
import { createContract } from '../lib/service/create-contract'
|
||||
import { Contract, path } from '../lib/firebase/contracts'
|
||||
import { Page } from '../components/page'
|
||||
import { formatMoney } from '../lib/util/format'
|
||||
import { AdvancedPanel } from '../components/advanced-panel'
|
||||
import { createContract } from '../lib/firebase/api-call'
|
||||
import { Row } from '../components/layout/row'
|
||||
|
||||
// Allow user to create a new contract
|
||||
export default function NewContract() {
|
||||
|
@ -15,26 +21,69 @@ export default function NewContract() {
|
|||
|
||||
useEffect(() => {
|
||||
if (creator === null) router.push('/')
|
||||
})
|
||||
}, [creator])
|
||||
|
||||
useEffect(() => {
|
||||
// warm up function
|
||||
createContract({}).catch()
|
||||
}, [])
|
||||
|
||||
const [initialProb, setInitialProb] = useState(50)
|
||||
const [question, setQuestion] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
|
||||
const [ante, setAnte] = useState<number | undefined>(0)
|
||||
const [anteError, setAnteError] = useState('')
|
||||
const [closeDate, setCloseDate] = useState('')
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const closeTime = dateToMillis(closeDate) || undefined
|
||||
// We'd like this to look like "Apr 2, 2022, 23:59:59 PM PT" but timezones are hard with dayjs
|
||||
const formattedCloseTime = closeTime ? new Date(closeTime).toString() : ''
|
||||
|
||||
const user = useUser()
|
||||
const remainingBalance = (user?.balance || 0) - (ante || 0)
|
||||
|
||||
const isValid =
|
||||
initialProb > 0 &&
|
||||
initialProb < 100 &&
|
||||
question.length > 0 &&
|
||||
(ante === undefined || (ante >= 0 && ante <= remainingBalance)) &&
|
||||
// If set, closeTime must be in the future
|
||||
(!closeTime || closeTime > Date.now())
|
||||
|
||||
async function submit() {
|
||||
// TODO: add more rigorous error handling for question
|
||||
if (!creator || !question) return
|
||||
// TODO: Tell users why their contract is invalid
|
||||
if (!creator || !isValid) return
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
const contract = await createContract(
|
||||
const result: any = await createContract({
|
||||
question,
|
||||
description,
|
||||
initialProb,
|
||||
creator
|
||||
)
|
||||
await router.push(path(contract))
|
||||
ante,
|
||||
closeTime: closeTime || undefined,
|
||||
}).then((r) => r.data || {})
|
||||
|
||||
if (result.status !== 'success') {
|
||||
console.log('error creating contract', result)
|
||||
return
|
||||
}
|
||||
|
||||
await router.push(path(result.contract as Contract))
|
||||
}
|
||||
|
||||
function onAnteChange(str: string) {
|
||||
const amount = parseInt(str)
|
||||
|
||||
if (str && isNaN(amount)) return
|
||||
|
||||
setAnte(str ? amount : undefined)
|
||||
|
||||
if (user && user.balance < amount) setAnteError('Insufficient balance')
|
||||
else setAnteError('')
|
||||
}
|
||||
|
||||
const descriptionPlaceholder = `e.g. This market will resolve to “Yes” if, by June 2, 2021, 11:59:59 PM ET, Paxlovid (also known under PF-07321332)...`
|
||||
|
@ -45,19 +94,19 @@ export default function NewContract() {
|
|||
<Page>
|
||||
<Title text="Create a new prediction market" />
|
||||
|
||||
<div className="w-full bg-gray-100 rounded-lg shadow-xl px-6 py-4">
|
||||
<div className="w-full bg-gray-100 rounded-lg shadow-md px-6 py-4">
|
||||
{/* Create a Tailwind form that takes in all the fields needed for a new contract */}
|
||||
{/* When the form is submitted, create a new contract in the database */}
|
||||
<form>
|
||||
<div className="form-control">
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text">Question</span>
|
||||
<span className="mb-1">Question</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Will the FDA approve Paxlovid before Jun 2nd, 2022?"
|
||||
className="input"
|
||||
<Textarea
|
||||
placeholder="e.g. Will the FDA will approve Paxlovid before Jun 2nd, 2022?"
|
||||
className="input input-bordered resize-none"
|
||||
disabled={isSubmitting}
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value || '')}
|
||||
/>
|
||||
|
@ -67,49 +116,120 @@ export default function NewContract() {
|
|||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Description (optional)</span>
|
||||
<span className="mb-1">Initial probability</span>
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
className="textarea h-24 textarea-bordered"
|
||||
placeholder={descriptionPlaceholder}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value || '')}
|
||||
></textarea>
|
||||
<Row className="items-center gap-2">
|
||||
<label className="input-group input-group-lg w-fit text-xl">
|
||||
<input
|
||||
type="number"
|
||||
value={initialProb}
|
||||
className="input input-bordered input-md text-primary text-3xl w-24"
|
||||
disabled={isSubmitting}
|
||||
min={1}
|
||||
max={99}
|
||||
onChange={(e) =>
|
||||
setInitialProb(parseInt(e.target.value.substring(0, 2)))
|
||||
}
|
||||
/>
|
||||
<span>%</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
className="range range-primary"
|
||||
min={1}
|
||||
max={99}
|
||||
value={initialProb}
|
||||
onChange={(e) => setInitialProb(parseInt(e.target.value))}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">
|
||||
Initial probability: {initialProb}%
|
||||
</span>
|
||||
<span className="mb-1">Description</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
className="range range-lg range-primary"
|
||||
min="1"
|
||||
max={99}
|
||||
value={initialProb}
|
||||
onChange={(e) => setInitialProb(parseInt(e.target.value))}
|
||||
<Textarea
|
||||
className="textarea w-full textarea-bordered"
|
||||
rows={3}
|
||||
placeholder={descriptionPlaceholder}
|
||||
value={description}
|
||||
disabled={isSubmitting}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setDescription(e.target.value || '')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdvancedPanel>
|
||||
<div className="form-control mb-1">
|
||||
<label className="label">
|
||||
<span className="mb-1">Subsidize your market</span>
|
||||
</label>
|
||||
|
||||
<label className="input-group">
|
||||
<span className="text-sm ">M$</span>
|
||||
<input
|
||||
className={clsx(
|
||||
'input input-bordered',
|
||||
anteError && 'input-error'
|
||||
)}
|
||||
type="text"
|
||||
placeholder="0"
|
||||
maxLength={9}
|
||||
value={ante ?? ''}
|
||||
disabled={isSubmitting}
|
||||
onChange={(e) => onAnteChange(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span className="label-text text-gray-400 ml-1">
|
||||
Remaining balance:{' '}
|
||||
{formatMoney(remainingBalance > 0 ? remainingBalance : 0)}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="mb-1">Close date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="input input-bordered"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setCloseDate(e.target.value || '')}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
disabled={isSubmitting}
|
||||
value={closeDate}
|
||||
/>
|
||||
</div>
|
||||
<label>
|
||||
<span className="label-text text-gray-400 ml-1">
|
||||
No new trades will be allowed after{' '}
|
||||
{closeDate ? formattedCloseTime : 'this time'}
|
||||
</span>
|
||||
</label>
|
||||
</AdvancedPanel>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="flex justify-end my-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={isSubmitting || !question}
|
||||
className={clsx(
|
||||
'btn btn-primary',
|
||||
isSubmitting && 'loading disabled'
|
||||
)}
|
||||
disabled={isSubmitting || !isValid}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
}}
|
||||
>
|
||||
Create market
|
||||
{isSubmitting ? 'Creating...' : 'Create market'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -119,7 +239,17 @@ export default function NewContract() {
|
|||
|
||||
<Title text="Your markets" />
|
||||
|
||||
{creator && <ContractsList creator={creator} />}
|
||||
{creator && <CreatorContractsList creator={creator} />}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
// Given a date string like '2022-04-02',
|
||||
// return the time just before midnight on that date (in the user's local time), as millis since epoch
|
||||
function dateToMillis(date: string) {
|
||||
return dayjs(date)
|
||||
.set('hour', 23)
|
||||
.set('minute', 59)
|
||||
.set('second', 59)
|
||||
.valueOf()
|
||||
}
|
||||
|
|
|
@ -3,26 +3,42 @@ import React from 'react'
|
|||
import { useUser } from '../hooks/use-user'
|
||||
import Markets from './markets'
|
||||
import LandingPage from './landing-page'
|
||||
import { Contract, listAllContracts } from '../lib/firebase/contracts'
|
||||
import {
|
||||
Contract,
|
||||
getHotContracts,
|
||||
listAllContracts,
|
||||
} from '../lib/firebase/contracts'
|
||||
import _ from 'lodash'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const contracts = await listAllContracts().catch((_) => [])
|
||||
const [contracts, hotContractIds] = await Promise.all([
|
||||
listAllContracts().catch((_) => []),
|
||||
getHotContracts().catch(() => []),
|
||||
])
|
||||
|
||||
return {
|
||||
props: {
|
||||
contracts,
|
||||
hotContractIds,
|
||||
},
|
||||
|
||||
revalidate: 60, // regenerate after a minute
|
||||
}
|
||||
}
|
||||
|
||||
const Home = (props: { contracts: Contract[] }) => {
|
||||
const Home = (props: { contracts: Contract[]; hotContractIds: string[] }) => {
|
||||
const user = useUser()
|
||||
|
||||
if (user === undefined) return <></>
|
||||
|
||||
return user ? <Markets contracts={props.contracts} /> : <LandingPage />
|
||||
return user ? (
|
||||
<Markets
|
||||
contracts={props.contracts}
|
||||
hotContractIds={props.hotContractIds}
|
||||
/>
|
||||
) : (
|
||||
<LandingPage />
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
|
|
@ -13,6 +13,7 @@ import { SearchableGrid } from '../components/contracts-list'
|
|||
import { Col } from '../components/layout/col'
|
||||
import { NavBar } from '../components/nav-bar'
|
||||
import Link from 'next/link'
|
||||
import { useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
|
@ -93,7 +94,7 @@ function FeaturesSection() {
|
|||
{
|
||||
name: 'Play money, real results',
|
||||
description:
|
||||
'Get accurate predictions by betting with Mantic Dollars, our virtual currency.',
|
||||
'Get accurate predictions by betting with Manifold Dollars, our virtual currency.',
|
||||
icon: LightningBoltIcon,
|
||||
},
|
||||
{
|
||||
|
@ -116,7 +117,7 @@ function FeaturesSection() {
|
|||
<div className="max-w-7xl mx-auto px-6 lg:px-8">
|
||||
<div className="lg:text-center">
|
||||
<h2 className="text-base text-teal-600 font-semibold tracking-wide uppercase">
|
||||
Mantic Markets
|
||||
Manifold Markets
|
||||
</h2>
|
||||
<p className="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl">
|
||||
Better forecasting for everyone
|
||||
|
@ -159,13 +160,20 @@ function FeaturesSection() {
|
|||
|
||||
function ExploreMarketsSection() {
|
||||
const contracts = useContracts()
|
||||
const { query, setQuery, sort, setSort } = useQueryAndSortParams()
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl px-4 py-8 mx-auto">
|
||||
<p className="my-12 text-3xl leading-8 font-extrabold tracking-tight text-indigo-700 sm:text-4xl">
|
||||
Explore our markets
|
||||
</p>
|
||||
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />
|
||||
<SearchableGrid
|
||||
contracts={contracts === 'loading' ? [] : contracts}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
sort={sort}
|
||||
setSort={setSort}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
224
web/pages/make-predictions.tsx
Normal file
224
web/pages/make-predictions.tsx
Normal file
|
@ -0,0 +1,224 @@
|
|||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Col } from '../components/layout/col'
|
||||
import { Row } from '../components/layout/row'
|
||||
import { Spacer } from '../components/layout/spacer'
|
||||
import { Linkify } from '../components/linkify'
|
||||
import { Page } from '../components/page'
|
||||
import { Title } from '../components/title'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { createContract } from '../lib/firebase/api-call'
|
||||
import { compute, Contract, path } from '../lib/firebase/contracts'
|
||||
|
||||
type Prediction = {
|
||||
question: string
|
||||
description: string
|
||||
initialProb: number
|
||||
createdUrl?: string
|
||||
}
|
||||
|
||||
function toPrediction(contract: Contract): Prediction {
|
||||
const { startProb } = compute(contract)
|
||||
return {
|
||||
question: contract.question,
|
||||
description: contract.description,
|
||||
initialProb: startProb * 100,
|
||||
createdUrl: path(contract),
|
||||
}
|
||||
}
|
||||
|
||||
function PredictionRow(props: { prediction: Prediction }) {
|
||||
const { prediction } = props
|
||||
return (
|
||||
<Row className="gap-4 justify-between hover:bg-gray-300 p-4">
|
||||
<Col className="justify-between">
|
||||
<div className="font-medium text-indigo-700 mb-2">
|
||||
<Linkify text={prediction.question} />
|
||||
</div>
|
||||
<div className="text-gray-500 text-sm">{prediction.description}</div>
|
||||
</Col>
|
||||
{/* Initial probability */}
|
||||
<div className="ml-auto">
|
||||
<div className="text-3xl">
|
||||
<div className="text-primary">
|
||||
{prediction.initialProb.toFixed(0)}%
|
||||
<div className="text-lg">chance</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Current probability; hidden for now */}
|
||||
{/* <div>
|
||||
<div className="text-3xl">
|
||||
<div className="text-primary">
|
||||
{prediction.initialProb}%<div className="text-lg">chance</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
function PredictionList(props: { predictions: Prediction[] }) {
|
||||
const { predictions } = props
|
||||
return (
|
||||
<Col className="divide-gray-300 divide-y border-gray-300 border rounded-md">
|
||||
{predictions.map((prediction) =>
|
||||
prediction.createdUrl ? (
|
||||
<Link href={prediction.createdUrl}>
|
||||
<a>
|
||||
<PredictionRow
|
||||
key={prediction.question}
|
||||
prediction={prediction}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<PredictionRow key={prediction.question} prediction={prediction} />
|
||||
)
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
const TEST_VALUE = `1. Biden approval rating (as per 538) is greater than 50%: 80%
|
||||
2. Court packing is clearly going to happen (new justices don't have to be appointed by end of year): 5%
|
||||
3. Yang is New York mayor: 80%
|
||||
4. Newsom recalled as CA governor: 5%
|
||||
5. At least $250 million in damage from BLM protests this year: 30%
|
||||
6. Significant capital gains tax hike (above 30% for highest bracket): 20%`
|
||||
|
||||
export default function MakePredictions() {
|
||||
const user = useUser()
|
||||
const [predictionsString, setPredictionsString] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [createdContracts, setCreatedContracts] = useState<Contract[]>([])
|
||||
|
||||
const bulkPlaceholder = `e.g.
|
||||
${TEST_VALUE}
|
||||
...
|
||||
`
|
||||
|
||||
const predictions: Prediction[] = []
|
||||
|
||||
// Parse bulkContracts, then run createContract for each
|
||||
const lines = predictionsString ? predictionsString.split('\n') : []
|
||||
for (const line of lines) {
|
||||
// Parse line with regex
|
||||
const matches = line.match(/^(.*):\s*(\d+)%\s*$/) || ['', '', '']
|
||||
const [_, question, prob] = matches
|
||||
|
||||
if (!question || !prob) {
|
||||
console.error('Invalid prediction: ', line)
|
||||
continue
|
||||
}
|
||||
|
||||
predictions.push({
|
||||
question,
|
||||
description,
|
||||
initialProb: parseInt(prob),
|
||||
})
|
||||
}
|
||||
|
||||
async function createContracts() {
|
||||
if (!user) {
|
||||
// TODO: Convey error with snackbar/toast
|
||||
console.error('You need to be signed in!')
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
for (const prediction of predictions) {
|
||||
const contract = await createContract({
|
||||
question: prediction.question,
|
||||
description: prediction.description,
|
||||
initialProb: prediction.initialProb,
|
||||
}).then((r) => (r.data as any).contract)
|
||||
|
||||
setCreatedContracts((prev) => [...prev, contract])
|
||||
}
|
||||
setPredictionsString('')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Title text="Make Predictions" />
|
||||
<div className="w-full bg-gray-100 rounded-lg shadow-xl px-6 py-4">
|
||||
<form>
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Prediction</span>
|
||||
<div className="text-sm text-gray-500 ml-1">
|
||||
One prediction per line, each formatted like "The sun will rise
|
||||
tomorrow: 99%"
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
className="textarea h-60 textarea-bordered"
|
||||
placeholder={bulkPlaceholder}
|
||||
value={predictionsString}
|
||||
onChange={(e) => setPredictionsString(e.target.value || '')}
|
||||
></textarea>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text">Tags</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. #ACX2021 #World"
|
||||
className="input"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value || '')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{predictions.length > 0 && (
|
||||
<div>
|
||||
<Spacer h={4} />
|
||||
<label className="label">
|
||||
<span className="label-text">Preview</span>
|
||||
</label>
|
||||
<PredictionList predictions={predictions} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="flex justify-end my-4">
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx('btn btn-primary', {
|
||||
loading: isSubmitting,
|
||||
})}
|
||||
disabled={predictions.length === 0 || isSubmitting}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
createContracts()
|
||||
}}
|
||||
>
|
||||
Create all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{createdContracts.length > 0 && (
|
||||
<>
|
||||
<Spacer h={16} />
|
||||
<Title text="Created Predictions" />
|
||||
<div className="w-full bg-gray-100 rounded-lg shadow-xl px-6 py-4">
|
||||
<PredictionList predictions={createdContracts.map(toPrediction)} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
)
|
||||
}
|
|
@ -1,28 +1,65 @@
|
|||
import { SearchableGrid } from '../components/contracts-list'
|
||||
import _ from 'lodash'
|
||||
import { ContractsGrid, SearchableGrid } from '../components/contracts-list'
|
||||
import { Spacer } from '../components/layout/spacer'
|
||||
import { Page } from '../components/page'
|
||||
import { useContracts } from '../hooks/use-contracts'
|
||||
import { Contract, listAllContracts } from '../lib/firebase/contracts'
|
||||
import { Title } from '../components/title'
|
||||
import { useContracts, useHotContracts } from '../hooks/use-contracts'
|
||||
import { useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
||||
import {
|
||||
Contract,
|
||||
getHotContracts,
|
||||
listAllContracts,
|
||||
} from '../lib/firebase/contracts'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const contracts = await listAllContracts().catch((_) => [])
|
||||
const [contracts, hotContractIds] = await Promise.all([
|
||||
listAllContracts().catch((_) => []),
|
||||
getHotContracts().catch(() => []),
|
||||
])
|
||||
|
||||
return {
|
||||
props: {
|
||||
contracts,
|
||||
hotContractIds,
|
||||
},
|
||||
|
||||
revalidate: 60, // regenerate after a minute
|
||||
}
|
||||
}
|
||||
|
||||
export default function Markets(props: { contracts: Contract[] }) {
|
||||
export default function Markets(props: {
|
||||
contracts: Contract[]
|
||||
hotContractIds: string[]
|
||||
}) {
|
||||
const contracts = useContracts()
|
||||
const { query, setQuery, sort, setSort } = useQueryAndSortParams()
|
||||
const hotContractIds = useHotContracts()
|
||||
|
||||
const readyHotContractIds =
|
||||
hotContractIds === 'loading' ? props.hotContractIds : hotContractIds
|
||||
const readyContracts = contracts === 'loading' ? props.contracts : contracts
|
||||
|
||||
const hotContracts = readyHotContractIds.map(
|
||||
(hotId) =>
|
||||
_.find(readyContracts, (contract) => contract.id === hotId) as Contract
|
||||
)
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="w-full bg-indigo-50 border-2 border-indigo-100 p-6 rounded-lg shadow-md">
|
||||
<Title className="mt-0" text="🔥 Markets" />
|
||||
<ContractsGrid contracts={hotContracts} />
|
||||
</div>
|
||||
|
||||
<Spacer h={10} />
|
||||
|
||||
{(props.contracts || contracts !== 'loading') && (
|
||||
<SearchableGrid
|
||||
contracts={contracts === 'loading' ? props.contracts : contracts}
|
||||
contracts={readyContracts}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
sort={sort}
|
||||
setSort={setSort}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
|
|
|
@ -86,7 +86,7 @@ function TableRowEnd(props: { entry: Entry | null; isNew?: boolean }) {
|
|||
return (
|
||||
<>
|
||||
<td>{(entry.prob * 100).toFixed(1)}%</td>
|
||||
<td>${(entry.yesBid + entry.yesWeight).toFixed(0)}</td>
|
||||
<td>${entry.yesWeight.toFixed(0)}</td>
|
||||
{!props.isNew && (
|
||||
<>
|
||||
<td>${entry.yesPayout.toFixed(0)}</td>
|
||||
|
@ -99,7 +99,7 @@ function TableRowEnd(props: { entry: Entry | null; isNew?: boolean }) {
|
|||
return (
|
||||
<>
|
||||
<td>{(entry.prob * 100).toFixed(1)}%</td>
|
||||
<td>${(entry.noBid + entry.noWeight).toFixed(0)}</td>
|
||||
<td>${entry.noWeight.toFixed(0)}</td>
|
||||
{!props.isNew && (
|
||||
<>
|
||||
<td>${entry.noPayout.toFixed(0)}</td>
|
||||
|
@ -149,9 +149,9 @@ function NewBidTable(props: {
|
|||
|
||||
function randomBid() {
|
||||
const bidType = Math.random() < 0.5 ? 'YES' : 'NO'
|
||||
const p = bidType === 'YES' ? nextEntry.prob : 1 - nextEntry.prob
|
||||
// const p = bidType === 'YES' ? nextEntry.prob : 1 - nextEntry.prob
|
||||
|
||||
const amount = Math.round(p * Math.random() * 300) + 1
|
||||
const amount = Math.floor(Math.random() * 300) + 1
|
||||
const bid = makeBid(bidType, amount)
|
||||
|
||||
bids.splice(steps, 0, bid)
|
||||
|
@ -238,7 +238,7 @@ function NewBidTable(props: {
|
|||
// Show a hello world React page
|
||||
export default function Simulator() {
|
||||
const [steps, setSteps] = useState(1)
|
||||
const [bids, setBids] = useState([{ yesBid: 550, noBid: 450 }])
|
||||
const [bids, setBids] = useState([{ yesBid: 100, noBid: 100 }])
|
||||
|
||||
const entries = useMemo(
|
||||
() => makeEntries(bids.slice(0, steps)),
|
||||
|
|
|
@ -3,6 +3,7 @@ import { SearchableGrid } from '../../components/contracts-list'
|
|||
import { Page } from '../../components/page'
|
||||
import { Title } from '../../components/title'
|
||||
import { useContracts } from '../../hooks/use-contracts'
|
||||
import { useQueryAndSortParams } from '../../hooks/use-sort-and-query-params'
|
||||
|
||||
export default function TagPage() {
|
||||
const router = useRouter()
|
||||
|
@ -18,10 +19,24 @@ export default function TagPage() {
|
|||
)
|
||||
}
|
||||
|
||||
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
||||
defaultSort: 'most-traded',
|
||||
})
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Title text={`#${tag}`} />
|
||||
<SearchableGrid contracts={contracts === 'loading' ? [] : contracts} />
|
||||
{contracts === 'loading' ? (
|
||||
<></>
|
||||
) : (
|
||||
<SearchableGrid
|
||||
contracts={contracts}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
sort={sort}
|
||||
setSort={setSort}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,13 +4,13 @@ import { SEO } from '../components/SEO'
|
|||
import { Title } from '../components/title'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
|
||||
export default function BetsPage() {
|
||||
export default function TradesPage() {
|
||||
const user = useUser()
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<SEO title="Your bets" description="Your bets" url="/bets" />
|
||||
<Title text="Your bets" />
|
||||
<SEO title="Your trades" description="Your trades" url="/trades" />
|
||||
<Title text="Your trades" />
|
||||
{user && <BetsList user={user} />}
|
||||
</Page>
|
||||
)
|
BIN
web/public/logo-banner.png
Normal file
BIN
web/public/logo-banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
web/public/logo-bg.png
Normal file
BIN
web/public/logo-bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
16
web/public/twitter-icon-white.svg
Normal file
16
web/public/twitter-icon-white.svg
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 248 204" style="enable-background:new 0 0 248 204;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Logo_1_">
|
||||
<path id="white_background" class="st0" d="M221.95,51.29c0.15,2.17,0.15,4.34,0.15,6.53c0,66.73-50.8,143.69-143.69,143.69v-0.04
|
||||
C50.97,201.51,24.1,193.65,1,178.83c3.99,0.48,8,0.72,12.02,0.73c22.74,0.02,44.83-7.61,62.72-21.66
|
||||
c-21.61-0.41-40.56-14.5-47.18-35.07c7.57,1.46,15.37,1.16,22.8-0.87C27.8,117.2,10.85,96.5,10.85,72.46c0-0.22,0-0.43,0-0.64
|
||||
c7.02,3.91,14.88,6.08,22.92,6.32C11.58,63.31,4.74,33.79,18.14,10.71c25.64,31.55,63.47,50.73,104.08,52.76
|
||||
c-4.07-17.54,1.49-35.92,14.61-48.25c20.34-19.12,52.33-18.14,71.45,2.19c11.31-2.23,22.15-6.38,32.07-12.26
|
||||
c-3.77,11.69-11.66,21.62-22.2,27.93c10.01-1.18,19.79-3.86,29-7.95C240.37,35.29,231.83,44.14,221.95,51.29z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -8,7 +8,7 @@ module.exports = {
|
|||
fontFamily: Object.assign(
|
||||
{ ...defaultTheme.fontFamily },
|
||||
{
|
||||
'major-mono': ['Courier', 'monospace'],
|
||||
'major-mono': ['Major Mono Display', 'monospace'],
|
||||
'readex-pro': ['Readex Pro', 'sans-serif'],
|
||||
}
|
||||
),
|
||||
|
|
|
@ -2289,6 +2289,11 @@ fast-levenshtein@^2.0.6:
|
|||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
fast-shallow-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b"
|
||||
integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
|
||||
|
@ -3837,6 +3842,15 @@ react-dom@17.0.2:
|
|||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-expanding-textarea@^2.3.4:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-expanding-textarea/-/react-expanding-textarea-2.3.4.tgz#3eee788cf3b36798d0f9aed5a50f752278d44a49"
|
||||
integrity sha512-zg/14CyPrIbjPgjfQGxcCmSv9nCrSNpmYpnqyOnNwaOJb8zxWD/GXN8Dgnp5jx8CPU6uIfZVhxs7h2hiOXiSHQ==
|
||||
dependencies:
|
||||
fast-shallow-equal "^1.0.0"
|
||||
react-with-forwarded-ref "^0.3.3"
|
||||
tslib "^2.0.3"
|
||||
|
||||
react-is@17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
|
@ -3866,6 +3880,13 @@ react-refresh@0.8.3:
|
|||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
|
||||
|
||||
react-with-forwarded-ref@^0.3.3:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.4.tgz#b1e884ea081ec3c5dd578f37889159797454c0a5"
|
||||
integrity sha512-SRq/uTdTh+02JDwYzEEhY2aNNWl/CP2EKP2nQtXzhJw06w6PgYnJt2ObrebvFJu6+wGzX3vDHU3H/ux9hxyZUQ==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
react@17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
|
@ -4508,7 +4529,7 @@ tslib@^1.8.1, tslib@^1.9.0:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.1.0:
|
||||
tslib@^2.0.3, tslib@^2.1.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
|
Loading…
Reference in New Issue
Block a user