Merge branch 'main' into cpmm

This commit is contained in:
mantikoros 2022-03-02 16:22:16 -05:00
commit a9ccca6458
63 changed files with 4408 additions and 506 deletions

14
common/access.ts Normal file
View File

@ -0,0 +1,14 @@
export function isWhitelisted(email?: string) {
return true
// e.g. return email.endsWith('@theoremone.co') || isAdmin(email)
}
export function isAdmin(email: string) {
const ADMINS = [
'akrolsmir@gmail.com', // Austin
'jahooma@gmail.com', // James
'taowell@gmail.com', // Stephen
'manticmarkets@gmail.com', // Manifold
]
return ADMINS.includes(email)
}

View File

@ -4,6 +4,7 @@ export type Bet = {
contractId: string
amount: number // bet size; negative if SELL bet
loanAmount?: number
outcome: string
shares: number // dynamic parimutuel pool weight; negative if SELL bet
@ -21,3 +22,5 @@ export type Bet = {
createdTime: number
}
export const MAX_LOAN_PER_CONTRACT = 20

View File

@ -1,4 +1,5 @@
import { Bet } from './bet'
import * as _ from 'lodash'
import { Bet, MAX_LOAN_PER_CONTRACT } from './bet'
import {
calculateShares,
getProbability,
@ -20,6 +21,7 @@ export const getNewBinaryCpmmBetInfo = (
outcome: 'YES' | 'NO',
amount: number,
contract: FullContract<CPMM, Binary>,
loanAmount: number,
newBetId: string
) => {
const { pool, k } = contract
@ -32,7 +34,7 @@ export const getNewBinaryCpmmBetInfo = (
? [y - shares + amount, amount]
: [amount, n - shares + amount]
const newBalance = user.balance - amount
const newBalance = user.balance - (amount - loanAmount)
const probBefore = getCpmmProbability(pool)
const newPool = { YES: newY, NO: newN }
@ -45,6 +47,7 @@ export const getNewBinaryCpmmBetInfo = (
amount,
shares,
outcome,
loanAmount,
probBefore,
probAfter,
createdTime: Date.now(),
@ -58,6 +61,7 @@ export const getNewBinaryDpmBetInfo = (
outcome: 'YES' | 'NO',
amount: number,
contract: FullContract<DPM, Binary>,
loanAmount: number,
newBetId: string
) => {
const { YES: yesPool, NO: noPool } = contract.pool
@ -91,6 +95,7 @@ export const getNewBinaryDpmBetInfo = (
userId: user.id,
contractId: contract.id,
amount,
loanAmount,
shares,
outcome,
probBefore,
@ -98,7 +103,7 @@ export const getNewBinaryDpmBetInfo = (
createdTime: Date.now(),
}
const newBalance = user.balance - amount
const newBalance = user.balance - (amount - loanAmount)
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
}
@ -108,6 +113,7 @@ export const getNewMultiBetInfo = (
outcome: string,
amount: number,
contract: FullContract<DPM, Multi | FreeResponse>,
loanAmount: number,
newBetId: string
) => {
const { pool, totalShares, totalBets } = contract
@ -131,6 +137,7 @@ export const getNewMultiBetInfo = (
userId: user.id,
contractId: contract.id,
amount,
loanAmount,
shares,
outcome,
probBefore,
@ -138,7 +145,17 @@ export const getNewMultiBetInfo = (
createdTime: Date.now(),
}
const newBalance = user.balance - amount
const newBalance = user.balance - (amount - loanAmount)
return { newBet, newPool, newTotalShares, newTotalBets, newBalance }
}
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
const prevLoanAmount = _.sumBy(openBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = Math.min(
newBetAmount,
MAX_LOAN_PER_CONTRACT - prevLoanAmount
)
return loanAmount
}

View File

@ -281,3 +281,12 @@ export const getPayoutsMultiOutcome = (
.map(({ userId, payout }) => ({ userId, payout }))
.concat([{ userId: contract.creatorId, payout: creatorPayout }]) // add creator fee
}
export const getLoanPayouts = (bets: Bet[]) => {
const betsWithLoans = bets.filter((bet) => bet.loanAmount)
const betsByUser = _.groupBy(betsWithLoans, (bet) => bet.userId)
const loansByUser = _.mapValues(betsByUser, (bets) =>
_.sumBy(bets, (bet) => -(bet.loanAmount ?? 0))
)
return _.toPairs(loansByUser).map(([userId, payout]) => ({ userId, payout }))
}

View File

@ -48,8 +48,9 @@ export function scoreUsersByContract(
const investments = bets
.filter((bet) => !bet.sale)
.map((bet) => {
const { userId, amount } = bet
return { userId, payout: -amount }
const { userId, amount, loanAmount } = bet
const payout = -amount - (loanAmount ?? 0)
return { userId, payout }
})
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]

View File

@ -16,7 +16,7 @@ export const getSellBetInfo = (
newBetId: string
) => {
const { pool, totalShares, totalBets } = contract
const { id: betId, amount, shares, outcome } = bet
const { id: betId, amount, shares, outcome, loanAmount } = bet
const adjShareValue = calculateShareValue(contract, bet)
@ -62,7 +62,7 @@ export const getSellBetInfo = (
},
}
const newBalance = user.balance + saleAmount
const newBalance = user.balance + saleAmount - (loanAmount ?? 0)
return {
newBet,

View File

@ -28,6 +28,8 @@ export type PrivateUser = {
email?: string
unsubscribedFromResolutionEmails?: boolean
unsubscribedFromCommentEmails?: boolean
unsubscribedFromAnswerEmails?: boolean
initialDeviceToken?: string
initialIpAddress?: string
}

View File

@ -5,6 +5,7 @@
"source": "functions"
},
"firestore": {
"rules": "firestore.rules"
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
}
}

296
firestore.indexes.json Normal file
View File

@ -0,0 +1,296 @@
{
"indexes": [
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "creatorId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "closeTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "closeTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "volume24Hours",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "volume24Hours",
"order": "ASCENDING"
},
{
"fieldPath": "closeTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "volume24Hours",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "volume24Hours",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "slug",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
}
],
"fieldOverrides": [
{
"collectionGroup": "answers",
"fieldPath": "isAnte",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "answers",
"fieldPath": "username",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "bets",
"fieldPath": "createdTime",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "bets",
"fieldPath": "userId",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "comments",
"fieldPath": "createdTime",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "comments",
"fieldPath": "userUsername",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "followers",
"fieldPath": "userId",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
}
]
}

View File

@ -1,3 +1,6 @@
# Secrets
.env*
# Compiled JavaScript files
lib/**/*.js
lib/**/*.js.map

View File

@ -3,9 +3,11 @@ import * as admin from 'firebase-admin'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { getNewMultiBetInfo } from '../../common/new-bet'
import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet'
import { Answer } from '../../common/answer'
import { getValues } from './utils'
import { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails'
import { Bet } from '../../common/bet'
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
async (
@ -28,7 +30,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
return { status: 'error', message: 'Invalid text' }
// Run as transaction to prevent race conditions.
return await firestore.runTransaction(async (transaction) => {
const result = await firestore.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${userId}`)
const userSnap = await transaction.get(userDoc)
if (!userSnap.exists)
@ -54,6 +56,11 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
const yourBetsSnap = await transaction.get(
contractDoc.collection('bets').where('userId', '==', userId)
)
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
const [lastAnswer] = await getValues<Answer>(
firestore
.collection(`contracts/${contractId}/answers`)
@ -91,8 +98,17 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
.collection(`contracts/${contractId}/bets`)
.doc()
const loanAmount = getLoanAmount(yourBets, amount)
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
getNewMultiBetInfo(user, answerId, amount, contract, newBetDoc.id)
getNewMultiBetInfo(
user,
answerId,
amount,
loanAmount,
contract,
newBetDoc.id
)
transaction.create(newBetDoc, newBet)
transaction.update(contractDoc, {
@ -103,8 +119,15 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
})
transaction.update(userDoc, { balance: newBalance })
return { status: 'success', answerId, betId: newBetDoc.id }
return { status: 'success', answerId, betId: newBetDoc.id, answer }
})
const { answer } = result
const contract = await getContract(contractId)
if (answer && contract) await sendNewAnswerEmail(answer, contract)
return result
}
)

View File

@ -14,6 +14,7 @@ import {
cleanUsername,
} from '../../common/util/clean-username'
import { sendWelcomeEmail } from './emails'
import { isWhitelisted } from '../../common/access'
export const createUser = functions
.runWith({ minInstances: 1 })
@ -32,6 +33,9 @@ export const createUser = functions
const fbUser = await admin.auth().getUser(userId)
const email = fbUser.email
if (!isWhitelisted(email)) {
return { status: 'error', message: `${email} is not whitelisted` }
}
const emailName = email?.replace(/@.*$/, '')
const rawName = fbUser.displayName || emailName || 'User' + randomString(4)

View File

@ -0,0 +1,561 @@
<!DOCTYPE html>
<html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market comment</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
display: block !important;
max-width: 600px !important;
clear: both !important;
margin: 0 auto;
"
valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
max-width: 600px;
display: block;
margin: 0 auto;
padding: 20px;
"
>
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
border-radius: 3px;
background-color: #fff;
margin: 0;
border: 1px solid #e9e9e9;
"
bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 20px;
"
align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
width: 90%;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 0px 0;
text-align: left;
"
valign="top"
>
<img
src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 0;
"
align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
text-align: left;
width: 80%;
margin: 40px auto;
margin-top: 10px;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 5px 0;
"
valign="top"
>
Answer {{answerNumber}}
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding-top: 5px;
padding-bottom: 20px;
"
valign="top"
>
<div
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<span style="white-space: pre-line"
>{{answer}}</span
>
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 5px 0;
"
valign="top"
>
<div>
<img
src="{{commentorAvatarUrl}}"
width="30"
height="30"
style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
"
alt=""
/>
<span style="font-weight: bold"
>{{commentorName}}</span
>
{{betDescription}}
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 5px 0;
"
valign="top"
>
<div
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<span style="white-space: pre-line"
>{{comment}}</span
>
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td style="padding: 20px 0 0 0; margin: 0">
<div align="center">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<a
href="{{marketUrl}}"
target="_blank"
style="
box-sizing: border-box;
display: inline-block;
font-family: arial, helvetica, sans-serif;
text-decoration: none;
-webkit-text-size-adjust: none;
text-align: center;
color: #ffffff;
background-color: #11b981;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
width: auto;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
mso-border-alt: none;
"
>
<span
style="
display: block;
padding: 10px 20px;
line-height: 120%;
"
><span
style="
font-size: 16px;
font-weight: bold;
line-height: 18.8px;
"
>View comment</span
></span
>
</a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
clear: both;
color: #999;
margin: 0;
padding: 20px;
"
>
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
vertical-align: top;
color: #999;
text-align: center;
margin: 0;
padding: 0 0 20px;
"
align="center"
valign="top"
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>our Discord</a
>! Or,
<a
href="{{unsubscribeUrl}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div>
</td>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,499 @@
<!DOCTYPE html>
<html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market answer</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
display: block !important;
max-width: 600px !important;
clear: both !important;
margin: 0 auto;
"
valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
max-width: 600px;
display: block;
margin: 0 auto;
padding: 20px;
"
>
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
border-radius: 3px;
background-color: #fff;
margin: 0;
border: 1px solid #e9e9e9;
"
bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 20px;
"
align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
width: 90%;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 0px 0;
text-align: left;
"
valign="top"
>
<img
src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 0;
"
align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
text-align: left;
width: 80%;
margin: 40px auto;
margin-top: 10px;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 5px 0;
font-weight: bold;
"
valign="top"
>
<div>
<img
src="{{avatarUrl}}"
width="30"
height="30"
style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
"
alt=""
/>
{{name}}
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 5px 0;
"
valign="top"
>
<div
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<span style="white-space: pre-line"
>{{answer}}</span
>
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td style="padding: 20px 0 0 0; margin: 0">
<div align="center">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<a
href="{{marketUrl}}"
target="_blank"
style="
box-sizing: border-box;
display: inline-block;
font-family: arial, helvetica, sans-serif;
text-decoration: none;
-webkit-text-size-adjust: none;
text-align: center;
color: #ffffff;
background-color: #11b981;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
width: auto;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
mso-border-alt: none;
"
>
<span
style="
display: block;
padding: 10px 20px;
line-height: 120%;
"
><span
style="
font-size: 16px;
font-weight: bold;
line-height: 18.8px;
"
>View answer</span
></span
>
</a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
clear: both;
color: #999;
margin: 0;
padding: 20px;
"
>
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
vertical-align: top;
color: #999;
text-align: center;
margin: 0;
padding: 0 0 20px;
"
align="center"
valign="top"
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>our Discord</a
>! Or,
<a
href="{{unsubscribeUrl}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div>
</td>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,647 @@
<!DOCTYPE html>
<html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market closed</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
display: block !important;
max-width: 600px !important;
clear: both !important;
margin: 0 auto;
"
valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
max-width: 600px;
display: block;
margin: 0 auto;
padding: 20px;
"
>
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
border-radius: 3px;
background-color: #fff;
margin: 0;
border: 1px solid #e9e9e9;
"
bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 20px;
"
align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
width: 90%;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 40px 0;
text-align: left;
"
valign="top"
>
<img
src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 6px 0;
text-align: left;
"
valign="top"
>
You asked
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
padding: 0 0 20px;
"
valign="top"
>
<a
href="{{url}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif;
box-sizing: border-box;
font-size: 24px;
color: #000;
line-height: 1.2em;
font-weight: 500;
text-align: left;
margin: 0 0 0 0;
color: #4337c9;
display: block;
text-decoration: none;
"
>
{{question}}</a
>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
padding: 0 0 0px;
"
valign="top"
>
<h2
class="aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif;
box-sizing: border-box;
font-size: 24px;
color: #000;
line-height: 1.2em;
font-weight: 500;
text-align: center;
margin: 10px 0 0;
"
align="center"
>
Market closed
</h2>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 0;
"
align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
text-align: left;
width: 80%;
margin: 40px auto;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 5px 0;
"
valign="top"
>
Hi {{name}},
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
A market you created has closed. It's attracted
<span style="font-weight: bold">{{pool}}</span> in
bets — congrats!
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
Resolve your market to earn {{creatorFee}}% of the
profits as the creator commission.
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
Thanks,
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
Manifold Team
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td style="padding: 10px 0 0 0; margin: 0">
<div align="center">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<a
href="{{url}}"
target="_blank"
style="
box-sizing: border-box;
display: inline-block;
font-family: arial, helvetica, sans-serif;
text-decoration: none;
-webkit-text-size-adjust: none;
text-align: center;
color: #ffffff;
background-color: #11b981;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
width: auto;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
mso-border-alt: none;
"
>
<span
style="
display: block;
padding: 10px 20px;
line-height: 120%;
"
><span
style="
font-size: 16px;
font-weight: bold;
line-height: 18.8px;
"
>View market</span
></span
>
</a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
clear: both;
color: #999;
margin: 0;
padding: 20px;
"
>
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
vertical-align: top;
color: #999;
text-align: center;
margin: 0;
padding: 0 0 20px;
"
align="center"
valign="top"
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>our Discord</a
>! Or,
<a
href="https://us-central1-mantic-markets.cloudfunctions.net/unsubscribe?id={{userId}}&type=market-resolve"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div>
</td>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,501 @@
<!DOCTYPE html>
<html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market comment</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
display: block !important;
max-width: 600px !important;
clear: both !important;
margin: 0 auto;
"
valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
max-width: 600px;
display: block;
margin: 0 auto;
padding: 20px;
"
>
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
border-radius: 3px;
background-color: #fff;
margin: 0;
border: 1px solid #e9e9e9;
"
bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 20px;
"
align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
width: 90%;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 0px 0;
text-align: left;
"
valign="top"
>
<img
src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 0;
"
align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
text-align: left;
width: 80%;
margin: 40px auto;
margin-top: 10px;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 5px 0;
"
valign="top"
>
<div>
<img
src="{{commentorAvatarUrl}}"
width="30"
height="30"
style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
"
alt=""
/>
<span style="font-weight: bold"
>{{commentorName}}</span
>
{{betDescription}}
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 5px 0;
"
valign="top"
>
<div
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<span style="white-space: pre-line"
>{{comment}}</span
>
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td style="padding: 20px 0 0 0; margin: 0">
<div align="center">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<a
href="{{marketUrl}}"
target="_blank"
style="
box-sizing: border-box;
display: inline-block;
font-family: arial, helvetica, sans-serif;
text-decoration: none;
-webkit-text-size-adjust: none;
text-align: center;
color: #ffffff;
background-color: #11b981;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
width: auto;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
mso-border-alt: none;
"
>
<span
style="
display: block;
padding: 10px 20px;
line-height: 120%;
"
><span
style="
font-size: 16px;
font-weight: bold;
line-height: 18.8px;
"
>View comment</span
></span
>
</a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
clear: both;
color: #999;
margin: 0;
padding: 20px;
"
>
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
vertical-align: top;
color: #999;
text-align: center;
margin: 0;
padding: 0 0 20px;
"
align="center"
valign="top"
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>our Discord</a
>! Or,
<a
href="{{unsubscribeUrl}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div>
</td>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,669 @@
<!DOCTYPE html>
<html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market resolved</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
display: block !important;
max-width: 600px !important;
clear: both !important;
margin: 0 auto;
"
valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
max-width: 600px;
display: block;
margin: 0 auto;
padding: 20px;
"
>
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
border-radius: 3px;
background-color: #fff;
margin: 0;
border: 1px solid #e9e9e9;
"
bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 20px;
"
align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
width: 90%;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 40px 0;
text-align: left;
"
valign="top"
>
<img
src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 6px 0;
text-align: left;
"
valign="top"
>
{{creatorName}} asked
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
padding: 0 0 20px;
"
valign="top"
>
<a
href="{{url}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif;
box-sizing: border-box;
font-size: 24px;
color: #000;
line-height: 1.2em;
font-weight: 500;
text-align: left;
margin: 0 0 0 0;
color: #4337c9;
display: block;
text-decoration: none;
"
>
{{question}}</a
>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
padding: 0 0 0px;
"
valign="top"
>
<h2
class="aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif;
box-sizing: border-box;
font-size: 24px;
color: #000;
line-height: 1.2em;
font-weight: 500;
text-align: center;
margin: 10px 0 0;
"
align="center"
>
Resolved {{outcome}}
</h2>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 0;
"
align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
text-align: left;
width: 80%;
margin: 40px auto;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 5px 0;
"
valign="top"
>
Dear {{name}},
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
A market you bet in has been resolved!
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
Your investment was
<span style="font-weight: bold"
>M$ {{investment}}</span
>.
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
Your payout is
<span style="font-weight: bold"
>M$ {{payout}}</span
>.
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
Thanks,
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
Manifold Team
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
<br
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td style="padding: 10px 0 0 0; margin: 0">
<div align="center">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<a
href="{{url}}"
target="_blank"
style="
box-sizing: border-box;
display: inline-block;
font-family: arial, helvetica, sans-serif;
text-decoration: none;
-webkit-text-size-adjust: none;
text-align: center;
color: #ffffff;
background-color: #11b981;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
width: auto;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
mso-border-alt: none;
"
>
<span
style="
display: block;
padding: 10px 20px;
line-height: 120%;
"
><span
style="
font-size: 16px;
font-weight: bold;
line-height: 18.8px;
"
>View market</span
></span
>
</a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
clear: both;
color: #999;
margin: 0;
padding: 20px;
"
>
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
vertical-align: top;
color: #999;
text-align: center;
margin: 0;
padding: 0 0 20px;
"
align="center"
valign="top"
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>our Discord</a
>! Or,
<a
href="https://us-central1-mantic-markets.cloudfunctions.net/unsubscribe?id={{userId}}&type=market-resolved"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div>
</td>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
</tr>
</table>
</body>
</html>

View File

@ -1,11 +1,14 @@
import _ = require('lodash')
import { Answer } from '../../common/answer'
import { Bet } from '../../common/bet'
import { getProbability } from '../../common/calculate'
import { Comment } from '../../common/comment'
import { Contract } from '../../common/contract'
import { CREATOR_FEE } from '../../common/fees'
import { PrivateUser, User } from '../../common/user'
import { formatMoney, formatPercent } from '../../common/util/format'
import { sendTemplateEmail, sendTextEmail } from './send-email'
import { getPrivateUser, getUser } from './utils'
import { getPrivateUser, getUser, isProd } from './utils'
type market_resolved_template = {
userId: string
@ -13,13 +16,14 @@ type market_resolved_template = {
creatorName: string
question: string
outcome: string
investment: string
payout: string
url: string
}
const toDisplayResolution = (
outcome: string,
prob: number,
prob?: number,
resolutions?: { [outcome: string]: number }
) => {
if (outcome === 'MKT' && resolutions) return 'MULTI'
@ -28,7 +32,7 @@ const toDisplayResolution = (
YES: 'YES',
NO: 'NO',
CANCEL: 'N/A',
MKT: formatPercent(prob),
MKT: formatPercent(prob ?? 0),
}[outcome]
return display === undefined ? `#${outcome}` : display
@ -36,6 +40,7 @@ const toDisplayResolution = (
export const sendMarketResolutionEmail = async (
userId: string,
investment: number,
payout: number,
creator: User,
contract: Contract,
@ -66,6 +71,7 @@ export const sendMarketResolutionEmail = async (
creatorName: creator.name,
question: contract.question,
outcome,
investment: `${Math.round(investment)}`,
payout: `${Math.round(payout)}`,
url: `https://manifold.markets/${creator.username}/${contract.slug}`,
}
@ -138,3 +144,119 @@ export const sendMarketCloseEmail = async (
}
)
}
export const sendNewCommentEmail = async (
userId: string,
commentCreator: User,
contract: Contract,
comment: Comment,
bet: Bet,
answer?: Answer
) => {
const privateUser = await getPrivateUser(userId)
if (
!privateUser ||
!privateUser.email ||
privateUser.unsubscribedFromCommentEmails
)
return
const { question, creatorUsername, slug } = contract
const marketUrl = `https://manifold.markets/${creatorUsername}/${slug}`
const unsubscribeUrl = `https://us-central1-${
isProd ? 'mantic-markets' : 'dev-mantic-markets'
}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-comment`
const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
const { text } = comment
const { amount, sale, outcome } = bet
let betDescription = `${sale ? 'sold' : 'bought'} M$ ${Math.round(amount)}`
const subject = `Comment on ${question}`
const from = `${commentorName} <info@manifold.markets>`
if (contract.outcomeType === 'FREE_RESPONSE') {
const answerText = answer?.text ?? ''
const answerNumber = `#${answer?.id ?? ''}`
await sendTemplateEmail(
privateUser.email,
subject,
'market-answer-comment',
{
answer: answerText,
answerNumber,
commentorName,
commentorAvatarUrl: commentorAvatarUrl ?? '',
comment: text,
marketUrl,
unsubscribeUrl,
betDescription,
},
{ from }
)
} else {
betDescription = `${betDescription} of ${toDisplayResolution(outcome)}`
await sendTemplateEmail(
privateUser.email,
subject,
'market-comment',
{
commentorName,
commentorAvatarUrl: commentorAvatarUrl ?? '',
comment: text,
marketUrl,
unsubscribeUrl,
betDescription,
},
{ from }
)
}
}
export const sendNewAnswerEmail = async (
answer: Answer,
contract: Contract
) => {
// Send to just the creator for now.
const { creatorId: userId } = contract
// Don't send the creator's own answers.
if (answer.userId === userId) return
const privateUser = await getPrivateUser(userId)
if (
!privateUser ||
!privateUser.email ||
privateUser.unsubscribedFromAnswerEmails
)
return
const { question, creatorUsername, slug } = contract
const { name, avatarUrl, text } = answer
const marketUrl = `https://manifold.markets/${creatorUsername}/${slug}`
const unsubscribeUrl = `https://us-central1-${
isProd ? 'mantic-markets' : 'dev-mantic-markets'
}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-answer`
const subject = `New answer on ${question}`
const from = `${name} <info@manifold.markets>`
await sendTemplateEmail(
privateUser.email,
subject,
'market-answer',
{
name,
avatarUrl: avatarUrl ?? '',
answer: text,
marketUrl,
unsubscribeUrl,
},
{ from }
)
}

View File

@ -12,6 +12,7 @@ export * from './create-contract'
export * from './create-user'
export * from './create-fold'
export * from './create-answer'
export * from './on-create-comment'
export * from './on-fold-follow'
export * from './on-fold-delete'
export * from './unsubscribe'

View File

@ -0,0 +1,60 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { getContract, getUser, getValues } from './utils'
import { Comment } from '../../common/comment'
import { sendNewCommentEmail } from './emails'
import { Bet } from '../../common/bet'
const firestore = admin.firestore()
export const onCreateComment = functions.firestore
.document('contracts/{contractId}/comments/{commentId}')
.onCreate(async (change, context) => {
const { contractId } = context.params as {
contractId: string
}
const contract = await getContract(contractId)
if (!contract) return
const comment = change.data() as Comment
const commentCreator = await getUser(comment.userId)
if (!commentCreator) return
const betSnapshot = await firestore
.collection('contracts')
.doc(contractId)
.collection('bets')
.doc(comment.betId)
.get()
const bet = betSnapshot.data() as Bet
const answer =
contract.answers &&
contract.answers.find((answer) => answer.id === bet.outcome)
const comments = await getValues<Comment>(
firestore.collection('contracts').doc(contractId).collection('comments')
)
const recipientUserIds = _.uniq([
contract.creatorId,
...comments.map((comment) => comment.userId),
]).filter((id) => id !== comment.userId)
await Promise.all(
recipientUserIds.map((userId) =>
sendNewCommentEmail(
userId,
commentCreator,
contract,
comment,
bet,
answer
)
)
)
})

View File

@ -7,8 +7,11 @@ import {
getNewBinaryCpmmBetInfo,
getNewBinaryDpmBetInfo,
getNewMultiBetInfo,
getLoanAmount,
} from '../../common/new-bet'
import { removeUndefinedProps } from '../../common/util/object'
import { Bet } from '../../common/bet'
import { getValues } from './utils'
export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
async (
@ -51,6 +54,11 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
const yourBetsSnap = await transaction.get(
contractDoc.collection('bets').where('userId', '==', userId)
)
const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
if (outcomeType === 'FREE_RESPONSE') {
const answerSnap = await transaction.get(
contractDoc.collection('answers').doc(outcome)
@ -63,6 +71,8 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
.collection(`contracts/${contractId}/bets`)
.doc()
const loanAmount = getLoanAmount(yourBets, amount)
const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
outcomeType === 'BINARY'
? mechanism === 'dpm-2'
@ -71,6 +81,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
outcome as 'YES' | 'NO',
amount,
contract,
loanAmount,
newBetDoc.id
)
: (getNewBinaryCpmmBetInfo(
@ -78,6 +89,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
outcome as 'YES' | 'NO',
amount,
contract,
loanAmount,
newBetDoc.id
) as any)
: getNewMultiBetInfo(
@ -85,6 +97,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
outcome,
amount,
contract as any,
loanAmount,
newBetDoc.id
)

View File

@ -7,7 +7,12 @@ import { User } from '../../common/user'
import { Bet } from '../../common/bet'
import { getUser, payUser } from './utils'
import { sendMarketResolutionEmail } from './emails'
import { getPayouts, getPayoutsMultiOutcome } from '../../common/payouts'
import {
getLoanPayouts,
getPayouts,
getPayoutsMultiOutcome,
} from '../../common/payouts'
import { removeUndefinedProps } from '../../common/util/object'
export const resolveMarket = functions
.runWith({ minInstances: 1 })
@ -31,7 +36,7 @@ export const resolveMarket = functions
if (!contractSnap.exists)
return { status: 'error', message: 'Invalid contract' }
const contract = contractSnap.data() as Contract
const { creatorId, outcomeType } = contract
const { creatorId, outcomeType, closeTime } = contract
if (outcomeType === 'BINARY') {
if (!['YES', 'NO', 'MKT', 'CANCEL'].includes(outcome))
@ -68,15 +73,21 @@ export const resolveMarket = functions
const resolutionProbability =
probabilityInt !== undefined ? probabilityInt / 100 : undefined
await contractDoc.update({
isResolved: true,
resolution: outcome,
resolutionTime: Date.now(),
...(resolutionProbability === undefined
? {}
: { resolutionProbability }),
...(resolutions === undefined ? {} : { resolutions }),
})
const resolutionTime = Date.now()
const newCloseTime = closeTime
? Math.min(closeTime, resolutionTime)
: closeTime
await contractDoc.update(
removeUndefinedProps({
isResolved: true,
resolution: outcome,
resolutionTime,
closeTime: newCloseTime,
resolutionProbability,
resolutions,
})
)
console.log('contract ', contractId, 'resolved to:', outcome)
@ -92,13 +103,23 @@ export const resolveMarket = functions
? getPayoutsMultiOutcome(resolutions, contract as any, openBets)
: getPayouts(outcome, contract, openBets, resolutionProbability)
const loanPayouts = getLoanPayouts(openBets)
console.log('payouts:', payouts)
const groups = _.groupBy(payouts, (payout) => payout.userId)
const groups = _.groupBy(
[...payouts, ...loanPayouts],
(payout) => payout.userId
)
const userPayouts = _.mapValues(groups, (group) =>
_.sumBy(group, (g) => g.payout)
)
const groupsWithoutLoans = _.groupBy(payouts, (payout) => payout.userId)
const userPayoutsWithoutLoans = _.mapValues(groupsWithoutLoans, (group) =>
_.sumBy(group, (g) => g.payout)
)
const payoutPromises = Object.entries(userPayouts).map(
([userId, payout]) => payUser(userId, payout)
)
@ -109,7 +130,7 @@ export const resolveMarket = functions
await sendResolutionEmails(
openBets,
userPayouts,
userPayoutsWithoutLoans,
creator,
contract,
outcome,
@ -134,14 +155,24 @@ const sendResolutionEmails = async (
_.uniq(openBets.map(({ userId }) => userId)),
Object.keys(userPayouts)
)
const investedByUser = _.mapValues(
_.groupBy(openBets, (bet) => bet.userId),
(bets) => _.sumBy(bets, (bet) => bet.amount)
)
const emailPayouts = [
...Object.entries(userPayouts),
...nonWinners.map((userId) => [userId, 0] as const),
]
].map(([userId, payout]) => ({
userId,
investment: investedByUser[userId],
payout,
}))
await Promise.all(
emailPayouts.map(([userId, payout]) =>
emailPayouts.map(({ userId, investment, payout }) =>
sendMarketResolutionEmail(
userId,
investment,
payout,
creator,
contract,

View File

@ -0,0 +1,44 @@
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { initAdmin } from './script-init'
initAdmin('james')
import { Bet } from '../../../common/bet'
import { Contract } from '../../../common/contract'
import { getValues } from '../utils'
async function removeAnswerAnte() {
const firestore = admin.firestore()
console.log('Removing isAnte from bets on answers')
const contracts = await getValues<Contract>(
firestore
.collection('contracts')
.where('outcomeType', '==', 'FREE_RESPONSE')
)
console.log('Loaded', contracts, 'contracts')
for (const contract of contracts) {
const betsSnapshot = await firestore
.collection('contracts')
.doc(contract.id)
.collection('bets')
.get()
console.log('updating', contract.question)
for (const doc of betsSnapshot.docs) {
const bet = doc.data() as Bet
if (bet.isAnte && bet.outcome !== '0') {
console.log('updating', bet.outcome)
await doc.ref.update('isAnte', false)
}
}
}
}
if (require.main === module) {
removeAnswerAnte().then(() => process.exit())
}

View File

@ -24,10 +24,11 @@ export const sendTemplateEmail = (
to: string,
subject: string,
templateId: string,
templateData: Record<string, string>
templateData: Record<string, string>,
options?: { from: string }
) => {
const data = {
from: 'Manifold Markets <info@manifold.markets>',
from: options?.from ?? 'Manifold Markets <info@manifold.markets>',
to,
subject,
template: templateId,

View File

@ -2,7 +2,7 @@ import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import Stripe from 'stripe'
import { payUser } from './utils'
import { isProd, payUser } from './utils'
export type StripeTransaction = {
userId: string
@ -18,20 +18,19 @@ const stripe = new Stripe(functions.config().stripe.apikey, {
})
// manage at https://dashboard.stripe.com/test/products?active=true
const manticDollarStripePrice =
admin.instanceId().app.options.projectId === 'mantic-markets'
? {
500: 'price_1KFQXcGdoFKoCJW770gTNBrm',
1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65',
2500: 'price_1KFQqNGdoFKoCJW7SDvrSaEB',
10000: 'price_1KFQraGdoFKoCJW77I4XCwM3',
}
: {
500: 'price_1K8W10GdoFKoCJW7KWORLec1',
1000: 'price_1K8bC1GdoFKoCJW76k3g5MJk',
2500: 'price_1K8bDSGdoFKoCJW7avAwpV0e',
10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE',
}
const manticDollarStripePrice = isProd
? {
500: 'price_1KFQXcGdoFKoCJW770gTNBrm',
1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65',
2500: 'price_1KFQqNGdoFKoCJW7SDvrSaEB',
10000: 'price_1KFQraGdoFKoCJW77I4XCwM3',
}
: {
500: 'price_1K8W10GdoFKoCJW7KWORLec1',
1000: 'price_1K8bC1GdoFKoCJW76k3g5MJk',
2500: 'price_1K8bDSGdoFKoCJW7avAwpV0e',
10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE',
}
export const createCheckoutSession = functions
.runWith({ minInstances: 1 })

View File

@ -1,30 +1,47 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { getPrivateUser } from './utils'
import { getUser } from './utils'
import { PrivateUser } from '../../common/user'
export const unsubscribe = functions
.runWith({ minInstances: 1 })
.https.onRequest(async (req, res) => {
let id = req.query.id as string
if (!id) return
const { id, type } = req.query as { id: string; type: string }
if (!id || !type) return
let privateUser = await getPrivateUser(id)
const user = await getUser(id)
if (privateUser) {
let { username } = privateUser
if (user) {
const { name } = user
const update: Partial<PrivateUser> = {
unsubscribedFromResolutionEmails: true,
...(type === 'market-resolve' && {
unsubscribedFromResolutionEmails: true,
}),
...(type === 'market-comment' && {
unsubscribedFromCommentEmails: true,
}),
...(type === 'market-answer' && {
unsubscribedFromAnswerEmails: true,
}),
}
await firestore.collection('private-users').doc(id).update(update)
res.send(
username +
', you have been unsubscribed from market resolution emails on Manifold Markets.'
)
if (type === 'market-resolve')
res.send(
`${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
)
else if (type === 'market-comment')
res.send(
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
)
else if (type === 'market-answer')
res.send(
`${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
)
else res.send(`${name}, you have been unsubscribed.`)
} else {
res.send('This user is not currently subscribed or does not exist.')
}

View File

@ -52,7 +52,8 @@ const computeInvestmentValue = async (
if (!contract || contract.isResolved) return 0
if (bet.sale || bet.isSold) return 0
return calculatePayout(contract, bet, 'MKT')
const payout = calculatePayout(contract, bet, 'MKT')
return payout - (bet.loanAmount ?? 0)
})
}

View File

@ -3,6 +3,9 @@ import * as admin from 'firebase-admin'
import { Contract } from '../../common/contract'
import { PrivateUser, User } from '../../common/user'
export const isProd =
admin.instanceId().app.options.projectId === 'mantic-markets'
export const getValue = async <T>(collection: string, doc: string) => {
const snap = await admin.firestore().collection(collection).doc(doc).get()

View File

@ -1,15 +1,20 @@
import clsx from 'clsx'
import _ from 'lodash'
import { useUser } from '../hooks/use-user'
import { formatMoney } from '../../common/util/format'
import { AddFundsButton } from './add-funds-button'
import { Col } from './layout/col'
import { Row } from './layout/row'
import { useUserContractBets } from '../hooks/use-user-bets'
import { MAX_LOAN_PER_CONTRACT } from '../../common/bet'
import { InfoTooltip } from './info-tooltip'
import { Spacer } from './layout/spacer'
export function AmountInput(props: {
amount: number | undefined
onChange: (newAmount: number | undefined) => void
error: string | undefined
setError: (error: string | undefined) => void
contractId: string | undefined
minimumAmount?: number
disabled?: boolean
className?: string
@ -22,6 +27,7 @@ export function AmountInput(props: {
onChange,
error,
setError,
contractId,
disabled,
className,
inputClassName,
@ -31,10 +37,24 @@ export function AmountInput(props: {
const user = useUser()
const userBets = useUserContractBets(user?.id, contractId) ?? []
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = Math.min(
amount ?? 0,
MAX_LOAN_PER_CONTRACT - prevLoanAmount
)
const onAmountChange = (str: string) => {
if (str.includes('-')) {
onChange(undefined)
return
}
const amount = parseInt(str.replace(/[^\d]/, ''))
if (str && isNaN(amount)) return
if (amount >= 10 ** 9) return
onChange(str ? amount : undefined)
@ -47,7 +67,8 @@ export function AmountInput(props: {
}
}
const remainingBalance = Math.max(0, (user?.balance ?? 0) - (amount ?? 0))
const amountNetLoan = (amount ?? 0) - loanAmount
const remainingBalance = Math.max(0, (user?.balance ?? 0) - amountNetLoan)
return (
<Col className={className}>
@ -68,19 +89,34 @@ export function AmountInput(props: {
onChange={(e) => onAmountChange(e.target.value)}
/>
</label>
<Spacer h={4} />
{error && (
<div className="mr-auto mt-4 self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
{error}
</div>
)}
{user && (
<Col className="mt-3 text-sm">
<div className="mb-2 whitespace-nowrap text-gray-500">
Remaining balance
</div>
<Row className="gap-4">
<div>{formatMoney(Math.floor(remainingBalance))}</div>
{user.balance !== 1000 && <AddFundsButton />}
<Col className="gap-3 text-sm">
{contractId && (
<Row className="items-center justify-between gap-2 text-gray-500">
<Row className="items-center gap-2">
Amount loaned{' '}
<InfoTooltip
text={`In every market, you get an interest-free loan on the first ${formatMoney(
MAX_LOAN_PER_CONTRACT
)}.`}
/>
</Row>
<span className="text-neutral">{formatMoney(loanAmount)}</span>{' '}
</Row>
)}
<Row className="items-center justify-between gap-2 text-gray-500">
Remaining balance{' '}
<span className="text-neutral">
{formatMoney(Math.floor(remainingBalance))}
</span>
</Row>
</Col>
)}

View File

@ -30,8 +30,9 @@ export function AnswerBetPanel(props: {
answer: Answer
contract: Contract
closePanel: () => void
className?: string
}) {
const { answer, contract, closePanel } = props
const { answer, contract, closePanel, className } = props
const { id: answerId } = answer
const user = useUser()
@ -97,7 +98,7 @@ export function AnswerBetPanel(props: {
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
return (
<Col className="items-start px-2 pb-2 pt-4 sm:pt-0">
<Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}>
<Row className="self-stretch items-center justify-between">
<div className="text-xl">Buy this answer</div>
@ -114,40 +115,44 @@ export function AnswerBetPanel(props: {
setError={setError}
disabled={isSubmitting}
inputRef={inputRef}
contractId={contract.id}
/>
<Col className="gap-3 mt-3 w-full">
<Row className="justify-between items-center text-sm">
<div className="text-gray-500">Probability</div>
<Row>
<div>{formatPercent(initialProb)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
</Row>
<Spacer h={4} />
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
<Row>
<div>{formatPercent(initialProb)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
<Spacer h={4} />
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
Payout if chosen
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(
shares + contract.totalShares[answerId]
)} shares`}
/>
</Row>
<div>
{formatMoney(currentPayout)}
&nbsp; <span>(+{currentReturnPercent})</span>
</div>
<Row className="justify-between items-start text-sm gap-2">
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
<div>Payout if chosen</div>
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(
shares + contract.totalShares[answerId]
)} shares`}
/>
</Row>
<Row className="flex-wrap justify-end items-end gap-2">
<span className="whitespace-nowrap">
{formatMoney(currentPayout)}
</span>
<span>(+{currentReturnPercent})</span>
</Row>
</Row>
</Col>
<Spacer h={6} />
{user ? (
<button
className={clsx(
'btn',
'btn self-stretch',
betDisabled ? 'btn-disabled' : 'btn-primary',
isSubmitting ? 'loading' : ''
)}
@ -157,7 +162,7 @@ export function AnswerBetPanel(props: {
</button>
) : (
<button
className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
className="btn self-stretch whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign in to trade!

View File

@ -8,13 +8,13 @@ import { Col } from '../layout/col'
import { Row } from '../layout/row'
import { Avatar } from '../avatar'
import { SiteLink } from '../site-link'
import { DateTimeTooltip } from '../datetime-tooltip'
import dayjs from 'dayjs'
import { BuyButton } from '../yes-no-selector'
import { formatPercent } from '../../../common/util/format'
import { getOutcomeProbability } from '../../../common/calculate'
import { tradingAllowed } from '../../lib/firebase/contracts'
import { AnswerBetPanel } from './answer-bet-panel'
import { ContractFeed } from '../contract-feed'
import { Linkify } from '../linkify'
export function AnswerItem(props: {
answer: Answer
@ -35,10 +35,9 @@ export function AnswerItem(props: {
onDeselect,
} = props
const { resolution, resolutions, totalShares } = contract
const { username, avatarUrl, name, createdTime, number, text } = answer
const { username, avatarUrl, name, number, text } = answer
const isChosen = chosenProb !== undefined
const createdDate = dayjs(createdTime).format('MMM D')
const prob = getOutcomeProbability(totalShares, answer.id)
const roundedProb = Math.round(prob * 100)
const probPercent = formatPercent(prob)
@ -47,42 +46,50 @@ export function AnswerItem(props: {
const [isBetting, setIsBetting] = useState(false)
const canBet = !isBetting && !showChoice && tradingAllowed(contract)
return (
<Col
<div
className={clsx(
'p-4 sm:flex-row rounded gap-4',
'flex flex-col gap-4 rounded p-4 sm:flex-row',
wasResolvedTo
? resolution === 'MKT'
? 'bg-blue-50 mb-2'
: 'bg-green-50 mb-8'
? 'mb-2 bg-blue-50'
: 'mb-8 bg-green-50'
: chosenProb === undefined
? 'bg-gray-50'
: showChoice === 'radio'
? 'bg-green-50'
: 'bg-blue-50'
: 'bg-blue-50',
canBet && 'cursor-pointer hover:bg-gray-100'
)}
onClick={() => canBet && setIsBetting(true)}
>
<Col className="gap-3 flex-1">
<div className="whitespace-pre-line break-words">{text}</div>
<Col className="flex-1 gap-3">
<div className="whitespace-pre-line">
<Linkify text={text} />
</div>
<Row className="text-gray-500 text-sm gap-2 items-center">
<Row className="items-center gap-2 text-sm text-gray-500">
<SiteLink className="relative" href={`/${username}`}>
<Row className="items-center gap-2">
<Avatar avatarUrl={avatarUrl} size={6} />
<div className="truncate">{name}</div>
</Row>
</SiteLink>
<div className=""></div>
<div className="whitespace-nowrap">
<DateTimeTooltip text="" time={contract.createdTime}>
{createdDate}
</DateTimeTooltip>
</div>
<div className=""></div>
{/* TODO: Show total pool? */}
<div className="text-base">#{number}</div>
</Row>
{isBetting && (
<ContractFeed
contract={contract}
bets={[]}
comments={[]}
feedType="multi"
outcome={answer.id}
/>
)}
</Col>
{isBetting ? (
@ -90,13 +97,14 @@ export function AnswerItem(props: {
answer={answer}
contract={contract}
closePanel={() => setIsBetting(false)}
className="sm:w-72"
/>
) : (
<Row className="self-end sm:self-start items-center gap-4 justify-end">
<Row className="items-center justify-end gap-4 self-end sm:self-start">
{!wasResolvedTo &&
(showChoice === 'checkbox' ? (
<input
className="input input-bordered text-2xl justify-self-end w-24"
className="input input-bordered w-24 justify-self-end text-2xl"
type="number"
placeholder={`${roundedProb}`}
maxLength={9}
@ -121,7 +129,7 @@ export function AnswerItem(props: {
))}
{showChoice ? (
<div className="form-control py-1">
<label className="cursor-pointer label gap-3">
<label className="label cursor-pointer gap-3">
<span className="">Choose this answer</span>
{showChoice === 'radio' && (
<input
@ -162,7 +170,7 @@ export function AnswerItem(props: {
<>
{tradingAllowed(contract) && (
<BuyButton
className="justify-end self-end flex-initial btn-md !px-8"
className="btn-md flex-initial justify-end self-end !px-8"
onClick={() => {
setIsBetting(true)
}}
@ -188,6 +196,6 @@ export function AnswerItem(props: {
)}
</Row>
)}
</Col>
</div>
)
}

View File

@ -86,7 +86,7 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
<div />
<Col
className={clsx(
'sm:flex-row gap-4',
'sm:flex-row sm:items-end gap-4',
text ? 'justify-between' : 'self-end'
)}
>
@ -101,34 +101,42 @@ export function CreateAnswerPanel(props: { contract: Contract }) {
setError={setAmountError}
minimumAmount={1}
disabled={isSubmitting}
contractId={contract.id}
/>
</Col>
<Col className="gap-2 mt-1">
<div className="text-sm text-gray-500">Implied probability</div>
<Row>
<div>{formatPercent(0)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
<Col className="gap-3">
<Row className="justify-between items-center text-sm">
<div className="text-gray-500">Probability</div>
<Row>
<div>{formatPercent(0)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
</Row>
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
Payout if chosen
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(shares)} shares`}
/>
<Row className="justify-between text-sm gap-2">
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
<div>Payout if chosen</div>
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(shares)} shares`}
/>
</Row>
<Row className="flex-wrap justify-end items-end gap-2">
<span className="whitespace-nowrap">
{formatMoney(currentPayout)}
</span>
<span>(+{currentReturnPercent})</span>
</Row>
</Row>
<div>
{formatMoney(currentPayout)}
&nbsp; <span>(+{currentReturnPercent})</span>
</div>
</Col>
</>
)}
{user ? (
<button
className={clsx(
'btn self-end mt-2',
'btn mt-2',
canSubmit ? 'btn-outline' : 'btn-disabled',
isSubmitting && 'loading'
)}

View File

@ -144,7 +144,6 @@ export function BetPanel(props: {
text={panelTitle}
/>
{/* <div className="mt-2 mb-1 text-sm text-gray-500">Outcome</div> */}
<YesNoSelector
className="mb-4"
selected={betChoice}
@ -160,45 +159,49 @@ export function BetPanel(props: {
setError={setError}
disabled={isSubmitting}
inputRef={inputRef}
contractId={contract.id}
/>
<Spacer h={4} />
<Col className="gap-3 mt-3 w-full">
<Row className="justify-between items-center text-sm">
<div className="text-gray-500">Probability</div>
<Row>
<div>{formatPercent(initialProb)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
</Row>
<div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
<Row>
<div>{formatPercent(initialProb)}</div>
<div className="mx-2"></div>
<div>{formatPercent(resultProb)}</div>
</Row>
{betChoice && (
<>
<Spacer h={4} />
<Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
Payout if <OutcomeLabel outcome={betChoice} />
<Row className="justify-between items-start text-sm gap-2">
<Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
<div>
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
</div>
<InfoTooltip
text={`Current payout for ${formatWithCommas(
shares
)} / ${formatWithCommas(
shares +
totalShares[betChoice] -
(phantomShares ? phantomShares[betChoice] : 0)
totalShares[betChoice ?? 'YES'] -
(phantomShares ? phantomShares[betChoice ?? 'YES'] : 0)
)} ${betChoice} shares`}
/>
</Row>
<div>
{formatMoney(currentPayout)}
&nbsp; <span>(+{currentReturnPercent})</span>
</div>
</>
)}
<Row className="flex-wrap justify-end items-end gap-2">
<span className="whitespace-nowrap">
{formatMoney(currentPayout)}
</span>
<span>(+{currentReturnPercent})</span>
</Row>
</Row>
</Col>
<Spacer h={6} />
<Spacer h={8} />
{user && (
<button
className={clsx(
'btn',
'btn flex-1',
betDisabled
? 'btn-disabled'
: betChoice === 'YES'
@ -213,7 +216,7 @@ export function BetPanel(props: {
)}
{user === null && (
<button
className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign in to trade!

View File

@ -37,7 +37,7 @@ import { filterDefined } from '../../common/util/array'
import { LoadingIndicator } from './loading-indicator'
import { SiteLink } from './site-link'
type BetSort = 'newest' | 'profit' | 'resolved' | 'value'
type BetSort = 'newest' | 'profit' | 'settled' | 'value'
export function BetsList(props: { user: User }) {
const { user } = props
@ -82,7 +82,8 @@ export function BetsList(props: { user: User }) {
if (bet.isSold || bet.sale) return 0
const contract = contracts.find((c) => c.id === contractId)
return contract ? calculatePayout(contract, bet, 'MKT') : 0
const payout = contract ? calculatePayout(contract, bet, 'MKT') : 0
return payout - (bet.loanAmount ?? 0)
})
}
)
@ -106,23 +107,20 @@ export function BetsList(props: { user: User }) {
contracts,
(c) => -1 * Math.max(...contractBets[c.id].map((bet) => bet.createdTime))
)
else if (sort === 'resolved')
else if (sort === 'settled')
sortedContracts = _.sortBy(contracts, (c) => -1 * (c.resolutionTime ?? 0))
const [resolved, unresolved] = _.partition(
const [settled, unsettled] = _.partition(
sortedContracts,
(c) => c.isResolved
(c) => c.isResolved || contractsInvestment[c.id] === 0
)
const displayedContracts = sort === 'resolved' ? resolved : unresolved
const displayedContracts = sort === 'settled' ? settled : unsettled
const currentInvestment = _.sumBy(
unresolved,
(c) => contractsInvestment[c.id]
)
const currentInvestment = _.sumBy(unsettled, (c) => contractsInvestment[c.id])
const currentBetsValue = _.sumBy(
unresolved,
unsettled,
(c) => contractsCurrentValue[c.id]
)
@ -140,7 +138,7 @@ export function BetsList(props: { user: User }) {
<Col>
<div className="text-sm text-gray-500">Invested</div>
<div className="text-lg">
{formatMoney(currentBetsValue)}{' '}
{formatMoney(currentInvestment)}{' '}
<ProfitBadge profitPercent={investedProfit} />
</div>
</Col>
@ -160,8 +158,8 @@ export function BetsList(props: { user: User }) {
>
<option value="value">By value</option>
<option value="profit">By profit</option>
<option value="newest">Newest</option>
<option value="resolved">Resolved</option>
<option value="newest">Most recent</option>
<option value="settled">Resolved</option>
</select>
</Col>
@ -228,7 +226,7 @@ function MyContractBets(props: { contract: Contract; bets: Bet[] }) {
/>
</Row>
<Row className="items-center gap-2 text-sm text-gray-500 flex-1">
<Row className="flex-1 items-center gap-2 text-sm text-gray-500">
{isBinary && (
<>
{resolution ? (
@ -361,22 +359,26 @@ export function MyBetsSummary(props: {
{formatMoney(expectation)}
</div>
</Col> */}
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <YesLabel />
</div>
<div className="whitespace-nowrap">
{formatMoney(yesWinnings)}
</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <NoLabel />
</div>
<div className="whitespace-nowrap">
{formatMoney(noWinnings)}
</div>
</Col>
{isBinary && (
<>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <YesLabel />
</div>
<div className="whitespace-nowrap">
{formatMoney(yesWinnings)}
</div>
</Col>
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
Payout if <NoLabel />
</div>
<div className="whitespace-nowrap">
{formatMoney(noWinnings)}
</div>
</Col>
</>
)}
<Col>
<div className="whitespace-nowrap text-sm text-gray-500">
{isBinary ? (
@ -421,9 +423,10 @@ export function ContractBetsTable(props: {
<thead>
<tr className="p-2">
<th></th>
<th>{isResolved ? <>Payout</> : <>Sale price</>}</th>
<th>Outcome</th>
<th>Amount</th>
<th>{isResolved ? <>Payout</> : <>Sale price</>}</th>
{!isResolved && <th>Payout if chosen</th>}
<th>Probability</th>
<th>Shares</th>
<th>Date</th>
@ -455,6 +458,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
shares,
isSold,
isAnte,
loanAmount,
} = bet
const { isResolved, closeTime } = contract
@ -462,7 +466,7 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
const saleAmount = saleBet?.sale?.amount
const saleDisplay = bet.isAnte ? (
const saleDisplay = isAnte ? (
'ANTE'
) : saleAmount !== undefined ? (
<>{formatMoney(saleAmount)} (sold)</>
@ -474,6 +478,11 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
)
)
const payoutIfChosenDisplay =
bet.outcome === '0' && bet.isAnte
? 'N/A'
: formatMoney(calculatePayout(contract, bet, bet.outcome))
return (
<tr>
<td className="text-neutral">
@ -481,11 +490,15 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
<SellButton contract={contract} bet={bet} />
)}
</td>
<td>{saleDisplay}</td>
<td>
<OutcomeLabel outcome={outcome} />
</td>
<td>{formatMoney(amount)}</td>
<td>
{formatMoney(amount)}
{loanAmount ? ` (${formatMoney(loanAmount ?? 0)} loan)` : ''}
</td>
<td>{saleDisplay}</td>
{!isResolved && <td>{payoutIfChosenDisplay}</td>}
<td>
{formatPercent(probBefore)} {formatPercent(probAfter)}
</td>
@ -502,17 +515,19 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
}, [])
const { contract, bet } = props
const { outcome, shares, loanAmount } = bet
const [isSubmitting, setIsSubmitting] = useState(false)
const initialProb = getOutcomeProbability(
contract.totalShares,
bet.outcome === 'NO' ? 'YES' : bet.outcome
outcome === 'NO' ? 'YES' : outcome
)
const outcomeProb = getProbabilityAfterSale(
contract.totalShares,
bet.outcome,
bet.shares
outcome,
shares
)
const saleAmount = calculateSaleAmount(contract, bet)
@ -524,7 +539,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
label: 'Sell',
}}
submitBtn={{ className: 'btn-primary' }}
submitBtn={{ className: 'btn-primary', label: 'Sell' }}
onSubmit={async () => {
setIsSubmitting(true)
await sellBet({ contractId: contract.id, betId: bet.id })
@ -532,15 +547,18 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
}}
>
<div className="mb-4 text-2xl">
Sell <OutcomeLabel outcome={bet.outcome} />
</div>
<div>
Do you want to sell {formatWithCommas(bet.shares)} shares of{' '}
<OutcomeLabel outcome={bet.outcome} /> for {formatMoney(saleAmount)}?
Sell {formatWithCommas(shares)} shares of{' '}
<OutcomeLabel outcome={outcome} /> for {formatMoney(saleAmount)}?
</div>
{!!loanAmount && (
<div className="mt-2">
You will also pay back {formatMoney(loanAmount)} of your loan, for a
net of {formatMoney(saleAmount - loanAmount)}.
</div>
)}
<div className="mt-2 mb-1 text-sm text-gray-500">
Implied probability: {formatPercent(initialProb)} {' '}
<div className="mt-2 mb-1 text-sm">
Market probability: {formatPercent(initialProb)} {' '}
{formatPercent(outcomeProb)}
</div>
</ConfirmationButton>

View File

@ -19,6 +19,7 @@ import { fromNow } from '../lib/util/time'
import { Avatar } from './avatar'
import { Spacer } from './layout/spacer'
import { useState } from 'react'
import { TweetButton } from './tweet-button'
export function ContractCard(props: {
contract: Contract
@ -149,7 +150,8 @@ function AbbrContractDetails(props: {
) : showCloseTime ? (
<Row className="gap-1">
<ClockIcon className="h-5 w-5" />
Closes {fromNow(closeTime || 0)}
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
{fromNow(closeTime || 0)}
</Row>
) : (
<Row className="gap-1">
@ -170,6 +172,8 @@ export function ContractDetails(props: {
const { closeTime, creatorName, creatorUsername } = contract
const { truePool, createdDate, resolvedDate } = contractMetrics(contract)
const tweetText = getTweetText(contract, !!isCreator)
return (
<Col className="gap-2 text-sm text-gray-500 sm:flex-row sm:flex-wrap">
<Row className="flex-wrap items-center gap-x-4 gap-y-2">
@ -222,6 +226,8 @@ export function ContractDetails(props: {
<div className="whitespace-nowrap">{formatMoney(truePool)} pool</div>
</Row>
<TweetButton className={'self-end'} tweetText={tweetText} />
</Row>
</Col>
)
@ -307,9 +313,28 @@ function EditableCloseDate(props: {
className="btn btn-xs btn-ghost"
onClick={() => setIsEditingCloseTime(true)}
>
<PencilIcon className="inline h-4 w-4 mr-2" /> Edit
<PencilIcon className="mr-2 inline h-4 w-4" /> Edit
</button>
))}
</>
)
}
const getTweetText = (contract: Contract, isCreator: boolean) => {
const { question, creatorName, resolution, outcomeType } = contract
const isBinary = outcomeType === 'BINARY'
const tweetQuestion = isCreator
? question
: `${question} Asked by ${creatorName}.`
const tweetDescription = resolution
? `Resolved ${resolution}!`
: isBinary
? `Currently ${getBinaryProbPercent(
contract
)} chance, place your bets here:`
: `Submit your own answer:`
const url = `https://manifold.markets${contractPath(contract)}`
return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
}

View File

@ -46,7 +46,7 @@ import { useAdmin } from '../hooks/use-admin'
function FeedComment(props: {
activityItem: any
moreHref: string
feedType: 'activity' | 'market'
feedType: FeedType
}) {
const { activityItem, moreHref, feedType } = props
const { person, text, amount, outcome, createdTime } = activityItem
@ -65,7 +65,8 @@ function FeedComment(props: {
username={person.username}
name={person.name}
/>{' '}
{bought} {money} of <OutcomeLabel outcome={outcome} />{' '}
{bought} {money}
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
<Timestamp time={createdTime} />
</p>
</div>
@ -90,8 +91,8 @@ function Timestamp(props: { time: number }) {
)
}
function FeedBet(props: { activityItem: any }) {
const { activityItem } = props
function FeedBet(props: { activityItem: any; feedType: FeedType }) {
const { activityItem, feedType } = props
const { id, contractId, amount, outcome, createdTime } = activityItem
const user = useUser()
const isSelf = user?.id == activityItem.userId
@ -122,8 +123,9 @@ function FeedBet(props: { activityItem: any }) {
</div>
<div className="min-w-0 flex-1 py-1.5">
<div className="text-sm text-gray-500">
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money} of{' '}
<OutcomeLabel outcome={outcome} /> <Timestamp time={createdTime} />
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money}
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
<Timestamp time={createdTime} />
{canComment && (
// Allow user to comment in an textarea if they are the creator
<div className="mt-2">
@ -134,7 +136,7 @@ function FeedBet(props: { activityItem: any }) {
placeholder="Add a comment..."
rows={3}
onKeyDown={(e) => {
if (e.key === 'Enter' && e.ctrlKey) {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
submitComment()
}
}}
@ -179,7 +181,7 @@ function EditContract(props: {
e.target.setSelectionRange(text.length, text.length)
}
onKeyDown={(e) => {
if (e.key === 'Enter' && e.ctrlKey) {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
onSave(text)
}
}}
@ -305,15 +307,13 @@ function FeedQuestion(props: { contract: Contract }) {
const { truePool } = contractMetrics(contract)
const isBinary = outcomeType === 'BINARY'
// Currently hidden on mobile; ideally we'd fit this in somewhere.
const closeMessage =
contract.isResolved || !contract.closeTime ? null : (
<span className="float-right hidden text-gray-400 sm:inline">
{formatMoney(truePool)} pool
<>
<span className="mx-2"></span>
{contract.closeTime > Date.now() ? 'Closes' : 'Closed'}
<Timestamp time={contract.closeTime || 0} />
</span>
</>
)
return (
@ -330,7 +330,11 @@ function FeedQuestion(props: { contract: Contract }) {
username={creatorUsername}
/>{' '}
asked
{closeMessage}
{/* Currently hidden on mobile; ideally we'd fit this in somewhere. */}
<span className="float-right hidden text-gray-400 sm:inline">
{formatMoney(truePool)} pool
{closeMessage}
</span>
</div>
<Col className="mb-4 items-start justify-between gap-2 sm:flex-row sm:gap-4">
<SiteLink
@ -380,6 +384,29 @@ function FeedDescription(props: { contract: Contract }) {
)
}
function FeedAnswer(props: { contract: Contract; outcome: string }) {
const { contract, outcome } = props
const answer = contract?.answers?.[Number(outcome) - 1]
if (!answer) return null
return (
<>
<Avatar username={answer.username} avatarUrl={answer.avatarUrl} />
<div className="min-w-0 flex-1 py-1.5">
<div className="text-sm text-gray-500">
<UserLink
className="text-gray-900"
name={answer.name}
username={answer.username}
/>{' '}
submitted answer <OutcomeLabel outcome={outcome} />{' '}
<Timestamp time={contract.createdTime} />
</div>
</div>
</>
)
}
function OutcomeIcon(props: { outcome?: string }) {
const { outcome } = props
switch (outcome) {
@ -538,8 +565,12 @@ function groupBets(
return items as ActivityItem[]
}
function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
const { bets, outcome } = props
function BetGroupSpan(props: {
bets: Bet[]
outcome: string
feedType: FeedType
}) {
const { bets, outcome, feedType } = props
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
@ -554,14 +585,14 @@ function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
{buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>}
{sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>}
</JoinSpans>
of <OutcomeLabel outcome={outcome} />
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />{' '}
</span>
)
}
// TODO: Make this expandable to show all grouped bets?
function FeedBetGroup(props: { activityItem: any }) {
const { activityItem } = props
function FeedBetGroup(props: { activityItem: any; feedType: FeedType }) {
const { activityItem, feedType } = props
const bets: Bet[] = activityItem.bets
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
@ -583,7 +614,11 @@ function FeedBetGroup(props: { activityItem: any }) {
<div className="text-sm text-gray-500">
{outcomes.map((outcome, index) => (
<Fragment key={outcome}>
<BetGroupSpan outcome={outcome} bets={betGroups[outcome]} />
<BetGroupSpan
outcome={outcome}
bets={betGroups[outcome]}
feedType={feedType}
/>
{index !== outcomes.length - 1 && <br />}
</Fragment>
))}
@ -621,6 +656,18 @@ function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) {
)
}
// On 'multi' feeds, the outcome is redundant, so we hide it
function MaybeOutcomeLabel(props: { outcome: string; feedType: FeedType }) {
const { outcome, feedType } = props
return feedType === 'multi' ? null : (
<span>
{' '}
of <OutcomeLabel outcome={outcome} />
{/* TODO: Link to the correct e.g. #23 */}
</span>
)
}
// Missing feed items:
// - Bet sold?
type ActivityItem = {
@ -635,15 +682,23 @@ type ActivityItem = {
| 'expand'
}
type FeedType =
// Main homepage/fold feed,
| 'activity'
// Comments feed on a market
| 'market'
// Grouped for a multi-category outcome
| 'multi'
export function ContractFeed(props: {
contract: Contract
bets: Bet[]
comments: Comment[]
// Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market
feedType: 'activity' | 'market'
feedType: FeedType
outcome?: string // Which multi-category outcome to filter
betRowClassName?: string
}) {
const { contract, feedType, betRowClassName } = props
const { contract, feedType, outcome, betRowClassName } = props
const { id, outcomeType } = contract
const isBinary = outcomeType === 'BINARY'
@ -655,6 +710,10 @@ export function ContractFeed(props: {
? bets.filter((bet) => !bet.isAnte)
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
if (feedType === 'multi') {
bets = bets.filter((bet) => bet.outcome === outcome)
}
const comments = useComments(id) ?? props.comments
const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
@ -669,6 +728,10 @@ export function ContractFeed(props: {
if (contract.resolution) {
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
}
if (feedType === 'multi') {
// Hack to add some more padding above the 'multi' feedType, by adding a null item
allItems.unshift({ type: '', id: -1 })
}
// If there are more than 5 items, only show the first, an expand item, and last 3
let items = allItems
@ -682,45 +745,69 @@ export function ContractFeed(props: {
return (
<div className="flow-root pr-2 md:pr-0">
<ul role="list" className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
{items.map((activityItem, activityItemIdx) => (
<li key={activityItem.id}>
<div className="relative pb-8">
{activityItemIdx !== items.length - 1 ? (
<span
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
aria-hidden="true"
<div key={activityItem.id} className="relative pb-8">
{activityItemIdx !== items.length - 1 ? (
<span
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
aria-hidden="true"
/>
) : null}
<div className="relative flex items-start space-x-3">
{activityItem.type === 'start' ? (
feedType === 'activity' ? (
<FeedQuestion contract={contract} />
) : feedType === 'market' ? (
<FeedDescription contract={contract} />
) : feedType === 'multi' ? (
<FeedAnswer contract={contract} outcome={outcome || '0'} />
) : null
) : activityItem.type === 'comment' ? (
<FeedComment
activityItem={activityItem}
moreHref={contractPath(contract)}
feedType={feedType}
/>
) : activityItem.type === 'bet' ? (
<FeedBet activityItem={activityItem} feedType={feedType} />
) : activityItem.type === 'betgroup' ? (
<FeedBetGroup activityItem={activityItem} feedType={feedType} />
) : activityItem.type === 'close' ? (
<FeedClose contract={contract} />
) : activityItem.type === 'resolve' ? (
<FeedResolve contract={contract} />
) : activityItem.type === 'expand' ? (
<FeedExpand setExpanded={setExpanded} />
) : null}
<div className="relative flex items-start space-x-3">
{activityItem.type === 'start' ? (
feedType == 'activity' ? (
<FeedQuestion contract={contract} />
) : (
<FeedDescription contract={contract} />
)
) : activityItem.type === 'comment' ? (
<FeedComment
activityItem={activityItem}
moreHref={contractPath(contract)}
feedType={feedType}
/>
) : activityItem.type === 'bet' ? (
<FeedBet activityItem={activityItem} />
) : activityItem.type === 'betgroup' ? (
<FeedBetGroup activityItem={activityItem} />
) : activityItem.type === 'close' ? (
<FeedClose contract={contract} />
) : activityItem.type === 'resolve' ? (
<FeedResolve contract={contract} />
) : activityItem.type === 'expand' ? (
<FeedExpand setExpanded={setExpanded} />
) : null}
</div>
</div>
</li>
</div>
))}
</ul>
</div>
{isBinary && tradingAllowed(contract) && (
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
)}
</div>
)
}
export function ContractSummaryFeed(props: {
contract: Contract
betRowClassName?: string
}) {
const { contract, betRowClassName } = props
const { outcomeType } = contract
const isBinary = outcomeType === 'BINARY'
return (
<div className="flow-root pr-2 md:pr-0">
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
<div className="relative pb-8">
<div className="relative flex items-start space-x-3">
<FeedQuestion contract={contract} />
</div>
</div>
</div>
{isBinary && tradingAllowed(contract) && (
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
)}

View File

@ -1,9 +1,7 @@
import {
Contract,
deleteContract,
contractPath,
tradingAllowed,
getBinaryProbPercent,
} from '../lib/firebase/contracts'
import { Col } from './layout/col'
import { Spacer } from './layout/spacer'
@ -15,7 +13,6 @@ import { Linkify } from './linkify'
import clsx from 'clsx'
import { ContractDetails, ResolutionOrChance } from './contract-card'
import { ContractFeed } from './contract-feed'
import { TweetButton } from './tweet-button'
import { Bet } from '../../common/bet'
import { Comment } from '../../common/comment'
import { RevealableTagsInput, TagsInput } from './tags-input'
@ -38,8 +35,6 @@ export const ContractOverview = (props: {
const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY'
const tweetText = getTweetText(contract, isCreator)
return (
<Col className={clsx('mb-6', className)}>
<Row className="justify-between gap-4 px-2">
@ -92,11 +87,9 @@ export const ContractOverview = (props: {
) : (
<FoldTagList folds={folds} />
)}
<TweetButton tweetText={tweetText} />
</Row>
<Col className="mt-6 gap-4 sm:hidden">
<TweetButton className="self-end" tweetText={tweetText} />
{folds.length === 0 ? (
<TagsInput contract={contract} />
) : (
@ -136,22 +129,3 @@ export const ContractOverview = (props: {
</Col>
)
}
const getTweetText = (contract: Contract, isCreator: boolean) => {
const { question, creatorName, resolution, outcomeType } = contract
const isBinary = outcomeType === 'BINARY'
const tweetQuestion = isCreator
? question
: `${question} Asked by ${creatorName}.`
const tweetDescription = resolution
? `Resolved ${resolution}!`
: isBinary
? `Currently ${getBinaryProbPercent(
contract
)} chance, place your bets here:`
: `Submit your own answer:`
const url = `https://manifold.markets${contractPath(contract)}`
return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
}

View File

@ -83,7 +83,7 @@ export function ContractProbGraph(props: { contract: Contract; bets: Bet[] }) {
format: (time) => formatTime(+time, lessThanAWeek),
}}
colors={{ datum: 'color' }}
pointSize={10}
pointSize={bets.length > 100 ? 0 : 10}
pointBorderWidth={1}
pointBorderColor="#fff"
enableSlices="x"

View File

@ -205,16 +205,19 @@ export function SearchableGrid(props: {
}) {
const { contracts, query, setQuery, sort, setSort, byOneCreator } = props
const queryWords = query.toLowerCase().split(' ')
function check(corpus: String) {
return corpus.toLowerCase().includes(query.toLowerCase())
return queryWords.every((word) => corpus.toLowerCase().includes(word))
}
let matches = contracts.filter(
(c) =>
check(c.question) ||
check(c.description) ||
check(c.creatorName) ||
check(c.creatorUsername) ||
check(c.lowercaseTags.map((tag) => `#${tag}`).join(' '))
check(c.lowercaseTags.map((tag) => `#${tag}`).join(' ')) ||
check((c.answers ?? []).map((answer) => answer.text).join(' '))
)
if (sort === 'newest' || sort === 'all') {
@ -226,11 +229,13 @@ export function SearchableGrid(props: {
)
} else if (sort === 'oldest') {
matches.sort((a, b) => a.createdTime - b.createdTime)
} else if (sort === 'close-date') {
} else if (sort === 'close-date' || sort === 'closed') {
matches = _.sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
matches = _.sortBy(matches, (contract) => contract.closeTime)
// Hide contracts that have already closed
matches = matches.filter(({ closeTime }) => (closeTime || 0) > Date.now())
const hideClosed = sort === 'closed'
matches = matches.filter(
({ closeTime }) => closeTime && closeTime > Date.now() !== hideClosed
)
} else if (sort === 'most-traded') {
matches.sort(
(a, b) => contractMetrics(b).truePool - contractMetrics(a).truePool
@ -269,6 +274,7 @@ export function SearchableGrid(props: {
<option value="most-traded">Most traded</option>
<option value="24-hour-vol">24h volume</option>
<option value="close-date">Closing soon</option>
<option value="closed">Closed</option>
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
@ -286,7 +292,7 @@ export function SearchableGrid(props: {
) : (
<ContractsGrid
contracts={matches}
showCloseTime={sort == 'close-date'}
showCloseTime={['close-date', 'closed'].includes(sort)}
/>
)}
</div>

View File

@ -101,7 +101,7 @@ export const FastFoldFollowing = (props: {
]}
/>
<Spacer h={10} />
<Spacer h={5} />
</>
)
}

View File

@ -34,10 +34,6 @@ function getNavigationOptions(
name: 'Home',
href: user ? '/home' : '/',
},
{
name: `Your profile`,
href: `/${user?.username}`,
},
...(mobile
? [
{
@ -50,10 +46,18 @@ function getNavigationOptions(
},
]
: []),
{
name: `Your profile`,
href: `/${user?.username}`,
},
{
name: 'Your trades',
href: '/trades',
},
{
name: 'Add funds',
href: '/add-funds',
},
{
name: 'Leaderboards',
href: '/leaderboards',

View File

@ -1,12 +1,8 @@
import { useUser } from './use-user'
import { isAdmin } from '../../common/access'
import { usePrivateUser, useUser } from './use-user'
export const useAdmin = () => {
const user = useUser()
const adminIds = [
'igi2zGXsfxYPgB0DJTXVJVmwCOr2', // Austin
'5LZ4LgYuySdL1huCWe7bti02ghx2', // James
'tlmGNz9kjXc2EteizMORes4qvWl2', // Stephen
'IPTOzEqrpkWmEzh6hwvAyY9PqFb2', // Manifold
]
return adminIds.includes(user?.id || '')
const privateUser = usePrivateUser(user?.id)
return isAdmin(privateUser?.email || '')
}

View File

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { Contract } from '../../common/contract'
import {
Bet,
getRecentBets,
listenForBets,
listenForRecentBets,
withoutAnteBets,
@ -36,3 +37,11 @@ export const useRecentBets = () => {
useEffect(() => listenForRecentBets(setRecentBets), [])
return recentBets
}
export const useGetRecentBets = () => {
const [recentBets, setRecentBets] = useState<Bet[] | undefined>()
useEffect(() => {
getRecentBets().then(setRecentBets)
}, [])
return recentBets
}

View File

@ -2,8 +2,10 @@ import _ from 'lodash'
import { useEffect, useState } from 'react'
import {
Contract,
listenForActiveContracts,
listenForContracts,
listenForHotContracts,
listenForInactiveContracts,
} from '../lib/firebase/contracts'
import { listenForTaggedContracts } from '../lib/firebase/folds'
@ -17,6 +19,26 @@ export const useContracts = () => {
return contracts
}
export const useActiveContracts = () => {
const [contracts, setContracts] = useState<Contract[] | undefined>()
useEffect(() => {
return listenForActiveContracts(setContracts)
}, [])
return contracts
}
export const useInactiveContracts = () => {
const [contracts, setContracts] = useState<Contract[] | undefined>()
useEffect(() => {
return listenForInactiveContracts(setContracts)
}, [])
return contracts
}
export const useUpdatedContracts = (initialContracts: Contract[]) => {
const [contracts, setContracts] = useState(initialContracts)

View File

@ -1,24 +1,22 @@
import _ from 'lodash'
import { useRef } from 'react'
import { useMemo, useRef } from 'react'
import { Fold } from '../../common/fold'
import { User } from '../../common/user'
import { filterDefined } from '../../common/util/array'
import { Bet, getRecentBets } from '../lib/firebase/bets'
import { Comment, getRecentComments } from '../lib/firebase/comments'
import { Contract, listAllContracts } from '../lib/firebase/contracts'
import { Contract, getActiveContracts } from '../lib/firebase/contracts'
import { listAllFolds } from '../lib/firebase/folds'
import { findActiveContracts } from '../pages/activity'
import { useRecentBets } from './use-bets'
import { useRecentComments } from './use-comments'
import { useContracts } from './use-contracts'
import { useInactiveContracts } from './use-contracts'
import { useFollowedFolds } from './use-fold'
import { useUserBetContracts } from './use-user-bets'
// used in static props
export const getAllContractInfo = async () => {
let [contracts, folds] = await Promise.all([
listAllContracts().catch((_) => []),
getActiveContracts().catch((_) => []),
listAllFolds().catch(() => []),
])
@ -30,25 +28,15 @@ export const getAllContractInfo = async () => {
return { contracts, recentBets, recentComments, folds }
}
export const useActiveContracts = (
props: {
contracts: Contract[]
folds: Fold[]
recentBets: Bet[]
recentComments: Comment[]
},
user: User | undefined | null
export const useFilterYourContracts = (
user: User | undefined | null,
folds: Fold[],
contracts: Contract[]
) => {
const contracts = useContracts() ?? props.contracts
const recentBets = useRecentBets() ?? props.recentBets
const recentComments = useRecentComments() ?? props.recentComments
const followedFoldIds = useFollowedFolds(user)
const followedFolds = filterDefined(
(followedFoldIds ?? []).map((id) =>
props.folds.find((fold) => fold.id === id)
)
(followedFoldIds ?? []).map((id) => folds.find((fold) => fold.id === id))
)
// Save the initial followed fold slugs.
@ -67,23 +55,35 @@ export const useActiveContracts = (
: undefined
// Show no contracts before your info is loaded.
let feedContracts: Contract[] = []
let yourContracts: Contract[] = []
if (yourBetContracts && followedFoldIds) {
// Show all contracts if no folds are followed.
if (followedFoldIds.length === 0) feedContracts = contracts
if (followedFoldIds.length === 0) yourContracts = contracts
else
feedContracts = contracts.filter(
yourContracts = contracts.filter(
(contract) =>
contract.lowercaseTags.some((tag) => tagSet.has(tag)) ||
yourBetContracts.has(contract.id)
)
}
return {
yourContracts,
initialFollowedFoldSlugs,
}
}
export const useFindActiveContracts = (props: {
contracts: Contract[]
recentBets: Bet[]
recentComments: Comment[]
}) => {
const { contracts, recentBets, recentComments } = props
const activeContracts = findActiveContracts(
feedContracts,
contracts,
recentComments,
recentBets,
365
recentBets
)
const betsByContract = _.groupBy(recentBets, (bet) => bet.contractId)
@ -105,6 +105,24 @@ export const useActiveContracts = (
activeContracts,
activeBets,
activeComments,
initialFollowedFoldSlugs,
}
}
export const useExploreContracts = (maxContracts = 75) => {
const inactiveContracts = useInactiveContracts()
const contractsDict = _.fromPairs(
(inactiveContracts ?? []).map((c) => [c.id, c])
)
// Preserve random ordering once inactiveContracts loaded.
const exploreContractIds = useMemo(
() => _.shuffle(Object.keys(contractsDict)),
// eslint-disable-next-line react-hooks/exhaustive-deps
[!!inactiveContracts]
).slice(0, maxContracts)
if (!inactiveContracts) return undefined
return filterDefined(exploreContractIds.map((id) => contractsDict[id]))
}

View File

@ -1,4 +1,6 @@
import _ from 'lodash'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
export type Sort =
| 'creator'
@ -8,6 +10,7 @@ export type Sort =
| 'most-traded'
| '24-hour-vol'
| 'close-date'
| 'closed'
| 'resolved'
| 'all'
@ -24,19 +27,34 @@ export function useQueryAndSortParams(options?: { defaultSort: Sort }) {
router.push(router, undefined, { shallow: true })
}
const setQuery = (query: string | undefined) => {
if (query) {
router.query.q = query
} else {
delete router.query.q
}
const [queryState, setQueryState] = useState(query)
router.push(router, undefined, { shallow: true })
useEffect(() => {
setQueryState(query)
}, [query])
// Debounce router query update.
const pushQuery = useMemo(
() =>
_.debounce((query: string | undefined) => {
if (query) {
router.query.q = query
} else {
delete router.query.q
}
router.push(router, undefined, { shallow: true })
}, 500),
[router]
)
const setQuery = (query: string | undefined) => {
setQueryState(query)
pushQuery(query)
}
return {
sort: sort ?? options?.defaultSort ?? '24-hour-vol',
query: query ?? '',
query: queryState ?? '',
setSort,
setQuery,
}

View File

@ -1,6 +1,10 @@
import _ from 'lodash'
import { useEffect, useState } from 'react'
import { Bet, listenForUserBets } from '../lib/firebase/bets'
import {
Bet,
listenForUserBets,
listenForUserContractBets,
} from '../lib/firebase/bets'
export const useUserBets = (userId: string | undefined) => {
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
@ -12,6 +16,20 @@ export const useUserBets = (userId: string | undefined) => {
return bets
}
export const useUserContractBets = (
userId: string | undefined,
contractId: string | undefined
) => {
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
useEffect(() => {
if (userId && contractId)
return listenForUserContractBets(userId, contractId, setBets)
}, [userId, contractId])
return bets
}
export const useUserBetContracts = (userId: string | undefined) => {
const [contractIds, setContractIds] = useState<string[] | undefined>()

View File

@ -74,6 +74,21 @@ export function listenForUserBets(
})
}
export function listenForUserContractBets(
userId: string,
contractId: string,
setBets: (bets: Bet[]) => void
) {
const betsQuery = query(
collection(db, 'contracts', contractId, 'bets'),
where('userId', '==', userId)
)
return listenForValues<Bet>(betsQuery, (bets) => {
bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime)
setBets(bets)
})
}
export function withoutAnteBets(contract: Contract, bets?: Bet[]) {
const { createdTime } = contract

View File

@ -115,6 +115,41 @@ export function listenForContracts(
return listenForValues<Contract>(q, setContracts)
}
const activeContractsQuery = query(
contractCollection,
where('isResolved', '==', false),
where('visibility', '==', 'public'),
where('volume24Hours', '>', 0)
)
export function getActiveContracts() {
return getValues<Contract>(activeContractsQuery)
}
export function listenForActiveContracts(
setContracts: (contracts: Contract[]) => void
) {
return listenForValues<Contract>(activeContractsQuery, setContracts)
}
const inactiveContractsQuery = query(
contractCollection,
where('isResolved', '==', false),
where('closeTime', '>', Date.now()),
where('visibility', '==', 'public'),
where('volume24Hours', '==', 0)
)
export function getInactiveContracts() {
return getValues<Contract>(inactiveContractsQuery)
}
export function listenForInactiveContracts(
setContracts: (contracts: Contract[]) => void
) {
return listenForValues<Contract>(inactiveContractsQuery, setContracts)
}
export function listenForContract(
contractId: string,
setContract: (contract: Contract | null) => void

View File

@ -54,13 +54,12 @@ export async function getFoldBySlug(slug: string) {
}
function contractsByTagsQuery(tags: string[]) {
// TODO: if tags.length > 10, execute multiple parallel queries
const lowercaseTags = tags.map((tag) => tag.toLowerCase()).slice(0, 10)
return query(
contractCollection,
where(
'lowercaseTags',
'array-contains-any',
tags.map((tag) => tag.toLowerCase())
)
where('lowercaseTags', 'array-contains-any', lowercaseTags)
)
}
@ -74,7 +73,6 @@ export async function getFoldContracts(fold: Fold) {
} = fold
const [tagsContracts, includedContracts] = await Promise.all([
// TODO: if tags.length > 10, execute multiple parallel queries
tags.length > 0 ? getValues<Contract>(contractsByTagsQuery(tags)) : [],
// TODO: if contractIds.length > 10, execute multiple parallel queries
@ -163,9 +161,10 @@ export function listenForFollow(
export async function getFoldsByTags(tags: string[]) {
if (tags.length === 0) return []
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
// TODO: split into multiple queries if tags.length > 10.
const lowercaseTags = tags.map((tag) => tag.toLowerCase()).slice(0, 10)
const folds = await getValues<Fold>(
// TODO: split into multiple queries if tags.length > 10.
query(
foldCollection,
where('lowercaseTags', 'array-contains-any', lowercaseTags)
@ -179,13 +178,13 @@ export function listenForFoldsWithTags(
tags: string[],
setFolds: (folds: Fold[]) => void
) {
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
const q =
// TODO: split into multiple queries if tags.length > 10.
query(
foldCollection,
where('lowercaseTags', 'array-contains-any', lowercaseTags)
)
// TODO: split into multiple queries if tags.length > 10.
const lowercaseTags = tags.map((tag) => tag.toLowerCase()).slice(0, 10)
const q = query(
foldCollection,
where('lowercaseTags', 'array-contains-any', lowercaseTags)
)
return listenForValues<Fold>(q, (folds) => {
const sorted = _.sortBy(folds, (fold) => -1 * fold.followCount)

View File

@ -1,28 +1,41 @@
import { getFirestore } from '@firebase/firestore'
import { initializeApp, getApps, getApp } from 'firebase/app'
// Used to decide which Stripe instance to point to
export const isProd = process.env.NEXT_PUBLIC_FIREBASE_ENV !== 'DEV'
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',
}
const FIREBASE_CONFIGS = {
PROD: {
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',
},
DEV: {
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',
},
THEOREMONE: {
apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M',
authDomain: 'theoremone-manifold.firebaseapp.com',
projectId: 'theoremone-manifold',
storageBucket: 'theoremone-manifold.appspot.com',
messagingSenderId: '698012149198',
appId: '1:698012149198:web:b342af75662831aa84b79f',
measurementId: 'G-Y3EZ1WNT6E',
},
}
const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
// @ts-ignore
const firebaseConfig = FIREBASE_CONFIGS[ENV]
// Initialize Firebase
export const app = getApps().length ? getApp() : initializeApp(firebaseConfig)

View File

@ -1,6 +1,5 @@
import { db } from './init'
import {
doc,
getDoc,
getDocs,
onSnapshot,
@ -22,7 +21,9 @@ export function listenForValue<T>(
docRef: DocumentReference,
setValue: (value: T | null) => void
) {
return onSnapshot(docRef, (snapshot) => {
// Exclude cached snapshots so we only trigger on fresh data.
// includeMetadataChanges ensures listener is called even when server data is the same as cached data.
return onSnapshot(docRef, { includeMetadataChanges: true }, (snapshot) => {
if (snapshot.metadata.fromCache) return
const value = snapshot.exists() ? (snapshot.data() as T) : null
@ -34,7 +35,9 @@ export function listenForValues<T>(
query: Query,
setValues: (values: T[]) => void
) {
return onSnapshot(query, (snapshot) => {
// Exclude cached snapshots so we only trigger on fresh data.
// includeMetadataChanges ensures listener is called even when server data is the same as cached data.
return onSnapshot(query, { includeMetadataChanges: true }, (snapshot) => {
if (snapshot.metadata.fromCache) return
const values = snapshot.docs.map((doc) => doc.data() as T)

View File

@ -145,7 +145,7 @@ export default function ContractPage(props: {
<Col className="flex-1">
{allowTrade && (
<BetPanel className="hidden lg:inline" contract={contract} />
<BetPanel className="hidden lg:flex" contract={contract} />
)}
{allowResolve && (
<ResolutionPanel creator={user} contract={contract} />
@ -177,12 +177,8 @@ function BetsSection(props: {
return (
<div>
<Title className="px-2" text="Your trades" />
{isBinary && (
<>
<MyBetsSummary className="px-2" contract={contract} bets={userBets} />
<Spacer h={6} />
</>
)}
<MyBetsSummary className="px-2" contract={contract} bets={userBets} />
<Spacer h={6} />
<ContractBetsTable contract={contract} bets={userBets} />
<Spacer h={12} />
</div>

View File

@ -139,6 +139,15 @@ function Contents() {
bettors that are correct more often will gain influence, leading to
better-calibrated forecasts over time.
</p>
<p>
Since our launch, we've seen hundreds of users trade each day, on over a
thousand different markets! You can track the popularity of our platform
at{' '}
<a href="http://manifold.markets/analytics">
http://manifold.markets/analytics
</a>
.
</p>
<h3 id="how-are-markets-resolved-">How are markets resolved?</h3>
<p>
The creator of the prediction market decides the outcome and earns{' '}

View File

@ -1,14 +1,12 @@
import _ from 'lodash'
import { ContractFeed } from '../components/contract-feed'
import { ContractFeed, ContractSummaryFeed } from '../components/contract-feed'
import { Page } from '../components/page'
import { Contract } from '../lib/firebase/contracts'
import { Comment } from '../lib/firebase/comments'
import { Col } from '../components/layout/col'
import { Bet } from '../../common/bet'
import { filterDefined } from '../../common/util/array'
const MAX_ACTIVE_CONTRACTS = 75
const MAX_HOT_MARKETS = 10
// This does NOT include comment times, since those aren't part of the contract atm.
// TODO: Maybe store last activity time directly in the contract?
@ -25,12 +23,11 @@ function lastActivityTime(contract: Contract) {
// - Comment on a market
// - New market created
// - Market resolved
// - Markets with most betting in last 24 hours
// - Bet on market
export function findActiveContracts(
allContracts: Contract[],
recentComments: Comment[],
recentBets: Bet[],
daysAgo = 3
recentBets: Bet[]
) {
const idToActivityTime = new Map<string, number>()
function record(contractId: string, time: number) {
@ -39,52 +36,38 @@ export function findActiveContracts(
idToActivityTime.set(contractId, Math.max(oldTime ?? 0, time))
}
let contracts: Contract[] = []
const contractsById = new Map(allContracts.map((c) => [c.id, c]))
// Find contracts with activity in the last 3 days
const DAY_IN_MS = 24 * 60 * 60 * 1000
for (const contract of allContracts || []) {
if (lastActivityTime(contract) > Date.now() - daysAgo * DAY_IN_MS) {
contracts.push(contract)
record(contract.id, lastActivityTime(contract))
}
// Record contract activity.
for (const contract of allContracts) {
record(contract.id, lastActivityTime(contract))
}
// Add every contract that had a recent comment, too
const contractsById = new Map(allContracts.map((c) => [c.id, c]))
for (const comment of recentComments) {
const contract = contractsById.get(comment.contractId)
if (contract) {
contracts.push(contract)
record(contract.id, comment.createdTime)
}
if (contract) record(contract.id, comment.createdTime)
}
// Add recent top-trading contracts, ordered by last bet.
// Add contracts by last bet time.
const contractBets = _.groupBy(recentBets, (bet) => bet.contractId)
const contractTotalBets = _.mapValues(contractBets, (bets) =>
_.sumBy(bets, (bet) => bet.amount)
const contractMostRecentBet = _.mapValues(
contractBets,
(bets) => _.maxBy(bets, (bet) => bet.createdTime) as Bet
)
const sortedPairs = _.sortBy(
_.toPairs(contractTotalBets),
([_, total]) => -1 * total
)
const topTradedContracts = filterDefined(
sortedPairs.map(([id]) => contractsById.get(id))
).slice(0, MAX_HOT_MARKETS)
for (const contract of topTradedContracts) {
const bet = recentBets.find((bet) => bet.contractId === contract.id)
if (bet) {
contracts.push(contract)
record(contract.id, bet.createdTime)
}
for (const bet of Object.values(contractMostRecentBet)) {
const contract = contractsById.get(bet.contractId)
if (contract) record(contract.id, bet.createdTime)
}
contracts = _.uniqBy(contracts, (c) => c.id)
contracts = contracts.filter((contract) => contract.visibility === 'public')
contracts = _.sortBy(contracts, (c) => -(idToActivityTime.get(c.id) ?? 0))
return contracts.slice(0, MAX_ACTIVE_CONTRACTS)
let activeContracts = allContracts.filter(
(contract) => contract.visibility === 'public' && !contract.isResolved
)
activeContracts = _.sortBy(
activeContracts,
(c) => -(idToActivityTime.get(c.id) ?? 0)
)
return activeContracts.slice(0, MAX_ACTIVE_CONTRACTS)
}
export function ActivityFeed(props: {
@ -116,6 +99,24 @@ export function ActivityFeed(props: {
)
}
export function SummaryActivityFeed(props: { contracts: Contract[] }) {
const { contracts } = props
return (
<Col className="items-center">
<Col className="w-full max-w-3xl">
<Col className="w-full divide-y divide-gray-300 self-center bg-white">
{contracts.map((contract) => (
<div key={contract.id} className="py-6 px-2 sm:px-4">
<ContractSummaryFeed contract={contract} />
</div>
))}
</Col>
</Col>
</Col>
)
}
export default function ActivityPage() {
return (
<Page>

View File

@ -19,11 +19,11 @@ export default function AddFundsPage() {
<SEO title="Add funds" description="Add funds" url="/add-funds" />
<Col className="items-center">
<Col>
<Title text="Get Manifold Dollars" />
<Col className="bg-white rounded sm:shadow-md p-4 py-8 sm:p-8 h-full">
<Title className="!mt-0" text="Get Manifold Dollars" />
<img
className="mt-6 block"
src="/praying-mantis-light.svg"
className="mb-6 block self-center -scale-x-100"
src="/stylized-crane-black.png"
width={200}
height={200}
/>
@ -50,7 +50,7 @@ export default function AddFundsPage() {
<form
action={checkoutURL(user?.id || '', amountSelected)}
method="POST"
className="mt-12"
className="mt-8"
>
<button
type="submit"

View File

@ -26,6 +26,7 @@ export type LiteMarket = {
volume24Hours: number
isResolved: boolean
resolution?: string
resolutionTime?: number
}
export type FullMarket = LiteMarket & {
@ -54,6 +55,7 @@ export function toLiteMarket({
volume24Hours,
isResolved,
resolution,
resolutionTime,
}: Contract): LiteMarket {
return {
id,
@ -61,7 +63,10 @@ export function toLiteMarket({
creatorName,
createdTime,
creatorAvatarUrl,
closeTime,
closeTime:
resolutionTime && closeTime
? Math.min(resolutionTime, closeTime)
: closeTime,
question,
description,
tags,
@ -72,5 +77,6 @@ export function toLiteMarket({
volume24Hours,
isResolved,
resolution,
resolutionTime,
}
}

View File

@ -248,6 +248,7 @@ export function NewContract(props: { question: string; tag?: string }) {
error={anteError}
setError={setAnteError}
disabled={isSubmitting}
contractId={undefined}
/>
</div>

View File

@ -5,12 +5,7 @@ import { Fold } from '../../../../common/fold'
import { Comment } from '../../../../common/comment'
import { Page } from '../../../components/page'
import { Title } from '../../../components/title'
import {
Bet,
getRecentContractBets,
listAllBets,
} from '../../../lib/firebase/bets'
import { listAllComments } from '../../../lib/firebase/comments'
import { Bet, listAllBets } from '../../../lib/firebase/bets'
import { Contract } from '../../../lib/firebase/contracts'
import {
foldPath,
@ -40,6 +35,7 @@ import FeedCreate from '../../../components/feed-create'
import { SEO } from '../../../components/SEO'
import { useTaggedContracts } from '../../../hooks/use-contracts'
import { Linkify } from '../../../components/linkify'
import { filterDefined } from '../../../../common/util/array'
export async function getStaticProps(props: { params: { slugs: string[] } }) {
const { slugs } = props.params
@ -49,42 +45,21 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
const betsPromise = Promise.all(
const bets = await Promise.all(
contracts.map((contract) => listAllBets(contract.id))
)
const betsByContract = _.fromPairs(contracts.map((c, i) => [c.id, bets[i]]))
const [contractComments, contractRecentBets] = await Promise.all([
Promise.all(
contracts.map((contract) => listAllComments(contract.id).catch((_) => []))
),
Promise.all(
contracts.map((contract) =>
getRecentContractBets(contract.id).catch((_) => [])
)
),
])
let activeContracts = findActiveContracts(
contracts,
_.flatten(contractComments),
_.flatten(contractRecentBets),
365
)
let activeContracts = findActiveContracts(contracts, [], _.flatten(bets))
const [resolved, unresolved] = _.partition(
activeContracts,
({ isResolved }) => isResolved
)
activeContracts = [...unresolved, ...resolved]
const activeContractBets = await Promise.all(
activeContracts.map((contract) => listAllBets(contract.id).catch((_) => []))
const activeContractBets = activeContracts.map(
(contract) => betsByContract[contract.id] ?? []
)
const activeContractComments = activeContracts.map(
(contract) =>
contractComments[contracts.findIndex((c) => c.id === contract.id)]
)
const bets = await betsPromise
const creatorScores = scoreCreators(contracts, bets)
const traderScores = scoreTraders(contracts, bets)
@ -102,7 +77,7 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
contracts,
activeContracts,
activeContractBets,
activeContractComments,
activeContractComments: activeContracts.map(() => []),
traderScores,
topTraders,
creatorScores,
@ -169,9 +144,11 @@ export default function FoldPage(props: {
taggedContracts.map((contract) => [contract.id, contract])
)
const contracts = props.contracts.map((contract) => contractsMap[contract.id])
const activeContracts = props.activeContracts.map(
(contract) => contractsMap[contract.id]
const contracts = filterDefined(
props.contracts.map((contract) => contractsMap[contract.id])
)
const activeContracts = filterDefined(
props.activeContracts.map((contract) => contractsMap[contract.id])
)
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {

View File

@ -1,9 +1,12 @@
import React from 'react'
import React, { useState } from 'react'
import Router from 'next/router'
import { SparklesIcon, GlobeAltIcon } from '@heroicons/react/solid'
import clsx from 'clsx'
import _ from 'lodash'
import { Contract } from '../lib/firebase/contracts'
import { Page } from '../components/page'
import { ActivityFeed } from './activity'
import { ActivityFeed, SummaryActivityFeed } from './activity'
import { Comment } from '../lib/firebase/comments'
import { Bet } from '../lib/firebase/bets'
import FeedCreate from '../components/feed-create'
@ -13,12 +16,15 @@ import { useUser } from '../hooks/use-user'
import { Fold } from '../../common/fold'
import { LoadingIndicator } from '../components/loading-indicator'
import { Row } from '../components/layout/row'
import { SparklesIcon } from '@heroicons/react/solid'
import { FastFoldFollowing } from '../components/fast-fold-following'
import {
getAllContractInfo,
useActiveContracts,
} from '../hooks/use-active-contracts'
useExploreContracts,
useFilterYourContracts,
useFindActiveContracts,
} from '../hooks/use-find-active-contracts'
import { useGetRecentBets } from '../hooks/use-bets'
import { useActiveContracts } from '../hooks/use-contracts'
export async function getStaticProps() {
const contractInfo = await getAllContractInfo()
@ -35,14 +41,27 @@ const Home = (props: {
recentBets: Bet[]
recentComments: Comment[]
}) => {
const { folds, recentComments } = props
const user = useUser()
const {
activeContracts,
activeBets,
activeComments,
initialFollowedFoldSlugs,
} = useActiveContracts(props, user)
const contracts = useActiveContracts() ?? props.contracts
const { yourContracts, initialFollowedFoldSlugs } = useFilterYourContracts(
user,
folds,
contracts
)
const recentBets = useGetRecentBets()
const { activeContracts, activeBets, activeComments } =
useFindActiveContracts({
contracts: yourContracts,
recentBets: recentBets ?? [],
recentComments,
})
const exploreContracts = useExploreContracts()
const [feedMode, setFeedMode] = useState<'activity' | 'explore'>('activity')
if (user === null) {
Router.replace('/')
@ -64,22 +83,52 @@ const Home = (props: {
/>
)}
<Spacer h={5} />
<Col className="mx-3 mb-3 gap-2 text-sm text-gray-800 sm:flex-row">
<Row className="gap-2">
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
<span className="whitespace-nowrap">Recent activity</span>
<div className="tabs">
<div
className={clsx(
'tab gap-2',
feedMode === 'activity' && 'tab-active'
)}
onClick={() => setFeedMode('activity')}
>
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
Recent activity
</div>
<div
className={clsx(
'tab gap-2',
feedMode === 'explore' && 'tab-active'
)}
onClick={() => setFeedMode('explore')}
>
<GlobeAltIcon className="inline h-5 w-5" aria-hidden="true" />
Explore
</div>
</div>
</Row>
</Col>
{activeContracts ? (
<ActivityFeed
contracts={activeContracts}
contractBets={activeBets}
contractComments={activeComments}
/>
) : (
<LoadingIndicator className="mt-4" />
)}
{feedMode === 'activity' &&
(recentBets ? (
<ActivityFeed
contracts={activeContracts}
contractBets={activeBets}
contractComments={activeComments}
/>
) : (
<LoadingIndicator className="mt-4" />
))}
{feedMode === 'explore' &&
(exploreContracts ? (
<SummaryActivityFeed contracts={exploreContracts} />
) : (
<LoadingIndicator className="mt-4" />
))}
</Col>
</Col>
</Page>

View File

@ -245,6 +245,7 @@ ${TEST_VALUE}
error={anteError}
setError={setAnteError}
disabled={isSubmitting}
contractId={undefined}
/>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 KiB