Compare commits

..

1 Commits

Author SHA1 Message Date
James Grugett
16a7df4fec Show creator volume in user page 2022-07-19 16:44:41 -05:00
572 changed files with 17915 additions and 45775 deletions

View File

@ -1,43 +0,0 @@
name: Reformat main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
env:
FORCE_COLOR: 3
NEXT_TELEMETRY_DISABLED: 1
# mqp - i generated a personal token to use for these writes -- it's unclear
# why, but the default token didn't work, even when i gave it max permissions
jobs:
prettify:
name: Auto-prettify
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.FORMATTER_ACCESS_TOKEN }}
- name: Restore cached node_modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install missing dependencies
run: yarn install --prefer-offline --frozen-lockfile
- name: Run Prettier on web client
working-directory: web
run: yarn format
- name: Commit any Prettier changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Auto-prettification
branch: ${{ github.head_ref }}

View File

@ -1,43 +0,0 @@
name: Run linter (remove unused imports)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
env:
FORCE_COLOR: 3
NEXT_TELEMETRY_DISABLED: 1
# mqp - i generated a personal token to use for these writes -- it's unclear
# why, but the default token didn't work, even when i gave it max permissions
jobs:
lint:
name: Auto-lint
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.FORMATTER_ACCESS_TOKEN }}
- name: Restore cached node_modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install missing dependencies
run: yarn install --prefer-offline --frozen-lockfile
- name: Run lint script
run: yarn lint
- name: Commit any lint changes
if: always()
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Auto-remove unused imports
branch: ${{ github.head_ref }}

View File

@ -1,17 +0,0 @@
name: Merge main into main2 on every commit
on:
push:
branches:
- 'main'
jobs:
merge-branch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Merge main -> main2
uses: devmasx/merge-branch@master
with:
type: now
target_branch: main2
github_token: ${{ github.token }}

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
plugins: ['lodash', 'unused-imports'], plugins: ['lodash'],
extends: ['eslint:recommended'], extends: ['eslint:recommended'],
ignorePatterns: ['lib'], ignorePatterns: ['lib'],
env: { env: {
@ -26,7 +26,6 @@ module.exports = {
caughtErrorsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_',
}, },
], ],
'unused-imports/no-unused-imports': 'warn',
}, },
}, },
], ],

View File

@ -1,30 +1,33 @@
import { getCpmmLiquidity } from './calculate-cpmm' import { addCpmmLiquidity, getCpmmLiquidity } from './calculate-cpmm'
import { CPMMContract } from './contract' import { CPMMContract } from './contract'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { User } from './user'
export const getNewLiquidityProvision = ( export const getNewLiquidityProvision = (
userId: string, user: User,
amount: number, amount: number,
contract: CPMMContract, contract: CPMMContract,
newLiquidityProvisionId: string newLiquidityProvisionId: string
) => { ) => {
const { pool, p, totalLiquidity, subsidyPool } = contract const { pool, p, totalLiquidity } = contract
const liquidity = getCpmmLiquidity(pool, p) const { newPool, newP } = addCpmmLiquidity(pool, p, amount)
const liquidity =
getCpmmLiquidity(newPool, newP) - getCpmmLiquidity(pool, newP)
const newLiquidityProvision: LiquidityProvision = { const newLiquidityProvision: LiquidityProvision = {
id: newLiquidityProvisionId, id: newLiquidityProvisionId,
userId: userId, userId: user.id,
contractId: contract.id, contractId: contract.id,
amount, amount,
pool, pool: newPool,
p, p: newP,
liquidity, liquidity,
createdTime: Date.now(), createdTime: Date.now(),
} }
const newTotalLiquidity = (totalLiquidity ?? 0) + amount const newTotalLiquidity = (totalLiquidity ?? 0) + amount
const newSubsidyPool = (subsidyPool ?? 0) + amount
return { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } return { newLiquidityProvision, newPool, newP, newTotalLiquidity }
} }

View File

@ -5,22 +5,17 @@ import {
CPMMBinaryContract, CPMMBinaryContract,
DPMBinaryContract, DPMBinaryContract,
FreeResponseContract, FreeResponseContract,
MultipleChoiceContract,
NumericContract, NumericContract,
} from './contract' } from './contract'
import { User } from './user' import { User } from './user'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { noFees } from './fees' import { noFees } from './fees'
import { Answer } from './answer' import { ENV_CONFIG } from './envs/constants'
export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100
export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id
export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id
export const UNIQUE_BETTOR_LIQUIDITY_AMOUNT = 20
type NormalizedBet<T extends Bet = Bet> = Omit<
T,
'userAvatarUrl' | 'userName' | 'userUsername'
>
export function getCpmmInitialLiquidity( export function getCpmmInitialLiquidity(
providerId: string, providerId: string,
@ -57,7 +52,7 @@ export function getAnteBets(
const { createdTime } = contract const { createdTime } = contract
const yesBet: NormalizedBet = { const yesBet: Bet = {
id: yesAnteId, id: yesAnteId,
userId: creator.id, userId: creator.id,
contractId: contract.id, contractId: contract.id,
@ -71,7 +66,7 @@ export function getAnteBets(
fees: noFees, fees: noFees,
} }
const noBet: NormalizedBet = { const noBet: Bet = {
id: noAnteId, id: noAnteId,
userId: creator.id, userId: creator.id,
contractId: contract.id, contractId: contract.id,
@ -99,7 +94,7 @@ export function getFreeAnswerAnte(
const { createdTime } = contract const { createdTime } = contract
const anteBet: NormalizedBet = { const anteBet: Bet = {
id: anteBetId, id: anteBetId,
userId: anteBettorId, userId: anteBettorId,
contractId: contract.id, contractId: contract.id,
@ -116,50 +111,6 @@ export function getFreeAnswerAnte(
return anteBet return anteBet
} }
export function getMultipleChoiceAntes(
creator: User,
contract: MultipleChoiceContract,
answers: string[],
betDocIds: string[]
) {
const { totalBets, totalShares } = contract
const amount = totalBets['0']
const shares = totalShares['0']
const p = 1 / answers.length
const { createdTime } = contract
const bets: NormalizedBet[] = answers.map((answer, i) => ({
id: betDocIds[i],
userId: creator.id,
contractId: contract.id,
amount,
shares,
outcome: i.toString(),
probBefore: p,
probAfter: p,
createdTime,
isAnte: true,
fees: noFees,
}))
const { username, name, avatarUrl } = creator
const answerObjects: Answer[] = answers.map((answer, i) => ({
id: i.toString(),
number: i,
contractId: contract.id,
createdTime,
userId: creator.id,
username,
name,
avatarUrl,
text: answer,
}))
return { bets, answerObjects }
}
export function getNumericAnte( export function getNumericAnte(
anteBettorId: string, anteBettorId: string,
contract: NumericContract, contract: NumericContract,
@ -179,7 +130,7 @@ export function getNumericAnte(
range(0, bucketCount).map((_, i) => [i, betAnte]) range(0, bucketCount).map((_, i) => [i, betAnte])
) )
const anteBet: NormalizedBet<NumericBet> = { const anteBet: NumericBet = {
id: newBetId, id: newBetId,
userId: anteBettorId, userId: anteBettorId,
contractId: contract.id, contractId: contract.id,

View File

@ -12,9 +12,7 @@ export class APIError extends Error {
} }
export function getFunctionUrl(name: string) { export function getFunctionUrl(name: string) {
if (process.env.NEXT_PUBLIC_FUNCTIONS_URL) { if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
return `${process.env.NEXT_PUBLIC_FUNCTIONS_URL}/${name}`
} else if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
const { projectId, region } = ENV_CONFIG.firebaseConfig const { projectId, region } = ENV_CONFIG.firebaseConfig
return `http://localhost:5001/${projectId}/${region}/${name}` return `http://localhost:5001/${projectId}/${region}/${name}`
} else { } else {

View File

@ -1,123 +0,0 @@
import { User } from './user'
export type Badge = {
type: BadgeTypes
createdTime: number
data: { [key: string]: any }
name: 'Proven Correct' | 'Streaker' | 'Market Creator'
}
export type BadgeTypes = 'PROVEN_CORRECT' | 'STREAKER' | 'MARKET_CREATOR'
export type ProvenCorrectBadgeData = {
type: 'PROVEN_CORRECT'
data: {
contractSlug: string
contractCreatorUsername: string
contractTitle: string
commentId: string
betAmount: number
}
}
export type MarketCreatorBadgeData = {
type: 'MARKET_CREATOR'
data: {
totalContractsCreated: number
}
}
export type StreakerBadgeData = {
type: 'STREAKER'
data: {
totalBettingStreak: number
}
}
export type ProvenCorrectBadge = Badge & ProvenCorrectBadgeData
export type StreakerBadge = Badge & StreakerBadgeData
export type MarketCreatorBadge = Badge & MarketCreatorBadgeData
export const MINIMUM_UNIQUE_BETTORS_FOR_PROVEN_CORRECT_BADGE = 5
export const provenCorrectRarityThresholds = [1, 1000, 10000]
const calculateProvenCorrectBadgeRarity = (badge: ProvenCorrectBadge) => {
const { betAmount } = badge.data
const thresholdArray = provenCorrectRarityThresholds
let i = thresholdArray.length - 1
while (i >= 0) {
if (betAmount >= thresholdArray[i]) {
return i + 1
}
i--
}
return 1
}
export const streakerBadgeRarityThresholds = [1, 50, 250]
const calculateStreakerBadgeRarity = (badge: StreakerBadge) => {
const { totalBettingStreak } = badge.data
const thresholdArray = streakerBadgeRarityThresholds
let i = thresholdArray.length - 1
while (i >= 0) {
if (totalBettingStreak == thresholdArray[i]) {
return i + 1
}
i--
}
return 1
}
export const marketCreatorBadgeRarityThresholds = [1, 75, 300]
const calculateMarketCreatorBadgeRarity = (badge: MarketCreatorBadge) => {
const { totalContractsCreated } = badge.data
const thresholdArray = marketCreatorBadgeRarityThresholds
let i = thresholdArray.length - 1
while (i >= 0) {
if (totalContractsCreated == thresholdArray[i]) {
return i + 1
}
i--
}
return 1
}
export type rarities = 'bronze' | 'silver' | 'gold'
const rarityRanks: { [key: number]: rarities } = {
1: 'bronze',
2: 'silver',
3: 'gold',
}
export const calculateBadgeRarity = (badge: Badge) => {
switch (badge.type) {
case 'PROVEN_CORRECT':
return rarityRanks[
calculateProvenCorrectBadgeRarity(badge as ProvenCorrectBadge)
]
case 'MARKET_CREATOR':
return rarityRanks[
calculateMarketCreatorBadgeRarity(badge as MarketCreatorBadge)
]
case 'STREAKER':
return rarityRanks[calculateStreakerBadgeRarity(badge as StreakerBadge)]
default:
return rarityRanks[0]
}
}
export const getBadgesByRarity = (user: User | null | undefined) => {
const rarities: { [key in rarities]: number } = {
bronze: 0,
silver: 0,
gold: 0,
}
if (!user) return rarities
Object.values(user.achievements).map((value) => {
value.badges.map((badge) => {
rarities[calculateBadgeRarity(badge)] =
(rarities[calculateBadgeRarity(badge)] ?? 0) + 1
})
})
return rarities
}

View File

@ -3,12 +3,6 @@ import { Fees } from './fees'
export type Bet = { export type Bet = {
id: string id: string
userId: string userId: string
// denormalized for bet lists
userAvatarUrl?: string
userUsername: string
userName: string
contractId: string contractId: string
createdTime: number createdTime: number
@ -20,21 +14,18 @@ export type Bet = {
probBefore: number probBefore: number
probAfter: number probAfter: number
sale?: {
amount: number // amount user makes from sale
betId: string // id of bet being sold
// TODO: add sale time?
}
fees: Fees fees: Fees
isSold?: boolean // true if this BUY bet has been sold
isAnte?: boolean isAnte?: boolean
isLiquidityProvision?: boolean isLiquidityProvision?: boolean
isRedemption?: boolean isRedemption?: boolean
challengeSlug?: string
// Props for bets in DPM contract below.
// A bet is either a BUY or a SELL that sells all of a previous buy.
isSold?: boolean // true if this BUY bet has been sold
// This field marks a SELL bet.
sale?: {
amount: number // amount user makes from sale
betId: string // id of BUY bet being sold
}
} & Partial<LimitProps> } & Partial<LimitProps>
export type NumericBet = Bet & { export type NumericBet = Bet & {
@ -67,3 +58,5 @@ export type fill = {
// I.e. -fill.shares === matchedBet.shares // I.e. -fill.shares === matchedBet.shares
isSale?: boolean isSale?: boolean
} }
export const MAX_LOAN_PER_CONTRACT = 20

View File

@ -1,10 +1,11 @@
import { groupBy, mapValues, sumBy } from 'lodash' import { sum, groupBy, mapValues, sumBy } from 'lodash'
import { LimitBet } from './bet' import { LimitBet } from './bet'
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees' import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { computeFills } from './new-bet' import { computeFills } from './new-bet'
import { binarySearch } from './util/algos' import { binarySearch } from './util/algos'
import { addObjects } from './util/object'
export type CpmmState = { export type CpmmState = {
pool: { [outcome: string]: number } pool: { [outcome: string]: number }
@ -122,7 +123,6 @@ export function calculateCpmmAmountToProb(
prob: number, prob: number,
outcome: 'YES' | 'NO' outcome: 'YES' | 'NO'
) { ) {
if (prob <= 0 || prob >= 1 || isNaN(prob)) return Infinity
if (outcome === 'NO') prob = 1 - prob if (outcome === 'NO') prob = 1 - prob
// First, find an upper bound that leads to a more extreme probability than prob. // First, find an upper bound that leads to a more extreme probability than prob.
@ -146,8 +146,7 @@ function calculateAmountToBuyShares(
state: CpmmState, state: CpmmState,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[], unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
) { ) {
// Search for amount between bounds (0, shares). // Search for amount between bounds (0, shares).
// Min share price is M$0, and max is M$1 each. // Min share price is M$0, and max is M$1 each.
@ -157,8 +156,7 @@ function calculateAmountToBuyShares(
amount, amount,
state, state,
undefined, undefined,
unfilledBets, unfilledBets
balanceByUserId
) )
const totalShares = sumBy(takers, (taker) => taker.shares) const totalShares = sumBy(takers, (taker) => taker.shares)
@ -170,8 +168,7 @@ export function calculateCpmmSale(
state: CpmmState, state: CpmmState,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[], unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
) { ) {
if (Math.round(shares) < 0) { if (Math.round(shares) < 0) {
throw new Error('Cannot sell non-positive shares') throw new Error('Cannot sell non-positive shares')
@ -182,17 +179,15 @@ export function calculateCpmmSale(
state, state,
shares, shares,
oppositeOutcome, oppositeOutcome,
unfilledBets, unfilledBets
balanceByUserId
) )
const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills( const { cpmmState, makers, takers, totalFees } = computeFills(
oppositeOutcome, oppositeOutcome,
buyAmount, buyAmount,
state, state,
undefined, undefined,
unfilledBets, unfilledBets
balanceByUserId
) )
// Transform buys of opposite outcome into sells. // Transform buys of opposite outcome into sells.
@ -215,7 +210,6 @@ export function calculateCpmmSale(
fees: totalFees, fees: totalFees,
makers, makers,
takers: saleTakers, takers: saleTakers,
ordersToCancel,
} }
} }
@ -223,16 +217,9 @@ export function getCpmmProbabilityAfterSale(
state: CpmmState, state: CpmmState,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[], unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
) { ) {
const { cpmmState } = calculateCpmmSale( const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets)
state,
shares,
outcome,
unfilledBets,
balanceByUserId
)
return getCpmmProbability(cpmmState.pool, cpmmState.p) return getCpmmProbability(cpmmState.pool, cpmmState.p)
} }
@ -266,22 +253,48 @@ export function addCpmmLiquidity(
return { newPool, liquidity, newP } return { newPool, liquidity, newP }
} }
export function getCpmmLiquidityPoolWeights(liquidities: LiquidityProvision[]) { const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => {
const userAmounts = groupBy(liquidities, (w) => w.userId) const oldLiquidity = getCpmmLiquidity(l.pool, p)
const totalAmount = sumBy(liquidities, (w) => w.amount)
return mapValues( const newPool = addObjects(l.pool, { YES: l.amount, NO: l.amount })
userAmounts, const newLiquidity = getCpmmLiquidity(newPool, p)
(amounts) => sumBy(amounts, (w) => w.amount) / totalAmount
const liquidity = newLiquidity - oldLiquidity
return liquidity
}
export function getCpmmLiquidityPoolWeights(
state: CpmmState,
liquidities: LiquidityProvision[],
excludeAntes: boolean
) {
const calcLiqudity = calculateLiquidityDelta(state.p)
const liquidityShares = liquidities.map(calcLiqudity)
const shareSum = sum(liquidityShares)
const weights = liquidityShares.map((shares, i) => ({
weight: shares / shareSum,
providerId: liquidities[i].userId,
}))
const includedWeights = excludeAntes
? weights.filter((_, i) => !liquidities[i].isAnte)
: weights
const userWeights = groupBy(includedWeights, (w) => w.providerId)
const totalUserWeights = mapValues(userWeights, (userWeight) =>
sumBy(userWeight, (w) => w.weight)
) )
return totalUserWeights
} }
export function getUserLiquidityShares( export function getUserLiquidityShares(
userId: string, userId: string,
state: CpmmState, state: CpmmState,
liquidities: LiquidityProvision[] liquidities: LiquidityProvision[],
excludeAntes: boolean
) { ) {
const weights = getCpmmLiquidityPoolWeights(liquidities) const weights = getCpmmLiquidityPoolWeights(state, liquidities, excludeAntes)
const userWeight = weights[userId] ?? 0 const userWeight = weights[userId] ?? 0
return mapValues(state.pool, (shares) => userWeight * shares) return mapValues(state.pool, (shares) => userWeight * shares)

View File

@ -1,315 +0,0 @@
import { Dictionary, groupBy, last, partition, sum, sumBy, uniq } from 'lodash'
import { calculatePayout, getContractBetMetrics } from './calculate'
import { Bet, LimitBet } from './bet'
import {
Contract,
CPMMBinaryContract,
CPMMContract,
DPMContract,
} from './contract'
import { PortfolioMetrics, User } from './user'
import { DAY_MS } from './util/time'
import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet'
import { getCpmmProbability } from './calculate-cpmm'
import { removeUndefinedProps } from './util/object'
const computeInvestmentValue = (
bets: Bet[],
contractsDict: { [k: string]: Contract }
) => {
return sumBy(bets, (bet) => {
const contract = contractsDict[bet.contractId]
if (!contract || contract.isResolved) return 0
if (bet.sale || bet.isSold) return 0
const payout = calculatePayout(contract, bet, 'MKT')
const value = payout - (bet.loanAmount ?? 0)
if (isNaN(value)) return 0
return value
})
}
export const computeInvestmentValueCustomProb = (
bets: Bet[],
contract: Contract,
p: number
) => {
return sumBy(bets, (bet) => {
if (!contract || contract.isResolved) return 0
if (bet.sale || bet.isSold) return 0
const { outcome, shares } = bet
const betP = outcome === 'YES' ? p : 1 - p
const value = betP * shares
if (isNaN(value)) return 0
return value
})
}
export const computeElasticity = (
bets: Bet[],
contract: Contract,
betAmount = 50
) => {
const { mechanism, outcomeType } = contract
return mechanism === 'cpmm-1' &&
(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC')
? computeBinaryCpmmElasticity(bets, contract, betAmount)
: computeDpmElasticity(contract, betAmount)
}
export const computeBinaryCpmmElasticity = (
bets: Bet[],
contract: CPMMContract,
betAmount: number
) => {
const limitBets = bets
.filter(
(b) =>
!b.isFilled &&
!b.isSold &&
!b.isRedemption &&
!b.sale &&
!b.isCancelled &&
b.limitProb !== undefined
)
.sort((a, b) => a.createdTime - b.createdTime) as LimitBet[]
const userIds = uniq(limitBets.map((b) => b.userId))
// Assume all limit orders are good.
const userBalances = Object.fromEntries(
userIds.map((id) => [id, Number.MAX_SAFE_INTEGER])
)
const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo(
'YES',
betAmount,
contract,
undefined,
limitBets,
userBalances
)
const resultYes = getCpmmProbability(poolY, pY)
const { newPool: poolN, newP: pN } = getBinaryCpmmBetInfo(
'NO',
betAmount,
contract,
undefined,
limitBets,
userBalances
)
const resultNo = getCpmmProbability(poolN, pN)
// handle AMM overflow
const safeYes = Number.isFinite(resultYes) ? resultYes : 1
const safeNo = Number.isFinite(resultNo) ? resultNo : 0
return safeYes - safeNo
}
export const computeDpmElasticity = (
contract: DPMContract,
betAmount: number
) => {
return getNewMultiBetInfo('', 2 * betAmount, contract).newBet.probAfter
}
const computeTotalPool = (userContracts: Contract[], startTime = 0) => {
const periodFilteredContracts = userContracts.filter(
(contract) => contract.createdTime >= startTime
)
return sum(
periodFilteredContracts.map((contract) => sum(Object.values(contract.pool)))
)
}
export const computeVolume = (contractBets: Bet[], since: number) => {
return sumBy(contractBets, (b) =>
b.createdTime > since && !b.isRedemption ? Math.abs(b.amount) : 0
)
}
const calculateProbChangeSince = (descendingBets: Bet[], since: number) => {
const newestBet = descendingBets[0]
if (!newestBet) return 0
const betBeforeSince = descendingBets.find((b) => b.createdTime < since)
if (!betBeforeSince) {
const oldestBet = last(descendingBets) ?? newestBet
return newestBet.probAfter - oldestBet.probBefore
}
return newestBet.probAfter - betBeforeSince.probAfter
}
export const calculateProbChanges = (descendingBets: Bet[]) => {
const now = Date.now()
const yesterday = now - DAY_MS
const weekAgo = now - 7 * DAY_MS
const monthAgo = now - 30 * DAY_MS
return {
day: calculateProbChangeSince(descendingBets, yesterday),
week: calculateProbChangeSince(descendingBets, weekAgo),
month: calculateProbChangeSince(descendingBets, monthAgo),
}
}
export const calculateCreatorVolume = (userContracts: Contract[]) => {
const allTimeCreatorVolume = computeTotalPool(userContracts, 0)
const monthlyCreatorVolume = computeTotalPool(
userContracts,
Date.now() - 30 * DAY_MS
)
const weeklyCreatorVolume = computeTotalPool(
userContracts,
Date.now() - 7 * DAY_MS
)
const dailyCreatorVolume = computeTotalPool(
userContracts,
Date.now() - 1 * DAY_MS
)
return {
daily: dailyCreatorVolume,
weekly: weeklyCreatorVolume,
monthly: monthlyCreatorVolume,
allTime: allTimeCreatorVolume,
}
}
export const calculateNewPortfolioMetrics = (
user: User,
contractsById: { [k: string]: Contract },
currentBets: Bet[]
) => {
const investmentValue = computeInvestmentValue(currentBets, contractsById)
const newPortfolio = {
investmentValue: investmentValue,
balance: user.balance,
totalDeposits: user.totalDeposits,
timestamp: Date.now(),
userId: user.id,
}
return newPortfolio
}
const calculateProfitForPeriod = (
startingPortfolio: PortfolioMetrics | undefined,
currentProfit: number
) => {
if (startingPortfolio === undefined) {
return currentProfit
}
const startingProfit = calculatePortfolioProfit(startingPortfolio)
return currentProfit - startingProfit
}
export const calculatePortfolioProfit = (portfolio: PortfolioMetrics) => {
return portfolio.investmentValue + portfolio.balance - portfolio.totalDeposits
}
export const calculateNewProfit = (
portfolioHistory: Record<
'current' | 'day' | 'week' | 'month',
PortfolioMetrics | undefined
>,
newPortfolio: PortfolioMetrics
) => {
const allTimeProfit = calculatePortfolioProfit(newPortfolio)
const newProfit = {
daily: calculateProfitForPeriod(portfolioHistory.day, allTimeProfit),
weekly: calculateProfitForPeriod(portfolioHistory.week, allTimeProfit),
monthly: calculateProfitForPeriod(portfolioHistory.month, allTimeProfit),
allTime: allTimeProfit,
}
return newProfit
}
export const calculateMetricsByContract = (
bets: Bet[],
contractsById: Dictionary<Contract>
) => {
const betsByContract = groupBy(bets, (bet) => bet.contractId)
const unresolvedContracts = Object.keys(betsByContract)
.map((cid) => contractsById[cid])
.filter((c) => c && !c.isResolved)
return unresolvedContracts.map((c) => {
const bets = betsByContract[c.id] ?? []
const current = getContractBetMetrics(c, bets)
let periodMetrics
if (c.mechanism === 'cpmm-1' && c.outcomeType === 'BINARY') {
const periods = ['day', 'week', 'month'] as const
periodMetrics = Object.fromEntries(
periods.map((period) => [
period,
calculatePeriodProfit(c, bets, period),
])
)
}
return removeUndefinedProps({
contractId: c.id,
...current,
from: periodMetrics,
})
})
}
export type ContractMetrics = ReturnType<
typeof calculateMetricsByContract
>[number]
const calculatePeriodProfit = (
contract: CPMMBinaryContract,
bets: Bet[],
period: 'day' | 'week' | 'month'
) => {
const days = period === 'day' ? 1 : period === 'week' ? 7 : 30
const fromTime = Date.now() - days * DAY_MS
const [previousBets, recentBets] = partition(
bets,
(b) => b.createdTime < fromTime
)
const prevProb = contract.prob - contract.probChanges[period]
const prob = contract.resolutionProbability
? contract.resolutionProbability
: contract.prob
const previousBetsValue = computeInvestmentValueCustomProb(
previousBets,
contract,
prevProb
)
const currentBetsValue = computeInvestmentValueCustomProb(
previousBets,
contract,
prob
)
const { profit: recentProfit, invested: recentInvested } =
getContractBetMetrics(contract, recentBets)
const profit = currentBetsValue - previousBetsValue + recentProfit
const invested = previousBetsValue + recentInvested
const profitPercent = invested === 0 ? 0 : 100 * (profit / invested)
return {
profit,
profitPercent,
invested,
prevValue: previousBetsValue,
value: currentBetsValue,
}
}

View File

@ -1,4 +1,4 @@
import { maxBy, partition, sortBy, sum, sumBy } from 'lodash' import { maxBy } from 'lodash'
import { Bet, LimitBet } from './bet' import { Bet, LimitBet } from './bet'
import { import {
calculateCpmmSale, calculateCpmmSale,
@ -23,7 +23,6 @@ import {
BinaryContract, BinaryContract,
FreeResponseContract, FreeResponseContract,
PseudoNumericContract, PseudoNumericContract,
MultipleChoiceContract,
} from './contract' } from './contract'
import { floatingEqual } from './util/math' import { floatingEqual } from './util/math'
@ -78,8 +77,7 @@ export function calculateShares(
export function calculateSaleAmount( export function calculateSaleAmount(
contract: Contract, contract: Contract,
bet: Bet, bet: Bet,
unfilledBets: LimitBet[], unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
) { ) {
return contract.mechanism === 'cpmm-1' && return contract.mechanism === 'cpmm-1' &&
(contract.outcomeType === 'BINARY' || (contract.outcomeType === 'BINARY' ||
@ -88,8 +86,7 @@ export function calculateSaleAmount(
contract, contract,
Math.abs(bet.shares), Math.abs(bet.shares),
bet.outcome as 'YES' | 'NO', bet.outcome as 'YES' | 'NO',
unfilledBets, unfilledBets
balanceByUserId
).saleValue ).saleValue
: calculateDpmSaleAmount(contract, bet) : calculateDpmSaleAmount(contract, bet)
} }
@ -104,16 +101,14 @@ export function getProbabilityAfterSale(
contract: Contract, contract: Contract,
outcome: string, outcome: string,
shares: number, shares: number,
unfilledBets: LimitBet[], unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
) { ) {
return contract.mechanism === 'cpmm-1' return contract.mechanism === 'cpmm-1'
? getCpmmProbabilityAfterSale( ? getCpmmProbabilityAfterSale(
contract, contract,
shares, shares,
outcome as 'YES' | 'NO', outcome as 'YES' | 'NO',
unfilledBets, unfilledBets
balanceByUserId
) )
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares) : getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
} }
@ -137,53 +132,10 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
: calculateDpmPayout(contract, bet, outcome) : calculateDpmPayout(contract, bet, outcome)
} }
function getCpmmInvested(yourBets: Bet[]) {
const totalShares: { [outcome: string]: number } = {}
const totalSpent: { [outcome: string]: number } = {}
const sortedBets = sortBy(yourBets, 'createdTime')
for (const bet of sortedBets) {
const { outcome, shares, amount } = bet
if (floatingEqual(shares, 0)) continue
const spent = totalSpent[outcome] ?? 0
const position = totalShares[outcome] ?? 0
if (amount > 0) {
totalShares[outcome] = position + shares
totalSpent[outcome] = spent + amount
} else if (amount < 0) {
const averagePrice = position === 0 ? 0 : spent / position
totalShares[outcome] = position + shares
totalSpent[outcome] = spent + averagePrice * shares
}
}
return sum([0, ...Object.values(totalSpent)])
}
function getDpmInvested(yourBets: Bet[]) {
const sortedBets = sortBy(yourBets, 'createdTime')
return sumBy(sortedBets, (bet) => {
const { amount, sale } = bet
if (sale) {
const originalBet = sortedBets.find((b) => b.id === sale.betId)
if (originalBet) return -originalBet.amount
return 0
}
return amount
})
}
export type ContractBetMetrics = ReturnType<typeof getContractBetMetrics>
export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) { export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
const { resolution } = contract const { resolution } = contract
const isCpmm = contract.mechanism === 'cpmm-1'
let currentInvested = 0
let totalInvested = 0 let totalInvested = 0
let payout = 0 let payout = 0
let loan = 0 let loan = 0
@ -209,6 +161,7 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
saleValue -= amount saleValue -= amount
} }
currentInvested += amount
loan += loanAmount ?? 0 loan += loanAmount ?? 0
payout += resolution payout += resolution
? calculatePayout(contract, bet, resolution) ? calculatePayout(contract, bet, resolution)
@ -216,18 +169,18 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
} }
} }
const netPayout = payout - loan
const profit = payout + saleValue + redeemed - totalInvested const profit = payout + saleValue + redeemed - totalInvested
const profitPercent = totalInvested === 0 ? 0 : (profit / totalInvested) * 100 const profitPercent = (profit / totalInvested) * 100
const invested = isCpmm ? getCpmmInvested(yourBets) : getDpmInvested(yourBets)
const hasShares = Object.values(totalShares).some( const hasShares = Object.values(totalShares).some(
(shares) => !floatingEqual(shares, 0) (shares) => !floatingEqual(shares, 0)
) )
return { return {
invested, invested: Math.max(0, currentInvested),
loan,
payout, payout,
netPayout,
profit, profit,
profitPercent, profitPercent,
totalShares, totalShares,
@ -238,8 +191,8 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
export function getContractBetNullMetrics() { export function getContractBetNullMetrics() {
return { return {
invested: 0, invested: 0,
loan: 0,
payout: 0, payout: 0,
netPayout: 0,
profit: 0, profit: 0,
profitPercent: 0, profitPercent: 0,
totalShares: {} as { [outcome: string]: number }, totalShares: {} as { [outcome: string]: number },
@ -247,9 +200,7 @@ export function getContractBetNullMetrics() {
} }
} }
export function getTopAnswer( export function getTopAnswer(contract: FreeResponseContract) {
contract: FreeResponseContract | MultipleChoiceContract
) {
const { answers } = contract const { answers } = contract
const top = maxBy( const top = maxBy(
answers?.map((answer) => ({ answers?.map((answer) => ({
@ -260,43 +211,3 @@ export function getTopAnswer(
) )
return top?.answer return top?.answer
} }
export function getLargestPosition(contract: Contract, userBets: Bet[]) {
let yesFloorShares = 0,
yesShares = 0,
noShares = 0,
noFloorShares = 0
if (userBets.length === 0) {
return null
}
if (contract.outcomeType === 'FREE_RESPONSE') {
const answerCounts: { [outcome: string]: number } = {}
for (const bet of userBets) {
if (bet.outcome) {
if (!answerCounts[bet.outcome]) {
answerCounts[bet.outcome] = bet.amount
} else {
answerCounts[bet.outcome] += bet.amount
}
}
}
const majorityAnswer =
maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? ''
return {
prob: undefined,
shares: answerCounts[majorityAnswer] || 0,
outcome: majorityAnswer,
}
}
const [yesBets, noBets] = partition(userBets, (bet) => bet.outcome === 'YES')
yesShares = sumBy(yesBets, (bet) => bet.shares)
noShares = sumBy(noBets, (bet) => bet.shares)
yesFloorShares = Math.floor(yesShares)
noFloorShares = Math.floor(noShares)
const shares = yesFloorShares || noFloorShares
const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO'
return { shares, outcome }
}

View File

@ -1,7 +1,6 @@
import { difference } from 'lodash' import { difference } from 'lodash'
export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default' export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default'
export const CATEGORIES = { export const CATEGORIES = {
politics: 'Politics', politics: 'Politics',
technology: 'Technology', technology: 'Technology',
@ -31,13 +30,10 @@ export const EXCLUDED_CATEGORIES: category[] = [
'manifold', 'manifold',
'personal', 'personal',
'covid', 'covid',
'culture',
'gaming', 'gaming',
'crypto', 'crypto',
'world',
] ]
export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES) export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES)
export const DEFAULT_CATEGORY_GROUPS = DEFAULT_CATEGORIES.map((c) => ({
slug: c.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX,
name: CATEGORIES[c as category],
}))

View File

@ -1,65 +0,0 @@
import { IS_PRIVATE_MANIFOLD } from './envs/constants'
export type Challenge = {
// The link to send: https://manifold.markets/challenges/username/market-slug/{slug}
// Also functions as the unique id for the link.
slug: string
// The user that created the challenge.
creatorId: string
creatorUsername: string
creatorName: string
creatorAvatarUrl?: string
// Displayed to people claiming the challenge
message: string
// How much to put up
creatorAmount: number
// YES or NO for now
creatorOutcome: string
// Different than the creator
acceptorOutcome: string
acceptorAmount: number
// The probability the challenger thinks
creatorOutcomeProb: number
contractId: string
contractSlug: string
contractQuestion: string
contractCreatorUsername: string
createdTime: number
// If null, the link is valid forever
expiresTime: number | null
// How many times the challenge can be used
maxUses: number
// Used for simpler caching
acceptedByUserIds: string[]
// Successful redemptions of the link
acceptances: Acceptance[]
// TODO: will have to fill this on resolve contract
isResolved: boolean
resolutionOutcome?: string
}
export type Acceptance = {
// User that accepted the challenge
userId: string
userUsername: string
userName: string
userAvatarUrl: string
// The ID of the successful bet that tracks the money moved
betId: string
createdTime: number
}
export const CHALLENGES_ENABLED = !IS_PRIVATE_MANIFOLD

View File

@ -169,7 +169,7 @@ export const charities: Charity[] = [
{ {
name: "Founder's Pledge Climate Change Fund", name: "Founder's Pledge Climate Change Fund",
website: 'https://founderspledge.com/funds/climate-change-fund', website: 'https://founderspledge.com/funds/climate-change-fund',
photo: 'https://i.imgur.com/9turaJW.png', photo: 'https://i.imgur.com/ZAhzHu4.png',
preview: preview:
'The Climate Change Fund aims to sustainably reach net-zero emissions globally, while still allowing growth to free millions from energy poverty.', 'The Climate Change Fund aims to sustainably reach net-zero emissions globally, while still allowing growth to free millions from energy poverty.',
description: `The Climate Change Fund aims to sustainably reach net-zero emissions globally. description: `The Climate Change Fund aims to sustainably reach net-zero emissions globally.
@ -183,7 +183,7 @@ export const charities: Charity[] = [
{ {
name: "Founder's Pledge Patient Philanthropy Fund", name: "Founder's Pledge Patient Philanthropy Fund",
website: 'https://founderspledge.com/funds/patient-philanthropy-fund', website: 'https://founderspledge.com/funds/patient-philanthropy-fund',
photo: 'https://i.imgur.com/LLR6CI6.png', photo: 'https://i.imgur.com/ZAhzHu4.png',
preview: preview:
'The Patient Philanthropy Project aims to safeguard and benefit the long-term future of humanity', 'The Patient Philanthropy Project aims to safeguard and benefit the long-term future of humanity',
description: `The Patient Philanthropy Project focuses on how we can collectively grow our resources to support the long-term flourishing of humanity. It addresses a crucial gap: as a society, we spend much too little on safeguarding and benefiting future generations. In fact, we spend more money on ice cream each year than we do on preventing our own extinction. However, people in the future - who do not have a voice in their future survival or environment - matter. Lots of them may yet come into existence and we have the ability to positively affect their lives now, if only by making sure we avoid major catastrophes that could destroy our common future. description: `The Patient Philanthropy Project focuses on how we can collectively grow our resources to support the long-term flourishing of humanity. It addresses a crucial gap: as a society, we spend much too little on safeguarding and benefiting future generations. In fact, we spend more money on ice cream each year than we do on preventing our own extinction. However, people in the future - who do not have a voice in their future survival or environment - matter. Lots of them may yet come into existence and we have the ability to positively affect their lives now, if only by making sure we avoid major catastrophes that could destroy our common future.
@ -551,53 +551,6 @@ With an emphasis on approval voting, we bring better elections to people across
The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that dont represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`, The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that dont represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`,
}, },
{
name: 'Founders Pledge Global Health and Development Fund',
website: 'https://founderspledge.com/funds/global-health-and-development',
photo: 'https://i.imgur.com/EXbxH7T.png',
preview:
'Tackling the vast global inequalities in health, wealth and opportunity',
description: `Nearly half the world lives on less than $2.50 a day, yet giving by the worlds richest often overlooks the worlds poorest and most vulnerable. Despite the average American household being richer than 90% of the rest of the world, only 6% of US charitable giving goes to charities which work internationally.
This Fund is focused on helping those who need it most, wherever that help can make the biggest difference. By building a mixed portfolio of direct and indirect interventions, such as policy work, we aim to:
Improve the lives of the world's most vulnerable people.
Reduce the number of easily preventable deaths worldwide.
Work towards sustainable, systemic change.`,
},
{
name: 'YIMBY Law',
website: 'https://www.yimbylaw.org/',
photo: 'https://i.imgur.com/zlzp21Z.png',
preview:
'YIMBY Law works to make housing in California more accessible and affordable, by enforcing state housing laws.',
description: `
YIMBY Law works to make housing in California more accessible and affordable. Our method is to enforce state housing laws, and some examples are outlined below. We send letters to cities considering zoning or general plan compliant housing developments informing them of their duties under state law, and sue them when they do not comply.
If you would like to support our work, you can do so by getting involved or by donating.`,
},
{
name: 'CaRLA',
website: 'https://carlaef.org/',
photo: 'https://i.imgur.com/IsNVTOY.png',
preview:
'The California Renters Legal Advocacy and Education Funds core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.',
description: `
The California Renters Legal Advocacy and Education Funds core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.
CaRLA uses legal advocacy and education to ensure all cities comply with their own zoning and state housing laws and do their part to help solve the states housing shortage.
In addition to housing impact litigation, we provide free legal aid, education and workshops, counseling and advocacy to advocates, homeowners, small developers, and city and state government officials.`,
},
{
name: 'Mriya',
website: 'https://mriya-ua.org/',
photo:
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2Fdefault%2Fci2h3hStFM.47?alt=media&token=0d2cdc3d-e4d8-4f5e-8f23-4a586b6ff637',
preview: 'Donate supplies to soldiers in Ukraine',
description:
'Donate supplies to soldiers in Ukraine, including tourniquets and plate carriers.',
},
].map((charity) => { ].map((charity) => {
const slug = charity.name.toLowerCase().replace(/\s/g, '-') const slug = charity.name.toLowerCase().replace(/\s/g, '-')
return { return {

View File

@ -1,56 +1,19 @@
import type { JSONContent } from '@tiptap/core'
export type AnyCommentType = OnContract | OnGroup | OnPost
// Currently, comments are created after the bet, not atomically with the bet. // Currently, comments are created after the bet, not atomically with the bet.
// They're uniquely identified by the pair contractId/betId. // They're uniquely identified by the pair contractId/betId.
export type Comment<T extends AnyCommentType = AnyCommentType> = { export type Comment = {
id: string id: string
contractId?: string
groupId?: string
betId?: string
answerOutcome?: string
replyToCommentId?: string replyToCommentId?: string
userId: string userId: string
/** @deprecated - content now stored as JSON in content*/ text: string
text?: string
content: JSONContent
createdTime: number createdTime: number
// Denormalized, for rendering comments // Denormalized, for rendering comments
userName: string userName: string
userUsername: string userUsername: string
userAvatarUrl?: string userAvatarUrl?: string
bountiesAwarded?: number
} & T
export type OnContract = {
commentType: 'contract'
contractId: string
answerOutcome?: string
betId?: string
// denormalized from contract
contractSlug: string
contractQuestion: string
// denormalized from bet
betAmount?: number
betOutcome?: string
// denormalized based on betting history
commenterPositionProb?: number // binary only
commenterPositionShares?: number
commenterPositionOutcome?: string
} }
export type OnGroup = {
commentType: 'group'
groupId: string
}
export type OnPost = {
commentType: 'post'
postId: string
}
export type ContractComment = Comment<OnContract>
export type GroupComment = Comment<OnGroup>
export type PostComment = Comment<OnPost>

View File

@ -1,168 +0,0 @@
import { Challenge } from './challenge'
import { BinaryContract, Contract } from './contract'
import { getFormattedMappedValue } from './pseudo-numeric'
import { getProbability } from './calculate'
import { richTextToString } from './util/parse'
import { getCpmmProbability } from './calculate-cpmm'
import { getDpmProbability } from './calculate-dpm'
import { formatMoney, formatPercent } from './util/format'
export function contractMetrics(contract: Contract) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const dayjs = require('dayjs')
const { createdTime, resolutionTime, isResolved } = contract
const createdDate = dayjs(createdTime).format('MMM D')
const resolvedDate = isResolved
? dayjs(resolutionTime).format('MMM D')
: undefined
const volumeLabel = `${formatMoney(contract.volume)} bet`
return { volumeLabel, createdDate, resolvedDate }
}
// String version of the above, to send to the OpenGraph image generator
export function contractTextDetails(contract: Contract) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const dayjs = require('dayjs')
const { closeTime, groupLinks } = contract
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
const groupHashtags = groupLinks?.map((g) => `#${g.name.replace(/ /g, '')}`)
return (
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
(closeTime
? `${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
closeTime
).format('MMM D, h:mma')}`
: '') +
`${volumeLabel}` +
(groupHashtags ? `${groupHashtags.join(' ')}` : '')
)
}
export function getBinaryProb(contract: BinaryContract) {
const { pool, resolutionProbability, mechanism } = contract
return (
resolutionProbability ??
(mechanism === 'cpmm-1'
? getCpmmProbability(pool, contract.p)
: getDpmProbability(contract.totalShares))
)
}
export const getOpenGraphProps = (contract: Contract) => {
const {
resolution,
question,
creatorName,
creatorUsername,
outcomeType,
creatorAvatarUrl,
description: desc,
} = contract
const probPercent =
outcomeType === 'BINARY'
? formatPercent(getBinaryProb(contract))
: undefined
const numericValue =
outcomeType === 'PSEUDO_NUMERIC'
? getFormattedMappedValue(contract)(getProbability(contract))
: undefined
const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc)
const description = resolution
? `Resolved ${resolution}. ${stringDesc}`
: probPercent
? `${probPercent} chance. ${stringDesc}`
: stringDesc
return {
question,
probability: probPercent,
metadata: contractTextDetails(contract),
creatorName,
creatorUsername,
creatorAvatarUrl,
description,
numericValue,
resolution,
}
}
export type OgCardProps = {
question: string
probability?: string
metadata: string
creatorName: string
creatorUsername: string
creatorAvatarUrl?: string
numericValue?: string
resolution?: string
}
export function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
const {
creatorAmount,
acceptances,
acceptorAmount,
creatorOutcome,
acceptorOutcome,
} = challenge || {}
const {
probability,
numericValue,
resolution,
creatorAvatarUrl,
question,
metadata,
creatorUsername,
creatorName,
} = props
const { userName, userAvatarUrl } = acceptances?.[0] ?? {}
const probabilityParam =
probability === undefined
? ''
: `&probability=${encodeURIComponent(probability ?? '')}`
const numericValueParam =
numericValue === undefined
? ''
: `&numericValue=${encodeURIComponent(numericValue ?? '')}`
const creatorAvatarUrlParam =
creatorAvatarUrl === undefined
? ''
: `&creatorAvatarUrl=${encodeURIComponent(creatorAvatarUrl ?? '')}`
const challengeUrlParams = challenge
? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` +
`&challengerAmount=${acceptorAmount}&challengerOutcome=${acceptorOutcome}` +
`&acceptedName=${userName ?? ''}&acceptedAvatarUrl=${userAvatarUrl ?? ''}`
: ''
const resolutionUrlParam = resolution
? `&resolution=${encodeURIComponent(resolution)}`
: ''
// URL encode each of the props, then add them as query params
return (
`https://manifold-og-image.vercel.app/m.png` +
`?question=${encodeURIComponent(question)}` +
probabilityParam +
numericValueParam +
`&metadata=${encodeURIComponent(metadata)}` +
`&creatorName=${encodeURIComponent(creatorName)}` +
creatorAvatarUrlParam +
`&creatorUsername=${encodeURIComponent(creatorUsername)}` +
challengeUrlParams +
resolutionUrlParam
)
}

View File

@ -1,23 +1,15 @@
import { Answer } from './answer' import { Answer } from './answer'
import { Fees } from './fees' import { Fees } from './fees'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
import { GroupLink } from 'common/group'
export type AnyMechanism = DPM | CPMM export type AnyMechanism = DPM | CPMM
export type AnyOutcomeType = export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
| Binary
| MultipleChoice
| PseudoNumeric
| FreeResponse
| Numeric
export type AnyContractType = export type AnyContractType =
| (CPMM & Binary) | (CPMM & Binary)
| (CPMM & PseudoNumeric) | (CPMM & PseudoNumeric)
| (DPM & Binary) | (DPM & Binary)
| (DPM & FreeResponse) | (DPM & FreeResponse)
| (DPM & Numeric) | (DPM & Numeric)
| (DPM & MultipleChoice)
export type Contract<T extends AnyContractType = AnyContractType> = { export type Contract<T extends AnyContractType = AnyContractType> = {
id: string id: string
@ -32,7 +24,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
description: string | JSONContent // More info about what the contract is about description: string | JSONContent // More info about what the contract is about
tags: string[] tags: string[]
lowercaseTags: string[] lowercaseTags: string[]
visibility: visibility visibility: 'public' | 'unlisted'
createdTime: number // Milliseconds since epoch createdTime: number // Milliseconds since epoch
lastUpdatedTime?: number // Updated on new bet or comment lastUpdatedTime?: number // Updated on new bet or comment
@ -50,30 +42,18 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
volume: number volume: number
volume24Hours: number volume24Hours: number
volume7Days: number volume7Days: number
elasticity: number
collectedFees: Fees collectedFees: Fees
groupSlugs?: string[] groupSlugs?: string[]
groupLinks?: GroupLink[]
uniqueBettorIds?: string[] uniqueBettorIds?: string[]
uniqueBettorCount?: number uniqueBettorCount?: number
popularityScore?: number
dailyScore?: number
followerCount?: number
featuredOnHomeRank?: number
likedByUserIds?: string[]
likedByUserCount?: number
flaggedByUsernames?: string[]
openCommentBounties?: number
unlistedById?: string
} & T } & T
export type BinaryContract = Contract & Binary export type BinaryContract = Contract & Binary
export type PseudoNumericContract = Contract & PseudoNumeric export type PseudoNumericContract = Contract & PseudoNumeric
export type NumericContract = Contract & Numeric export type NumericContract = Contract & Numeric
export type FreeResponseContract = Contract & FreeResponse export type FreeResponseContract = Contract & FreeResponse
export type MultipleChoiceContract = Contract & MultipleChoice
export type DPMContract = Contract & DPM export type DPMContract = Contract & DPM
export type CPMMContract = Contract & CPMM export type CPMMContract = Contract & CPMM
export type DPMBinaryContract = BinaryContract & DPM export type DPMBinaryContract = BinaryContract & DPM
@ -92,14 +72,7 @@ export type CPMM = {
mechanism: 'cpmm-1' mechanism: 'cpmm-1'
pool: { [outcome: string]: number } pool: { [outcome: string]: number }
p: number // probability constant in y^p * n^(1-p) = k p: number // probability constant in y^p * n^(1-p) = k
totalLiquidity: number // for historical reasons, this the total subsidy amount added in M$ totalLiquidity: number // in M$
subsidyPool: number // current value of subsidy pool in M$
prob: number
probChanges: {
day: number
week: number
month: number
}
} }
export type Binary = { export type Binary = {
@ -128,13 +101,6 @@ export type FreeResponse = {
resolutions?: { [outcome: string]: number } // Used for MKT resolution. resolutions?: { [outcome: string]: number } // Used for MKT resolution.
} }
export type MultipleChoice = {
outcomeType: 'MULTIPLE_CHOICE'
answers: Answer[]
resolution?: string | 'MKT' | 'CANCEL'
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
}
export type Numeric = { export type Numeric = {
outcomeType: 'NUMERIC' outcomeType: 'NUMERIC'
bucketCount: number bucketCount: number
@ -149,17 +115,13 @@ export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
export const OUTCOME_TYPES = [ export const OUTCOME_TYPES = [
'BINARY', 'BINARY',
'MULTIPLE_CHOICE',
'FREE_RESPONSE', 'FREE_RESPONSE',
'PSEUDO_NUMERIC', 'PSEUDO_NUMERIC',
'NUMERIC', 'NUMERIC',
] as const ] as const
export const MAX_QUESTION_LENGTH = 240 export const MAX_QUESTION_LENGTH = 480
export const MAX_DESCRIPTION_LENGTH = 16000 export const MAX_DESCRIPTION_LENGTH = 10000
export const MAX_TAG_LENGTH = 60 export const MAX_TAG_LENGTH = 60
export const CPMM_MIN_POOL_QTY = 0.01 export const CPMM_MIN_POOL_QTY = 0.01
export type visibility = 'public' | 'unlisted'
export const VISIBILITIES = ['public', 'unlisted'] as const

View File

@ -1,20 +0,0 @@
import { ENV_CONFIG } from './envs/constants'
const econ = ENV_CONFIG.economy
export const FIXED_ANTE = econ?.FIXED_ANTE ?? 100
export const STARTING_BALANCE = econ?.STARTING_BALANCE ?? 1000
// for sus users, i.e. multiple sign ups for same person
export const SUS_STARTING_BALANCE = econ?.SUS_STARTING_BALANCE ?? 10
export const REFERRAL_AMOUNT = econ?.REFERRAL_AMOUNT ?? 250
export const UNIQUE_BETTOR_BONUS_AMOUNT = econ?.UNIQUE_BETTOR_BONUS_AMOUNT ?? 10
export const BETTING_STREAK_BONUS_AMOUNT =
econ?.BETTING_STREAK_BONUS_AMOUNT ?? 5
export const BETTING_STREAK_BONUS_MAX = econ?.BETTING_STREAK_BONUS_MAX ?? 25
export const BETTING_STREAK_RESET_HOUR = econ?.BETTING_STREAK_RESET_HOUR ?? 7
export const FREE_MARKETS_PER_USER_MAX = econ?.FREE_MARKETS_PER_USER_MAX ?? 5
export const COMMENT_BOUNTY_AMOUNT = econ?.COMMENT_BOUNTY_AMOUNT ?? 250
export const UNIQUE_BETTOR_LIQUIDITY = 20

View File

@ -21,27 +21,15 @@ export function isWhitelisted(email?: string) {
} }
// TODO: Before open sourcing, we should turn these into env vars // TODO: Before open sourcing, we should turn these into env vars
export function isAdmin(email?: string) { export function isAdmin(email: string) {
if (!email) {
return false
}
return ENV_CONFIG.adminEmails.includes(email) return ENV_CONFIG.adminEmails.includes(email)
} }
export function isManifoldId(userId: string) {
return userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2'
}
export const DOMAIN = ENV_CONFIG.domain export const DOMAIN = ENV_CONFIG.domain
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE' export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE'
export const AUTH_COOKIE_NAME = `FBUSER_${PROJECT_ID.toUpperCase().replace(
/-/g,
'_'
)}`
// Manifold's domain or any subdomains thereof // Manifold's domain or any subdomains thereof
export const CORS_ORIGIN_MANIFOLD = new RegExp( export const CORS_ORIGIN_MANIFOLD = new RegExp(
'^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$' '^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$'
@ -52,7 +40,3 @@ export const CORS_ORIGIN_VERCEL = new RegExp(
) )
// Any localhost server on any port // Any localhost server on any port
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/ export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
export function firestoreConsolePath(contractId: string) {
return `https://console.firebase.google.com/project/${PROJECT_ID}/firestore/data/~2Fcontracts~2F${contractId}`
}

View File

@ -2,7 +2,6 @@ import { EnvConfig, PROD_CONFIG } from './prod'
export const DEV_CONFIG: EnvConfig = { export const DEV_CONFIG: EnvConfig = {
...PROD_CONFIG, ...PROD_CONFIG,
domain: 'dev.manifold.markets',
firebaseConfig: { firebaseConfig: {
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw', apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
authDomain: 'dev-mantic-markets.firebaseapp.com', authDomain: 'dev-mantic-markets.firebaseapp.com',
@ -16,6 +15,4 @@ export const DEV_CONFIG: EnvConfig = {
cloudRunId: 'w3txbmd3ba', cloudRunId: 'w3txbmd3ba',
cloudRunRegion: 'uc', cloudRunRegion: 'uc',
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3', amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
twitchBotEndpoint: 'https://dev-twitch-bot.manifold.markets',
sprigEnvironmentId: 'Tu7kRZPm7daP',
} }

View File

@ -2,8 +2,6 @@ export type EnvConfig = {
domain: string domain: string
firebaseConfig: FirebaseConfig firebaseConfig: FirebaseConfig
amplitudeApiKey?: string amplitudeApiKey?: string
twitchBotEndpoint?: string
sprigEnvironmentId?: string
// IDs for v2 cloud functions -- find these by deploying a cloud function and // IDs for v2 cloud functions -- find these by deploying a cloud function and
// examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app // examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app
@ -17,31 +15,14 @@ export type EnvConfig = {
// Branding // Branding
moneyMoniker: string // e.g. 'M$' moneyMoniker: string // e.g. 'M$'
bettor?: string // e.g. 'bettor' or 'predictor'
presentBet?: string // e.g. 'bet' or 'predict'
pastBet?: string // e.g. 'bet' or 'prediction'
faviconPath?: string // Should be a file in /public faviconPath?: string // Should be a file in /public
navbarLogoPath?: string navbarLogoPath?: string
newQuestionPlaceholders: string[] newQuestionPlaceholders: string[]
economy?: Economy // Currency controls
} fixedAnte?: number
startingBalance?: number
export type Economy = { referralBonus?: number
FIXED_ANTE?: number
STARTING_BALANCE?: number
SUS_STARTING_BALANCE?: number
REFERRAL_AMOUNT?: number
UNIQUE_BETTOR_BONUS_AMOUNT?: number
BETTING_STREAK_BONUS_AMOUNT?: number
BETTING_STREAK_BONUS_MAX?: number
BETTING_STREAK_RESET_HOUR?: number
FREE_MARKETS_PER_USER_MAX?: number
COMMENT_BOUNTY_AMOUNT?: number
} }
type FirebaseConfig = { type FirebaseConfig = {
@ -58,7 +39,6 @@ type FirebaseConfig = {
export const PROD_CONFIG: EnvConfig = { export const PROD_CONFIG: EnvConfig = {
domain: 'manifold.markets', domain: 'manifold.markets',
amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15', amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15',
sprigEnvironmentId: 'sQcrq9TDqkib',
firebaseConfig: { firebaseConfig: {
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw', apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
@ -70,7 +50,6 @@ export const PROD_CONFIG: EnvConfig = {
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
measurementId: 'G-SSFK1Q138D', measurementId: 'G-SSFK1Q138D',
}, },
twitchBotEndpoint: 'https://twitch-bot.manifold.markets',
cloudRunId: 'nggbo3neva', cloudRunId: 'nggbo3neva',
cloudRunRegion: 'uc', cloudRunRegion: 'uc',
adminEmails: [ adminEmails: [
@ -79,17 +58,10 @@ export const PROD_CONFIG: EnvConfig = {
'taowell@gmail.com', // Stephen 'taowell@gmail.com', // Stephen
'abc.sinclair@gmail.com', // Sinclair 'abc.sinclair@gmail.com', // Sinclair
'manticmarkets@gmail.com', // Manifold 'manticmarkets@gmail.com', // Manifold
'iansphilips@gmail.com', // Ian
'd4vidchee@gmail.com', // D4vid
'federicoruizcassarino@gmail.com', // Fede
'ingawei@gmail.com', //Inga
], ],
visibility: 'PUBLIC', visibility: 'PUBLIC',
moneyMoniker: 'M$', moneyMoniker: 'M$',
bettor: 'trader',
pastBet: 'trade',
presentBet: 'trade',
navbarLogoPath: '', navbarLogoPath: '',
faviconPath: '/favicon.ico', faviconPath: '/favicon.ico',
newQuestionPlaceholders: [ newQuestionPlaceholders: [

View File

@ -1,11 +1,9 @@
export const FLAT_TRADE_FEE = 0.1 // M$0.1
export const PLATFORM_FEE = 0 export const PLATFORM_FEE = 0
export const CREATOR_FEE = 0 export const CREATOR_FEE = 0.1
export const LIQUIDITY_FEE = 0 export const LIQUIDITY_FEE = 0
export const DPM_PLATFORM_FEE = 0.0 export const DPM_PLATFORM_FEE = 0.01
export const DPM_CREATOR_FEE = 0.0 export const DPM_CREATOR_FEE = 0.04
export const DPM_FEES = DPM_PLATFORM_FEE + DPM_CREATOR_FEE export const DPM_FEES = DPM_PLATFORM_FEE + DPM_CREATOR_FEE
export type Fees = { export type Fees = {

View File

@ -2,8 +2,3 @@ export type Follow = {
userId: string userId: string
timestamp: number timestamp: number
} }
export type ContractFollow = {
id: string // user id
createdTime: number
}

View File

@ -1,3 +0,0 @@
export type GlobalConfig = {
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
}

View File

@ -6,37 +6,16 @@ export type Group = {
creatorId: string // User id creatorId: string // User id
createdTime: number createdTime: number
mostRecentActivityTime: number mostRecentActivityTime: number
memberIds: string[] // User ids
anyoneCanJoin: boolean anyoneCanJoin: boolean
totalContracts: number contractIds: string[]
totalMembers: number
aboutPostId?: string
postIds: string[]
chatDisabled?: boolean
mostRecentContractAddedTime?: number
cachedLeaderboard?: {
topTraders: {
userId: string
score: number
}[]
topCreators: {
userId: string
score: number
}[]
}
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
}
chatDisabled?: boolean
mostRecentChatActivityTime?: number
mostRecentContractAddedTime?: number
}
export const MAX_GROUP_NAME_LENGTH = 75 export const MAX_GROUP_NAME_LENGTH = 75
export const MAX_ABOUT_LENGTH = 140 export const MAX_ABOUT_LENGTH = 140
export const MAX_ID_LENGTH = 60 export const MAX_ID_LENGTH = 60
export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome']
export const GROUP_CHAT_SLUG = 'chat' export const GROUP_CHAT_SLUG = 'chat'
export type GroupLink = {
slug: string
name: string
groupId: string
createdTime: number
userId?: string
}
export type GroupContractDoc = { contractId: string; createdTime: number }

View File

@ -1,9 +0,0 @@
export type Like = {
id: string // will be id of the object liked, i.e. contract.id
userId: string
type: 'contract' | 'post'
createdTime: number
tipTxnId?: string // only holds most recent tip txn id
}
export const LIKE_TIP_AMOUNT = 10
export const TIP_UNDO_DURATION = 2000

View File

@ -1,138 +0,0 @@
import { Dictionary, groupBy, sumBy, minBy } from 'lodash'
import { Bet } from './bet'
import { getContractBetMetrics } from './calculate'
import {
Contract,
CPMMContract,
FreeResponseContract,
MultipleChoiceContract,
} from './contract'
import { PortfolioMetrics, User } from './user'
import { filterDefined } from './util/array'
const LOAN_DAILY_RATE = 0.02
const calculateNewLoan = (investedValue: number, loanTotal: number) => {
const netValue = investedValue - loanTotal
return netValue * LOAN_DAILY_RATE
}
export const getLoanUpdates = (
users: User[],
contractsById: { [contractId: string]: Contract },
portfolioByUser: { [userId: string]: PortfolioMetrics | undefined },
betsByUser: { [userId: string]: Bet[] }
) => {
const eligibleUsers = filterDefined(
users.map((user) =>
isUserEligibleForLoan(portfolioByUser[user.id]) ? user : undefined
)
)
const betUpdates = eligibleUsers
.map((user) => {
const updates = calculateLoanBetUpdates(
betsByUser[user.id] ?? [],
contractsById
).betUpdates
return updates.map((update) => ({ ...update, user }))
})
.flat()
const updatesByUser = groupBy(betUpdates, (update) => update.userId)
const userPayouts = Object.values(updatesByUser).map((updates) => {
return {
user: updates[0].user,
payout: sumBy(updates, (update) => update.newLoan),
}
})
return {
betUpdates,
userPayouts,
}
}
const isUserEligibleForLoan = (portfolio: PortfolioMetrics | undefined) => {
if (!portfolio) return true
const { balance, investmentValue } = portfolio
return balance + investmentValue > 0
}
const calculateLoanBetUpdates = (
bets: Bet[],
contractsById: Dictionary<Contract>
) => {
const betsByContract = groupBy(bets, (bet) => bet.contractId)
const contracts = filterDefined(
Object.keys(betsByContract).map((contractId) => contractsById[contractId])
).filter((c) => !c.isResolved)
const betUpdates = filterDefined(
contracts
.map((c) => {
if (c.mechanism === 'cpmm-1') {
return getBinaryContractLoanUpdate(c, betsByContract[c.id])
} else if (
c.outcomeType === 'FREE_RESPONSE' ||
c.outcomeType === 'MULTIPLE_CHOICE'
)
return getFreeResponseContractLoanUpdate(c, betsByContract[c.id])
else {
// Unsupported contract / mechanism for loans.
return []
}
})
.flat()
)
const totalNewLoan = sumBy(betUpdates, (loanUpdate) => loanUpdate.loanTotal)
return {
totalNewLoan,
betUpdates,
}
}
const getBinaryContractLoanUpdate = (contract: CPMMContract, bets: Bet[]) => {
const { invested } = getContractBetMetrics(contract, bets)
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
const oldestBet = minBy(bets, (bet) => bet.createdTime)
const newLoan = calculateNewLoan(invested, loanAmount)
if (!isFinite(newLoan) || newLoan <= 0 || !oldestBet) return undefined
const loanTotal = (oldestBet.loanAmount ?? 0) + newLoan
return {
userId: oldestBet.userId,
contractId: contract.id,
betId: oldestBet.id,
newLoan,
loanTotal,
}
}
const getFreeResponseContractLoanUpdate = (
contract: FreeResponseContract | MultipleChoiceContract,
bets: Bet[]
) => {
const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
return openBets.map((bet) => {
const loanAmount = bet.loanAmount ?? 0
const newLoan = calculateNewLoan(bet.amount, loanAmount)
const loanTotal = loanAmount + newLoan
if (!isFinite(newLoan) || newLoan <= 0) return undefined
return {
userId: bet.userId,
contractId: contract.id,
betId: bet.id,
newLoan,
loanTotal,
}
})
}

View File

@ -1,6 +1,6 @@
import { sortBy, sum, sumBy } from 'lodash' import { sortBy, sumBy } from 'lodash'
import { Bet, fill, LimitBet, NumericBet } from './bet' import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
import { import {
calculateDpmShares, calculateDpmShares,
getDpmProbability, getDpmProbability,
@ -17,7 +17,7 @@ import {
import { import {
CPMMBinaryContract, CPMMBinaryContract,
DPMBinaryContract, DPMBinaryContract,
DPMContract, FreeResponseContract,
NumericContract, NumericContract,
PseudoNumericContract, PseudoNumericContract,
} from './contract' } from './contract'
@ -30,10 +30,7 @@ import {
floatingLesserEqual, floatingLesserEqual,
} from './util/math' } from './util/math'
export type CandidateBet<T extends Bet = Bet> = Omit< export type CandidateBet<T extends Bet = Bet> = Omit<T, 'id' | 'userId'>
T,
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
>
export type BetInfo = { export type BetInfo = {
newBet: CandidateBet newBet: CandidateBet
newPool?: { [outcome: string]: number } newPool?: { [outcome: string]: number }
@ -143,16 +140,8 @@ export const computeFills = (
betAmount: number, betAmount: number,
state: CpmmState, state: CpmmState,
limitProb: number | undefined, limitProb: number | undefined,
unfilledBets: LimitBet[], unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
) => { ) => {
if (isNaN(betAmount)) {
throw new Error('Invalid bet amount: ${betAmount}')
}
if (isNaN(limitProb ?? 0)) {
throw new Error('Invalid limitProb: ${limitProb}')
}
const sortedBets = sortBy( const sortedBets = sortBy(
unfilledBets.filter((bet) => bet.outcome !== outcome), unfilledBets.filter((bet) => bet.outcome !== outcome),
(bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb), (bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb),
@ -166,12 +155,10 @@ export const computeFills = (
shares: number shares: number
timestamp: number timestamp: number
}[] = [] }[] = []
const ordersToCancel: LimitBet[] = []
let amount = betAmount let amount = betAmount
let cpmmState = { pool: state.pool, p: state.p } let cpmmState = { pool: state.pool, p: state.p }
let totalFees = noFees let totalFees = noFees
const currentBalanceByUserId = { ...balanceByUserId }
let i = 0 let i = 0
while (true) { while (true) {
@ -188,20 +175,9 @@ export const computeFills = (
takers.push(taker) takers.push(taker)
} else { } else {
// Matched against bet. // Matched against bet.
i++
const { userId } = maker.bet
const makerBalance = currentBalanceByUserId[userId]
if (floatingGreaterEqual(makerBalance, maker.amount)) {
currentBalanceByUserId[userId] = makerBalance - maker.amount
} else {
// Insufficient balance. Cancel maker bet.
ordersToCancel.push(maker.bet)
continue
}
takers.push(taker) takers.push(taker)
makers.push(maker) makers.push(maker)
i++
} }
amount -= taker.amount amount -= taker.amount
@ -209,7 +185,7 @@ export const computeFills = (
if (floatingEqual(amount, 0)) break if (floatingEqual(amount, 0)) break
} }
return { takers, makers, totalFees, cpmmState, ordersToCancel } return { takers, makers, totalFees, cpmmState }
} }
export const getBinaryCpmmBetInfo = ( export const getBinaryCpmmBetInfo = (
@ -217,17 +193,15 @@ export const getBinaryCpmmBetInfo = (
betAmount: number, betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract, contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number | undefined, limitProb: number | undefined,
unfilledBets: LimitBet[], unfilledBets: LimitBet[]
balanceByUserId: { [userId: string]: number }
) => { ) => {
const { pool, p } = contract const { pool, p } = contract
const { takers, makers, cpmmState, totalFees, ordersToCancel } = computeFills( const { takers, makers, cpmmState, totalFees } = computeFills(
outcome, outcome,
betAmount, betAmount,
{ pool, p }, { pool, p },
limitProb, limitProb,
unfilledBets, unfilledBets
balanceByUserId
) )
const probBefore = getCpmmProbability(contract.pool, contract.p) const probBefore = getCpmmProbability(contract.pool, contract.p)
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p) const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
@ -262,42 +236,14 @@ export const getBinaryCpmmBetInfo = (
newP: cpmmState.p, newP: cpmmState.p,
newTotalLiquidity, newTotalLiquidity,
makers, makers,
ordersToCancel,
} }
} }
export const getBinaryBetStats = (
outcome: 'YES' | 'NO',
betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number,
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => {
const { newBet } = getBinaryCpmmBetInfo(
outcome,
betAmount ?? 0,
contract,
limitProb,
unfilledBets,
balanceByUserId
)
const remainingMatched =
((newBet.orderAmount ?? 0) - newBet.amount) /
(outcome === 'YES' ? limitProb : 1 - limitProb)
const currentPayout = newBet.shares + remainingMatched
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
const totalFees = sum(Object.values(newBet.fees))
return { currentPayout, currentReturn, totalFees, newBet }
}
export const getNewBinaryDpmBetInfo = ( export const getNewBinaryDpmBetInfo = (
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
amount: number, amount: number,
contract: DPMBinaryContract contract: DPMBinaryContract,
loanAmount: number
) => { ) => {
const { YES: yesPool, NO: noPool } = contract.pool const { YES: yesPool, NO: noPool } = contract.pool
@ -328,7 +274,7 @@ export const getNewBinaryDpmBetInfo = (
const newBet: CandidateBet = { const newBet: CandidateBet = {
contractId: contract.id, contractId: contract.id,
amount, amount,
loanAmount: 0, loanAmount,
shares, shares,
outcome, outcome,
probBefore, probBefore,
@ -343,7 +289,8 @@ export const getNewBinaryDpmBetInfo = (
export const getNewMultiBetInfo = ( export const getNewMultiBetInfo = (
outcome: string, outcome: string,
amount: number, amount: number,
contract: DPMContract contract: FreeResponseContract,
loanAmount: number
) => { ) => {
const { pool, totalShares, totalBets } = contract const { pool, totalShares, totalBets } = contract
@ -364,7 +311,7 @@ export const getNewMultiBetInfo = (
const newBet: CandidateBet = { const newBet: CandidateBet = {
contractId: contract.id, contractId: contract.id,
amount, amount,
loanAmount: 0, loanAmount,
shares, shares,
outcome, outcome,
probBefore, probBefore,
@ -418,3 +365,13 @@ export const getNumericBetsInfo = (
return { newBet, newPool, newTotalShares, newTotalBets } return { newBet, newPool, newTotalShares, newTotalBets }
} }
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

@ -5,13 +5,12 @@ import {
CPMM, CPMM,
DPM, DPM,
FreeResponse, FreeResponse,
MultipleChoice,
Numeric, Numeric,
outcomeType, outcomeType,
PseudoNumeric, PseudoNumeric,
visibility,
} from './contract' } from './contract'
import { User } from './user' import { User } from './user'
import { parseTags, richTextToString } from './util/parse'
import { removeUndefinedProps } from './util/object' import { removeUndefinedProps } from './util/object'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
@ -31,12 +30,17 @@ export function getNewContract(
bucketCount: number, bucketCount: number,
min: number, min: number,
max: number, max: number,
isLogScale: boolean, isLogScale: boolean
// for multiple choice
answers: string[],
visibility: visibility
) { ) {
const tags = parseTags(
[
question,
richTextToString(description),
...extraTags.map((tag) => `#${tag}`),
].join(' ')
)
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
const propsByOutcomeType = const propsByOutcomeType =
outcomeType === 'BINARY' outcomeType === 'BINARY'
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante) ? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
@ -44,8 +48,6 @@ export function getNewContract(
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale) ? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
: outcomeType === 'NUMERIC' : outcomeType === 'NUMERIC'
? getNumericProps(ante, bucketCount, min, max) ? getNumericProps(ante, bucketCount, min, max)
: outcomeType === 'MULTIPLE_CHOICE'
? getMultipleChoiceProps(ante, answers)
: getFreeAnswerProps(ante) : getFreeAnswerProps(ante)
const contract: Contract = removeUndefinedProps({ const contract: Contract = removeUndefinedProps({
@ -60,10 +62,9 @@ export function getNewContract(
question: question.trim(), question: question.trim(),
description, description,
tags: [], tags,
lowercaseTags: [], lowercaseTags,
visibility, visibility: 'public',
unlistedById: visibility === 'unlisted' ? creator.id : undefined,
isResolved: false, isResolved: false,
createdTime: Date.now(), createdTime: Date.now(),
closeTime, closeTime,
@ -71,7 +72,6 @@ export function getNewContract(
volume: 0, volume: 0,
volume24Hours: 0, volume24Hours: 0,
volume7Days: 0, volume7Days: 0,
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
collectedFees: { collectedFees: {
creatorFee: 0, creatorFee: 0,
@ -112,12 +112,9 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
mechanism: 'cpmm-1', mechanism: 'cpmm-1',
outcomeType: 'BINARY', outcomeType: 'BINARY',
totalLiquidity: ante, totalLiquidity: ante,
subsidyPool: 0,
initialProbability: p, initialProbability: p,
p, p,
pool: pool, pool: pool,
prob: initialProb,
probChanges: { day: 0, week: 0, month: 0 },
} }
return system return system
@ -154,26 +151,6 @@ const getFreeAnswerProps = (ante: number) => {
return system return system
} }
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
const numAnswers = answers.length
const betAnte = ante / numAnswers
const betShares = Math.sqrt(ante ** 2 / numAnswers)
const defaultValues = (x: any) =>
Object.fromEntries(range(0, numAnswers).map((k) => [k, x]))
const system: DPM & MultipleChoice = {
mechanism: 'dpm-2',
outcomeType: 'MULTIPLE_CHOICE',
pool: defaultValues(betAnte),
totalShares: defaultValues(betShares),
totalBets: defaultValues(betAnte),
answers: [],
}
return system
}
const getNumericProps = ( const getNumericProps = (
ante: number, ante: number,
bucketCount: number, bucketCount: number,

View File

@ -1,10 +1,8 @@
import { notification_preference } from './user-notification-preferences'
export type Notification = { export type Notification = {
id: string id: string
userId: string userId: string
reasonText?: string reasonText?: string
reason?: notification_reason_types | notification_preference reason?: notification_reason_types
createdTime: number createdTime: number
viewTime?: number viewTime?: number
isSeen: boolean isSeen: boolean
@ -17,7 +15,6 @@ export type Notification = {
sourceUserUsername?: string sourceUserUsername?: string
sourceUserAvatarUrl?: string sourceUserAvatarUrl?: string
sourceText?: string sourceText?: string
data?: { [key: string]: any }
sourceContractTitle?: string sourceContractTitle?: string
sourceContractCreatorUsername?: string sourceContractCreatorUsername?: string
@ -28,7 +25,6 @@ export type Notification = {
isSeenOnHref?: string isSeenOnHref?: string
} }
export type notification_source_types = export type notification_source_types =
| 'contract' | 'contract'
| 'comment' | 'comment'
@ -41,12 +37,6 @@ export type notification_source_types =
| 'group' | 'group'
| 'user' | 'user'
| 'bonus' | 'bonus'
| 'challenge'
| 'betting_streak_bonus'
| 'loan'
| 'like'
| 'tip_and_like'
| 'badge'
export type notification_source_update_types = export type notification_source_update_types =
| 'created' | 'created'
@ -55,216 +45,22 @@ export type notification_source_update_types =
| 'deleted' | 'deleted'
| 'closed' | 'closed'
/* Optional - if possible use a notification_preference */
export type notification_reason_types = export type notification_reason_types =
| 'tagged_user' | 'tagged_user'
| 'on_users_contract'
| 'on_contract_with_users_shares_in'
| 'on_contract_with_users_shares_out'
| 'on_contract_with_users_answer'
| 'on_contract_with_users_comment'
| 'reply_to_users_answer'
| 'reply_to_users_comment'
| 'on_new_follow' | 'on_new_follow'
| 'contract_from_followed_user' | 'you_follow_user'
| 'added_you_to_group'
| 'you_referred_user' | 'you_referred_user'
| 'user_joined_to_bet_on_your_market' | 'user_joined_to_bet_on_your_market'
| 'unique_bettors_on_your_contract' | 'unique_bettors_on_your_contract'
| 'on_group_you_are_member_of'
| 'tip_received' | 'tip_received'
| 'bet_fill' | 'bet_fill'
| 'user_joined_from_your_group_invite' | 'user_joined_from_your_group_invite'
| 'challenge_accepted'
| 'betting_streak_incremented'
| 'loan_income'
| 'liked_and_tipped_your_contract'
| 'comment_on_your_contract'
| 'answer_on_your_contract'
| 'comment_on_contract_you_follow'
| 'answer_on_contract_you_follow'
| 'update_on_contract_you_follow'
| 'resolution_on_contract_you_follow'
| 'comment_on_contract_with_users_shares_in'
| 'answer_on_contract_with_users_shares_in'
| 'update_on_contract_with_users_shares_in'
| 'resolution_on_contract_with_users_shares_in'
| 'comment_on_contract_with_users_answer'
| 'update_on_contract_with_users_answer'
| 'resolution_on_contract_with_users_answer'
| 'answer_on_contract_with_users_answer'
| 'comment_on_contract_with_users_comment'
| 'answer_on_contract_with_users_comment'
| 'update_on_contract_with_users_comment'
| 'resolution_on_contract_with_users_comment'
| 'reply_to_users_answer'
| 'reply_to_users_comment'
| 'your_contract_closed'
| 'subsidized_your_market'
type notification_descriptions = {
[key in notification_preference]: {
simple: string
detailed: string
necessary?: boolean
}
}
export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = {
all_answers_on_my_markets: {
simple: 'Answers on your markets',
detailed: 'Answers on your own markets',
},
all_comments_on_my_markets: {
simple: 'Comments on your markets',
detailed: 'Comments on your own markets',
},
answers_by_followed_users_on_watched_markets: {
simple: 'Only answers by users you follow',
detailed: "Only answers by users you follow on markets you're watching",
},
answers_by_market_creator_on_watched_markets: {
simple: 'Only answers by market creator',
detailed: "Only answers by market creator on markets you're watching",
},
betting_streaks: {
simple: `For prediction streaks`,
detailed: `Bonuses for predictions made over consecutive days (Prediction streaks)})`,
},
comments_by_followed_users_on_watched_markets: {
simple: 'Only comments by users you follow',
detailed:
'Only comments by users that you follow on markets that you watch',
},
contract_from_followed_user: {
simple: 'New markets from users you follow',
detailed: 'New markets from users you follow',
},
limit_order_fills: {
simple: 'Limit order fills',
detailed: 'When your limit order is filled by another user',
},
loan_income: {
simple: 'Automatic loans from your predictions in unresolved markets',
detailed:
'Automatic loans from your predictions that are locked in unresolved markets',
},
market_updates_on_watched_markets: {
simple: 'All creator updates',
detailed: 'All market updates made by the creator',
},
market_updates_on_watched_markets_with_shares_in: {
simple: "Only creator updates on markets that you're invested in",
detailed:
"Only updates made by the creator on markets that you're invested in",
},
on_new_follow: {
simple: 'A user followed you',
detailed: 'A user followed you',
},
onboarding_flow: {
simple: 'Emails to help you get started using Manifold',
detailed: 'Emails to help you learn how to use Manifold',
},
probability_updates_on_watched_markets: {
simple: 'Large changes in probability on markets that you watch',
detailed: 'Large changes in probability on markets that you watch',
},
profit_loss_updates: {
simple: 'Weekly portfolio updates',
detailed: 'Weekly portfolio updates',
},
referral_bonuses: {
simple: 'For referring new users',
detailed: 'Bonuses you receive from referring a new user',
},
resolutions_on_watched_markets: {
simple: 'All market resolutions',
detailed: "All resolutions on markets that you're watching",
},
resolutions_on_watched_markets_with_shares_in: {
simple: "Only market resolutions that you're invested in",
detailed:
"Only resolutions of markets you're watching and that you're invested in",
},
subsidized_your_market: {
simple: 'Your market was subsidized',
detailed: 'When someone subsidizes your market',
},
tagged_user: {
simple: 'A user tagged you',
detailed: 'When another use tags you',
},
thank_you_for_purchases: {
simple: 'Thank you notes for your purchases',
detailed: 'Thank you notes for your purchases',
},
tipped_comments_on_watched_markets: {
simple: 'Only highly tipped comments on markets that you watch',
detailed: 'Only highly tipped comments on markets that you watch',
},
tips_on_your_comments: {
simple: 'Tips on your comments',
detailed: 'Tips on your comments',
},
tips_on_your_markets: {
simple: 'Tips/Likes on your markets',
detailed: 'Tips/Likes on your markets',
},
trending_markets: {
simple: 'Weekly interesting markets',
detailed: 'Weekly interesting markets',
},
unique_bettors_on_your_contract: {
simple: 'For unique predictors on your markets',
detailed: 'Bonuses for unique predictors on your markets',
},
your_contract_closed: {
simple: 'Your market has closed and you need to resolve it (necessary)',
detailed: 'Your market has closed and you need to resolve it (necessary)',
necessary: true,
},
all_comments_on_watched_markets: {
simple: 'All new comments',
detailed: 'All new comments on markets you follow',
},
all_comments_on_contracts_with_shares_in_on_watched_markets: {
simple: `Only on markets you're invested in`,
detailed: `Comments on markets that you're watching and you're invested in`,
},
all_replies_to_my_comments_on_watched_markets: {
simple: 'Only replies to your comments',
detailed: "Only replies to your comments on markets you're watching",
},
all_replies_to_my_answers_on_watched_markets: {
simple: 'Only replies to your answers',
detailed: "Only replies to your answers on markets you're watching",
},
all_answers_on_watched_markets: {
simple: 'All new answers',
detailed: "All new answers on markets you're watching",
},
all_answers_on_contracts_with_shares_in_on_watched_markets: {
simple: `Only on markets you're invested in`,
detailed: `Answers on markets that you're watching and that you're invested in`,
},
badges_awarded: {
simple: 'New badges awarded',
detailed: 'New badges you have earned',
},
opt_out_all: {
simple: 'Opt out of all notifications (excludes when your markets close)',
detailed:
'Opt out of all notifications excluding your own market closure notifications',
},
}
export type BettingStreakData = {
streak: number
bonusAmount: number
}
export type BetFillData = {
betOutcome: string
creatorOutcome: string
probability: number
fillAmount: number
limitOrderTotal?: number
limitOrderRemaining?: number
}
export type ContractResolutionData = {
outcome: string
userPayout: number
userInvestment: number
}

View File

@ -3,3 +3,4 @@ export const NUMERIC_FIXED_VAR = 0.005
export const NUMERIC_GRAPH_COLOR = '#5fa5f9' export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
export const NUMERIC_TEXT_COLOR = 'text-blue-500' export const NUMERIC_TEXT_COLOR = 'text-blue-500'
export const UNIQUE_BETTOR_BONUS_AMOUNT = 5

View File

@ -8,13 +8,9 @@
}, },
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@tiptap/core": "2.0.0-beta.199", "@tiptap/extension-image": "2.0.0-beta.30",
"@tiptap/extension-image": "2.0.0-beta.199", "@tiptap/extension-link": "2.0.0-beta.43",
"@tiptap/extension-link": "2.0.0-beta.199", "@tiptap/starter-kit": "2.0.0-beta.190",
"@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,17 +2,14 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
import { Bet, NumericBet } from './bet' import { Bet, NumericBet } from './bet'
import { deductDpmFees, getDpmProbability } from './calculate-dpm' import { deductDpmFees, getDpmProbability } from './calculate-dpm'
import { import { DPMContract, FreeResponseContract } from './contract'
DPMContract,
FreeResponseContract,
MultipleChoiceContract,
} from './contract'
import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees' import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
import { addObjects } from './util/object' import { addObjects } from './util/object'
export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => { export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => {
const { pool } = contract const { pool } = contract
const poolTotal = sum(Object.values(pool)) const poolTotal = sum(Object.values(pool))
console.log('resolved N/A, pool M$', poolTotal)
const betSum = sumBy(bets, (b) => b.amount) const betSum = sumBy(bets, (b) => b.amount)
@ -57,6 +54,17 @@ export const getDpmStandardPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved',
outcome,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -98,6 +106,17 @@ export const getNumericDpmPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved numeric bucket: ',
outcome,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -140,6 +159,17 @@ export const getDpmMktPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved MKT',
p,
'pool',
pool,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -150,7 +180,7 @@ export const getDpmMktPayouts = (
export const getPayoutsMultiOutcome = ( export const getPayoutsMultiOutcome = (
resolutions: { [outcome: string]: number }, resolutions: { [outcome: string]: number },
contract: FreeResponseContract | MultipleChoiceContract, contract: FreeResponseContract,
bets: Bet[] bets: Bet[]
) => { ) => {
const poolTotal = sum(Object.values(contract.pool)) const poolTotal = sum(Object.values(contract.pool))
@ -168,7 +198,7 @@ export const getPayoutsMultiOutcome = (
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
const profit = winnings - amount const profit = winnings - amount
const payout = amount + (1 - DPM_FEES) * profit const payout = amount + (1 - DPM_FEES) * Math.max(0, profit)
return { userId, profit, payout } return { userId, profit, payout }
}) })
@ -182,6 +212,16 @@ export const getPayoutsMultiOutcome = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved',
resolutions,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,

View File

@ -1,3 +1,5 @@
import { sum } from 'lodash'
import { Bet } from './bet' import { Bet } from './bet'
import { getProbability } from './calculate' import { getProbability } from './calculate'
import { getCpmmLiquidityPoolWeights } from './calculate-cpmm' import { getCpmmLiquidityPoolWeights } from './calculate-cpmm'
@ -41,6 +43,18 @@ export const getStandardFixedPayouts = (
const { collectedFees } = contract const { collectedFees } = contract
const creatorPayout = collectedFees.creatorFee const creatorPayout = collectedFees.creatorFee
console.log(
'resolved',
outcome,
'pool',
contract.pool[outcome],
'payouts',
sum(payouts),
'creator fee',
creatorPayout
)
const liquidityPayouts = getLiquidityPoolPayouts( const liquidityPayouts = getLiquidityPoolPayouts(
contract, contract,
outcome, outcome,
@ -55,11 +69,10 @@ export const getLiquidityPoolPayouts = (
outcome: string, outcome: string,
liquidities: LiquidityProvision[] liquidities: LiquidityProvision[]
) => { ) => {
const { pool, subsidyPool } = contract const { pool } = contract
const finalPool = pool[outcome] + (subsidyPool ?? 0) const finalPool = pool[outcome]
if (finalPool < 1e-3) return []
const weights = getCpmmLiquidityPoolWeights(liquidities) const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false)
return Object.entries(weights).map(([providerId, weight]) => ({ return Object.entries(weights).map(([providerId, weight]) => ({
userId: providerId, userId: providerId,
@ -85,6 +98,18 @@ export const getMktFixedPayouts = (
const { collectedFees } = contract const { collectedFees } = contract
const creatorPayout = collectedFees.creatorFee const creatorPayout = collectedFees.creatorFee
console.log(
'resolved PROB',
p,
'pool',
p * contract.pool.YES + (1 - p) * contract.pool.NO,
'payouts',
sum(payouts),
'creator fee',
creatorPayout
)
const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities) const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities)
return { payouts, creatorPayout, liquidityPayouts, collectedFees } return { payouts, creatorPayout, liquidityPayouts, collectedFees }
@ -95,11 +120,10 @@ export const getLiquidityPoolProbPayouts = (
p: number, p: number,
liquidities: LiquidityProvision[] liquidities: LiquidityProvision[]
) => { ) => {
const { pool, subsidyPool } = contract const { pool } = contract
const finalPool = p * pool.YES + (1 - p) * pool.NO + (subsidyPool ?? 0) const finalPool = p * pool.YES + (1 - p) * pool.NO
if (finalPool < 1e-3) return []
const weights = getCpmmLiquidityPoolWeights(liquidities) const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false)
return Object.entries(weights).map(([providerId, weight]) => ({ return Object.entries(weights).map(([providerId, weight]) => ({
userId: providerId, userId: providerId,

View File

@ -117,7 +117,6 @@ export const getDpmPayouts = (
resolutionProbability?: number resolutionProbability?: number
): PayoutInfo => { ): PayoutInfo => {
const openBets = bets.filter((b) => !b.isSold && !b.sale) const openBets = bets.filter((b) => !b.isSold && !b.sale)
const { outcomeType } = contract
switch (outcome) { switch (outcome) {
case 'YES': case 'YES':
@ -125,8 +124,7 @@ export const getDpmPayouts = (
return getDpmStandardPayouts(outcome, contract, openBets) return getDpmStandardPayouts(outcome, contract, openBets)
case 'MKT': case 'MKT':
return outcomeType === 'FREE_RESPONSE' || return contract.outcomeType === 'FREE_RESPONSE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
outcomeType === 'MULTIPLE_CHOICE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
? getPayoutsMultiOutcome(resolutions!, contract, openBets) ? getPayoutsMultiOutcome(resolutions!, contract, openBets)
: getDpmMktPayouts(contract, openBets, resolutionProbability) : getDpmMktPayouts(contract, openBets, resolutionProbability)
case 'CANCEL': case 'CANCEL':
@ -134,7 +132,7 @@ export const getDpmPayouts = (
return getDpmCancelPayouts(contract, openBets) return getDpmCancelPayouts(contract, openBets)
default: default:
if (outcomeType === 'NUMERIC') if (contract.outcomeType === 'NUMERIC')
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[]) return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
// Outcome is a free response answer id. // Outcome is a free response answer id.

View File

@ -1,29 +0,0 @@
import { JSONContent } from '@tiptap/core'
export type Post = {
id: string
title: string
subtitle: string
content: JSONContent
creatorId: string // User id
createdTime: number
slug: string
// denormalized user fields
creatorName: string
creatorUsername: string
creatorAvatarUrl?: string
likedByUserIds?: string[]
likedByUserCount?: number
}
export type DateDoc = Post & {
bounty: number
birthday: number
type: 'date-doc'
contractSlug: string
}
export const MAX_POST_TITLE_LENGTH = 480
export const MAX_POST_SUBTITLE_LENGTH = 480

View File

@ -37,9 +37,6 @@ export const getPseudoProbability = (
max: number, max: number,
isLogScale = false isLogScale = false
) => { ) => {
if (value < min) return 0
if (value > max) return 1
if (isLogScale) { if (isLogScale) {
return Math.log10(value - min + 1) / Math.log10(max - min + 1) return Math.log10(value - min + 1) / Math.log10(max - min + 1)
} }

View File

@ -0,0 +1,187 @@
import { union, sum, sumBy, sortBy, groupBy, mapValues } from 'lodash'
import { Bet } from './bet'
import { Contract } from './contract'
import { ClickEvent } from './tracking'
import { filterDefined } from './util/array'
import { addObjects } from './util/object'
export const MAX_FEED_CONTRACTS = 75
export const getRecommendedContracts = (
contractsById: { [contractId: string]: Contract },
yourBetOnContractIds: string[]
) => {
const contracts = Object.values(contractsById)
const yourContracts = filterDefined(
yourBetOnContractIds.map((contractId) => contractsById[contractId])
)
const yourContractIds = new Set(yourContracts.map((c) => c.id))
const notYourContracts = contracts.filter((c) => !yourContractIds.has(c.id))
const yourWordFrequency = contractsToWordFrequency(yourContracts)
const otherWordFrequency = contractsToWordFrequency(notYourContracts)
const words = union(
Object.keys(yourWordFrequency),
Object.keys(otherWordFrequency)
)
const yourWeightedFrequency = Object.fromEntries(
words.map((word) => {
const [yourFreq, otherFreq] = [
yourWordFrequency[word] ?? 0,
otherWordFrequency[word] ?? 0,
]
const score = yourFreq / (yourFreq + otherFreq + 0.0001)
return [word, score]
})
)
// console.log(
// 'your weighted frequency',
// _.sortBy(_.toPairs(yourWeightedFrequency), ([, freq]) => -freq)
// )
const scoredContracts = contracts.map((contract) => {
const wordFrequency = contractToWordFrequency(contract)
const score = sumBy(Object.keys(wordFrequency), (word) => {
const wordFreq = wordFrequency[word] ?? 0
const weight = yourWeightedFrequency[word] ?? 0
return wordFreq * weight
})
return {
contract,
score,
}
})
return sortBy(scoredContracts, (scored) => -scored.score).map(
(scored) => scored.contract
)
}
const contractToText = (contract: Contract) => {
const { description, question, tags, creatorUsername } = contract
return `${creatorUsername} ${question} ${tags.join(' ')} ${description}`
}
const MAX_CHARS_IN_WORD = 100
const getWordsCount = (text: string) => {
const normalizedText = text.replace(/[^a-zA-Z]/g, ' ').toLowerCase()
const words = normalizedText
.split(' ')
.filter((word) => word)
.filter((word) => word.length <= MAX_CHARS_IN_WORD)
const counts: { [word: string]: number } = {}
for (const word of words) {
if (counts[word]) counts[word]++
else counts[word] = 1
}
return counts
}
const toFrequency = (counts: { [word: string]: number }) => {
const total = sum(Object.values(counts))
return mapValues(counts, (count) => count / total)
}
const contractToWordFrequency = (contract: Contract) =>
toFrequency(getWordsCount(contractToText(contract)))
const contractsToWordFrequency = (contracts: Contract[]) => {
const frequencySum = contracts
.map(contractToWordFrequency)
.reduce(addObjects, {})
return toFrequency(frequencySum)
}
export const getWordScores = (
contracts: Contract[],
contractViewCounts: { [contractId: string]: number },
clicks: ClickEvent[],
bets: Bet[]
) => {
const contractClicks = groupBy(clicks, (click) => click.contractId)
const contractBets = groupBy(bets, (bet) => bet.contractId)
const yourContracts = contracts.filter(
(c) =>
contractViewCounts[c.id] || contractClicks[c.id] || contractBets[c.id]
)
const yourTfIdf = calculateContractTfIdf(yourContracts)
const contractWordScores = mapValues(yourTfIdf, (wordsTfIdf, contractId) => {
const viewCount = contractViewCounts[contractId] ?? 0
const clickCount = contractClicks[contractId]?.length ?? 0
const betCount = contractBets[contractId]?.length ?? 0
const factor =
-1 * Math.log(viewCount + 1) +
10 * Math.log(betCount + clickCount / 4 + 1)
return mapValues(wordsTfIdf, (tfIdf) => tfIdf * factor)
})
const wordScores = Object.values(contractWordScores).reduce(addObjects, {})
const minScore = Math.min(...Object.values(wordScores))
const maxScore = Math.max(...Object.values(wordScores))
const normalizedWordScores = mapValues(
wordScores,
(score) => (score - minScore) / (maxScore - minScore)
)
// console.log(
// 'your word scores',
// _.sortBy(_.toPairs(normalizedWordScores), ([, score]) => -score).slice(0, 100),
// _.sortBy(_.toPairs(normalizedWordScores), ([, score]) => -score).slice(-100)
// )
return normalizedWordScores
}
export function getContractScore(
contract: Contract,
wordScores: { [word: string]: number }
) {
if (Object.keys(wordScores).length === 0) return 1
const wordFrequency = contractToWordFrequency(contract)
const score = sumBy(Object.keys(wordFrequency), (word) => {
const wordFreq = wordFrequency[word] ?? 0
const weight = wordScores[word] ?? 0
return wordFreq * weight
})
return score
}
// Caluculate Term Frequency-Inverse Document Frequency (TF-IDF):
// https://medium.datadriveninvestor.com/tf-idf-in-natural-language-processing-8db8ef4a7736
function calculateContractTfIdf(contracts: Contract[]) {
const contractFreq = contracts.map((c) => contractToWordFrequency(c))
const contractWords = contractFreq.map((freq) => Object.keys(freq))
const wordsCount: { [word: string]: number } = {}
for (const words of contractWords) {
for (const word of words) {
wordsCount[word] = (wordsCount[word] ?? 0) + 1
}
}
const wordIdf = mapValues(wordsCount, (count) =>
Math.log(contracts.length / count)
)
const contractWordsTfIdf = contractFreq.map((wordFreq) =>
mapValues(wordFreq, (freq, word) => freq * wordIdf[word])
)
return Object.fromEntries(
contracts.map((c, i) => [c.id, contractWordsTfIdf[i]])
)
}

View File

@ -13,12 +13,8 @@ export const getRedeemableAmount = (bets: RedeemableBet[]) => {
const yesShares = sumBy(yesBets, (b) => b.shares) const yesShares = sumBy(yesBets, (b) => b.shares)
const noShares = sumBy(noBets, (b) => b.shares) const noShares = sumBy(noBets, (b) => b.shares)
const shares = Math.max(Math.min(yesShares, noShares), 0) const shares = Math.max(Math.min(yesShares, noShares), 0)
const soldFrac =
shares > 0
? Math.min(yesShares, noShares) / Math.max(yesShares, noShares)
: 0
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0) const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
const loanPayment = loanAmount * soldFrac const loanPayment = Math.min(loanAmount, shares)
const netAmount = shares - loanPayment const netAmount = shares - loanPayment
return { shares, loanPayment, netAmount } return { shares, loanPayment, netAmount }
} }

View File

@ -1,9 +1,8 @@
import { groupBy, sumBy, mapValues, keyBy, sortBy } from 'lodash' import { groupBy, sumBy, mapValues, partition } from 'lodash'
import { Bet } from './bet' import { Bet } from './bet'
import { getContractBetMetrics, resolvedPayout } from './calculate'
import { Contract } from './contract' import { Contract } from './contract'
import { ContractComment } from './comment' import { getPayouts } from './payouts'
export function scoreCreators(contracts: Contract[]) { export function scoreCreators(contracts: Contract[]) {
const creatorScore = mapValues( const creatorScore = mapValues(
@ -31,11 +30,46 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
} }
export function scoreUsersByContract(contract: Contract, bets: Bet[]) { export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
const betsByUser = groupBy(bets, (bet) => bet.userId) const { resolution } = contract
return mapValues( const resolutionProb =
betsByUser, contract.outcomeType == 'BINARY'
(bets) => getContractBetMetrics(contract, bets).profit ? contract.resolutionProbability
: undefined
const [closedBets, openBets] = partition(
bets,
(bet) => bet.isSold || bet.sale
) )
const { payouts: resolvePayouts } = getPayouts(
resolution as string,
contract,
openBets,
[],
{},
resolutionProb
)
const salePayouts = closedBets.map((bet) => {
const { userId, sale } = bet
return { userId, payout: sale ? sale.amount : 0 }
})
const investments = bets
.filter((bet) => !bet.sale)
.map((bet) => {
const { userId, amount, loanAmount } = bet
const payout = -amount - (loanAmount ?? 0)
return { userId, payout }
})
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
const userScore = mapValues(
groupBy(netPayouts, (payout) => payout.userId),
(payouts) => sumBy(payouts, ({ payout }) => payout)
)
return userScore
} }
export function addUserScores( export function addUserScores(
@ -47,47 +81,3 @@ export function addUserScores(
dest[userId] += score dest[userId] += score
} }
} }
export function scoreCommentorsAndBettors(
contract: Contract,
bets: Bet[],
comments: ContractComment[]
) {
const commentsById = keyBy(comments, 'id')
const betsById = keyBy(bets, 'id')
// If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit
// Otherwise, we record the profit at resolution time
const profitById: Record<string, number> = {}
for (const bet of bets) {
if (bet.sale) {
const originalBet = betsById[bet.sale.betId]
const profit = bet.sale.amount - originalBet.amount
profitById[bet.id] = profit
profitById[originalBet.id] = profit
} else {
profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount
}
}
// Now find the betId with the highest profit
const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id
const topBettor = betsById[topBetId]?.userName
// And also the commentId of the comment with the highest profit
const topCommentId = sortBy(
comments,
(c) => c.betId && -profitById[c.betId]
)[0]?.id
const topCommentBetId = commentsById[topCommentId]?.betId
return {
topCommentId,
topBetId,
topBettor,
profitById,
commentsById,
betsById,
topCommentBetId,
}
}

View File

@ -9,14 +9,11 @@ import { CPMMContract, DPMContract } from './contract'
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees' import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
import { sumBy } from 'lodash' import { sumBy } from 'lodash'
export type CandidateBet<T extends Bet> = Omit< export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
T,
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
>
export const getSellBetInfo = (bet: Bet, contract: DPMContract) => { export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
const { pool, totalShares, totalBets } = contract const { pool, totalShares, totalBets } = contract
const { id: betId, amount, shares, outcome, loanAmount } = bet const { id: betId, amount, shares, outcome } = bet
const adjShareValue = calculateDpmShareValue(contract, bet) const adjShareValue = calculateDpmShareValue(contract, bet)
@ -67,7 +64,6 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
betId, betId,
}, },
fees, fees,
loanAmount: -(loanAmount ?? 0),
} }
return { return {
@ -83,20 +79,19 @@ export const getCpmmSellBetInfo = (
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
contract: CPMMContract, contract: CPMMContract,
unfilledBets: LimitBet[], prevLoanAmount: number,
balanceByUserId: { [userId: string]: number }, unfilledBets: LimitBet[]
loanPaid: number
) => { ) => {
const { pool, p } = contract const { pool, p } = contract
const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale( const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale(
contract, contract,
shares, shares,
outcome, outcome,
unfilledBets, unfilledBets
balanceByUserId,
) )
const loanPaid = Math.min(prevLoanAmount, saleValue)
const probBefore = getCpmmProbability(pool, p) const probBefore = getCpmmProbability(pool, p)
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p) const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
@ -136,6 +131,5 @@ export const getCpmmSellBetInfo = (
fees, fees,
makers, makers,
takers, takers,
ordersToCancel
} }
} }

View File

@ -1,22 +1,20 @@
export type Stats = { export type Stats = {
startDate: number startDate: number
dailyActiveUsers: number[] dailyActiveUsers: number[]
dailyActiveUsersWeeklyAvg: number[]
weeklyActiveUsers: number[] weeklyActiveUsers: number[]
monthlyActiveUsers: number[] monthlyActiveUsers: number[]
d1: number[]
d1WeeklyAvg: number[]
nd1: number[]
nd1WeeklyAvg: number[]
nw1: number[]
dailyBetCounts: number[] dailyBetCounts: number[]
dailyContractCounts: number[] dailyContractCounts: number[]
dailyCommentCounts: number[] dailyCommentCounts: number[]
dailySignups: number[] dailySignups: number[]
weekOnWeekRetention: number[] weekOnWeekRetention: number[]
monthlyRetention: number[] monthlyRetention: number[]
dailyActivationRate: number[] weeklyActivationRate: number[]
dailyActivationRateWeeklyAvg: number[] topTenthActions: {
daily: number[]
weekly: number[]
monthly: number[]
}
manaBet: { manaBet: {
daily: number[] daily: number[]
weekly: number[] weekly: number[]

View File

@ -1,14 +1,6 @@
// A txn (pronounced "texan") respresents a payment between two ids on Manifold // A txn (pronounced "texan") respresents a payment between two ids on Manifold
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars) // Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
type AnyTxnType = type AnyTxnType = Donation | Tip | Manalink | Referral | Bonus
| Donation
| Tip
| Manalink
| Referral
| UniqueBettorBonus
| BettingStreakBonus
| CancelUniqueBettorBonus
| CommentBountyRefund
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK' type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
export type Txn<T extends AnyTxnType = AnyTxnType> = { export type Txn<T extends AnyTxnType = AnyTxnType> = {
@ -24,16 +16,7 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
amount: number amount: number
token: 'M$' // | 'USD' | MarketOutcome token: 'M$' // | 'USD' | MarketOutcome
category: category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
| 'CHARITY'
| 'MANALINK'
| 'TIP'
| 'REFERRAL'
| 'UNIQUE_BETTOR_BONUS'
| 'BETTING_STREAK_BONUS'
| 'CANCEL_UNIQUE_BETTOR_BONUS'
| 'COMMENT_BOUNTY'
| 'REFUND_COMMENT_BOUNTY'
// Any extra data // Any extra data
data?: { [key: string]: any } data?: { [key: string]: any }
@ -71,70 +54,13 @@ type Referral = {
category: 'REFERRAL' category: 'REFERRAL'
} }
type UniqueBettorBonus = { type Bonus = {
fromType: 'BANK' fromType: 'BANK'
toType: 'USER' toType: 'USER'
category: 'UNIQUE_BETTOR_BONUS' category: 'UNIQUE_BETTOR_BONUS'
data: {
contractId: string
uniqueNewBettorId?: string
// Old unique bettor bonus txns stored all unique bettor ids
uniqueBettorIds?: string[]
}
}
type BettingStreakBonus = {
fromType: 'BANK'
toType: 'USER'
category: 'BETTING_STREAK_BONUS'
data: {
currentBettingStreak?: number
}
}
type CancelUniqueBettorBonus = {
fromType: 'USER'
toType: 'BANK'
category: 'CANCEL_UNIQUE_BETTOR_BONUS'
data: {
contractId: string
}
}
type CommentBountyDeposit = {
fromType: 'USER'
toType: 'BANK'
category: 'COMMENT_BOUNTY'
data: {
contractId: string
}
}
type CommentBountyWithdrawal = {
fromType: 'BANK'
toType: 'USER'
category: 'COMMENT_BOUNTY'
data: {
contractId: string
commentId: string
}
}
type CommentBountyRefund = {
fromType: 'BANK'
toType: 'USER'
category: 'REFUND_COMMENT_BOUNTY'
data: {
contractId: string
}
} }
export type DonationTxn = Txn & Donation export type DonationTxn = Txn & Donation
export type TipTxn = Txn & Tip export type TipTxn = Txn & Tip
export type ManalinkTxn = Txn & Manalink export type ManalinkTxn = Txn & Manalink
export type ReferralTxn = Txn & Referral export type ReferralTxn = Txn & Referral
export type BettingStreakBonusTxn = Txn & BettingStreakBonus
export type UniqueBettorBonusTxn = Txn & UniqueBettorBonus
export type CancelUniqueBettorBonusTxn = Txn & CancelUniqueBettorBonus
export type CommentBountyDepositTxn = Txn & CommentBountyDeposit
export type CommentBountyWithdrawalTxn = Txn & CommentBountyWithdrawal

View File

@ -1,222 +0,0 @@
import { filterDefined } from './util/array'
import { notification_reason_types } from './notification'
import { getFunctionUrl } from './api'
import { DOMAIN } from './envs/constants'
import { PrivateUser } from './user'
export type notification_destination_types = 'email' | 'browser'
export type notification_preference = keyof notification_preferences
export type notification_preferences = {
// Watched Markets
all_comments_on_watched_markets: notification_destination_types[]
all_answers_on_watched_markets: notification_destination_types[]
// Comments
tipped_comments_on_watched_markets: notification_destination_types[]
comments_by_followed_users_on_watched_markets: notification_destination_types[]
all_replies_to_my_comments_on_watched_markets: notification_destination_types[]
all_replies_to_my_answers_on_watched_markets: notification_destination_types[]
all_comments_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
// Answers
answers_by_followed_users_on_watched_markets: notification_destination_types[]
answers_by_market_creator_on_watched_markets: notification_destination_types[]
all_answers_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
// On users' markets
your_contract_closed: notification_destination_types[]
all_comments_on_my_markets: notification_destination_types[]
all_answers_on_my_markets: notification_destination_types[]
subsidized_your_market: notification_destination_types[]
// Market updates
resolutions_on_watched_markets: notification_destination_types[]
resolutions_on_watched_markets_with_shares_in: notification_destination_types[]
market_updates_on_watched_markets: notification_destination_types[]
market_updates_on_watched_markets_with_shares_in: notification_destination_types[]
probability_updates_on_watched_markets: notification_destination_types[]
// Balance Changes
loan_income: notification_destination_types[]
betting_streaks: notification_destination_types[]
referral_bonuses: notification_destination_types[]
unique_bettors_on_your_contract: notification_destination_types[]
tips_on_your_comments: notification_destination_types[]
tips_on_your_markets: notification_destination_types[]
limit_order_fills: notification_destination_types[]
// General
tagged_user: notification_destination_types[]
on_new_follow: notification_destination_types[]
contract_from_followed_user: notification_destination_types[]
trending_markets: notification_destination_types[]
profit_loss_updates: notification_destination_types[]
onboarding_flow: notification_destination_types[]
thank_you_for_purchases: notification_destination_types[]
badges_awarded: notification_destination_types[]
opt_out_all: notification_destination_types[]
// When adding a new notification preference, use add-new-notification-preference.ts to existing users
}
export const getDefaultNotificationPreferences = (
userId: string,
privateUser?: PrivateUser,
noEmails?: boolean
) => {
const constructPref = (browserIf: boolean, emailIf: boolean) => {
const browser = browserIf ? 'browser' : undefined
const email = noEmails ? undefined : emailIf ? 'email' : undefined
return filterDefined([browser, email]) as notification_destination_types[]
}
const defaults: notification_preferences = {
// Watched Markets
all_comments_on_watched_markets: constructPref(true, false),
all_answers_on_watched_markets: constructPref(true, false),
// Comments
tips_on_your_comments: constructPref(true, true),
comments_by_followed_users_on_watched_markets: constructPref(true, true),
all_replies_to_my_comments_on_watched_markets: constructPref(true, true),
all_replies_to_my_answers_on_watched_markets: constructPref(true, true),
all_comments_on_contracts_with_shares_in_on_watched_markets: constructPref(
true,
false
),
// Answers
answers_by_followed_users_on_watched_markets: constructPref(true, true),
answers_by_market_creator_on_watched_markets: constructPref(true, true),
all_answers_on_contracts_with_shares_in_on_watched_markets: constructPref(
true,
true
),
// On users' markets
your_contract_closed: constructPref(true, true), // High priority
all_comments_on_my_markets: constructPref(true, true),
all_answers_on_my_markets: constructPref(true, true),
subsidized_your_market: constructPref(true, true),
// Market updates
resolutions_on_watched_markets: constructPref(true, false),
market_updates_on_watched_markets: constructPref(true, false),
market_updates_on_watched_markets_with_shares_in: constructPref(
true,
false
),
resolutions_on_watched_markets_with_shares_in: constructPref(true, true),
//Balance Changes
loan_income: constructPref(true, false),
betting_streaks: constructPref(true, false),
referral_bonuses: constructPref(true, true),
unique_bettors_on_your_contract: constructPref(true, true),
tipped_comments_on_watched_markets: constructPref(true, true),
tips_on_your_markets: constructPref(true, true),
limit_order_fills: constructPref(true, false),
// General
tagged_user: constructPref(true, true),
on_new_follow: constructPref(true, true),
contract_from_followed_user: constructPref(true, true),
trending_markets: constructPref(false, true),
profit_loss_updates: constructPref(false, true),
probability_updates_on_watched_markets: constructPref(true, false),
thank_you_for_purchases: constructPref(false, false),
onboarding_flow: constructPref(false, false),
opt_out_all: [],
badges_awarded: constructPref(true, false),
}
return defaults
}
// Adding a new key:value here is optional, you can just use a key of notification_subscription_types
// You might want to add a key:value here if there will be multiple notification reasons that map to the same
// subscription type, i.e. 'comment_on_contract_you_follow' and 'comment_on_contract_with_users_answer' both map to
// 'all_comments_on_watched_markets' subscription type
// TODO: perhaps better would be to map notification_subscription_types to arrays of notification_reason_types
const notificationReasonToSubscriptionType: Partial<
Record<notification_reason_types, notification_preference>
> = {
you_referred_user: 'referral_bonuses',
user_joined_to_bet_on_your_market: 'referral_bonuses',
tip_received: 'tips_on_your_comments',
bet_fill: 'limit_order_fills',
user_joined_from_your_group_invite: 'referral_bonuses',
challenge_accepted: 'limit_order_fills',
betting_streak_incremented: 'betting_streaks',
liked_and_tipped_your_contract: 'tips_on_your_markets',
comment_on_your_contract: 'all_comments_on_my_markets',
answer_on_your_contract: 'all_answers_on_my_markets',
comment_on_contract_you_follow: 'all_comments_on_watched_markets',
answer_on_contract_you_follow: 'all_answers_on_watched_markets',
update_on_contract_you_follow: 'market_updates_on_watched_markets',
resolution_on_contract_you_follow: 'resolutions_on_watched_markets',
comment_on_contract_with_users_shares_in:
'all_comments_on_contracts_with_shares_in_on_watched_markets',
answer_on_contract_with_users_shares_in:
'all_answers_on_contracts_with_shares_in_on_watched_markets',
update_on_contract_with_users_shares_in:
'market_updates_on_watched_markets_with_shares_in',
resolution_on_contract_with_users_shares_in:
'resolutions_on_watched_markets_with_shares_in',
comment_on_contract_with_users_answer: 'all_comments_on_watched_markets',
update_on_contract_with_users_answer: 'market_updates_on_watched_markets',
resolution_on_contract_with_users_answer: 'resolutions_on_watched_markets',
answer_on_contract_with_users_answer: 'all_answers_on_watched_markets',
comment_on_contract_with_users_comment: 'all_comments_on_watched_markets',
answer_on_contract_with_users_comment: 'all_answers_on_watched_markets',
update_on_contract_with_users_comment: 'market_updates_on_watched_markets',
resolution_on_contract_with_users_comment: 'resolutions_on_watched_markets',
reply_to_users_answer: 'all_replies_to_my_answers_on_watched_markets',
reply_to_users_comment: 'all_replies_to_my_comments_on_watched_markets',
}
export const getNotificationDestinationsForUser = (
privateUser: PrivateUser,
// TODO: accept reasons array from most to least important and work backwards
reason: notification_reason_types | notification_preference
) => {
const notificationSettings = privateUser.notificationPreferences
const unsubscribeEndpoint = getFunctionUrl('unsubscribe')
try {
let destinations
let subscriptionType: notification_preference | undefined
if (Object.keys(notificationSettings).includes(reason)) {
subscriptionType = reason as notification_preference
destinations = notificationSettings[subscriptionType]
} else {
const key = reason as notification_reason_types
subscriptionType = notificationReasonToSubscriptionType[key]
destinations = subscriptionType
? notificationSettings[subscriptionType]
: []
}
const optOutOfAllSettings = notificationSettings['opt_out_all']
// Your market closure notifications are high priority, opt-out doesn't affect their delivery
const optedOutOfEmail =
optOutOfAllSettings.includes('email') &&
subscriptionType !== 'your_contract_closed'
const optedOutOfBrowser =
optOutOfAllSettings.includes('browser') &&
subscriptionType !== 'your_contract_closed'
return {
sendToEmail: destinations.includes('email') && !optedOutOfEmail,
sendToBrowser: destinations.includes('browser') && !optedOutOfBrowser,
unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings&section=${subscriptionType}`,
}
} catch (e) {
// Fail safely
console.log(
`couldn't get notification destinations for type ${reason} for user ${privateUser.id}`
)
return {
sendToEmail: false,
sendToBrowser: false,
unsubscribeUrl: '',
urlToManageThisNotification: '',
}
}
}

View File

@ -1,6 +1,4 @@
import { notification_preferences } from './user-notification-preferences'
import { ENV_CONFIG } from './envs/constants' import { ENV_CONFIG } from './envs/constants'
import { MarketCreatorBadge, ProvenCorrectBadge, StreakerBadge } from './badge'
export type User = { export type User = {
id: string id: string
@ -12,6 +10,7 @@ export type User = {
// For their user page // For their user page
bio?: string bio?: string
bannerUrl?: string
website?: string website?: string
twitterHandle?: string twitterHandle?: string
discordHandle?: string discordHandle?: string
@ -33,58 +32,37 @@ export type User = {
allTime: number allTime: number
} }
fractionResolvedCorrectly: number
nextLoanCached: number
followerCountCached: number followerCountCached: number
followedCategories?: string[] followedCategories?: string[]
homeSections?: string[]
referredByUserId?: string referredByUserId?: string
referredByContractId?: string referredByContractId?: string
referredByGroupId?: string referredByGroupId?: string
lastPingTime?: number lastPingTime?: number
shouldShowWelcome?: boolean
lastBetTime?: number
currentBettingStreak?: number
hasSeenContractFollowModal?: boolean
freeMarketsCreated?: number
isBannedFromPosting?: boolean
achievements: {
provenCorrect?: {
badges: ProvenCorrectBadge[]
}
marketCreator?: {
badges: MarketCreatorBadge[]
}
streaker?: {
badges: StreakerBadge[]
}
}
} }
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
// for sus users, i.e. multiple sign ups for same person
export const SUS_STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 10
export const REFERRAL_AMOUNT = ENV_CONFIG.referralBonus ?? 500
export type PrivateUser = { export type PrivateUser = {
id: string // same as User.id id: string // same as User.id
username: string // denormalized from User username: string // denormalized from User
email?: string email?: string
weeklyTrendingEmailSent?: boolean unsubscribedFromResolutionEmails?: boolean
weeklyPortfolioUpdateEmailSent?: boolean unsubscribedFromCommentEmails?: boolean
manaBonusEmailSent?: boolean unsubscribedFromAnswerEmails?: boolean
unsubscribedFromGenericEmails?: boolean
initialDeviceToken?: string initialDeviceToken?: string
initialIpAddress?: string initialIpAddress?: string
apiKey?: string apiKey?: string
notificationPreferences: notification_preferences notificationPreferences?: notification_subscribe_types
twitchInfo?: {
twitchName: string
controlToken: string
botEnabled?: boolean
needsRelinking?: boolean
}
} }
export type notification_subscribe_types = 'all' | 'less' | 'none'
export type PortfolioMetrics = { export type PortfolioMetrics = {
investmentValue: number investmentValue: number
balance: number balance: number
@ -93,15 +71,5 @@ export type PortfolioMetrics = {
userId: string userId: string
} }
export const MANIFOLD_USER_USERNAME = 'ManifoldMarkets' export const MANIFOLD_USERNAME = 'ManifoldMarkets'
export const MANIFOLD_USER_NAME = 'ManifoldMarkets'
export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png' export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png'
// TODO: remove. Hardcoding the strings would be better.
// Different views require different language.
export const BETTOR = ENV_CONFIG.bettor ?? 'trader'
export const BETTORS = ENV_CONFIG.bettor + 's' ?? 'traders'
export const PRESENT_BET = ENV_CONFIG.presentBet ?? 'trade'
export const PRESENT_BETS = ENV_CONFIG.presentBet + 's' ?? 'trades'
export const PAST_BET = ENV_CONFIG.pastBet ?? 'trade'
export const PAST_BETS = ENV_CONFIG.pastBet + 's' ?? 'trades'

View File

@ -1,40 +1,3 @@
import { isEqual } from 'lodash'
export function filterDefined<T>(array: (T | null | undefined)[]) { export function filterDefined<T>(array: (T | null | undefined)[]) {
return array.filter((item) => item !== null && item !== undefined) as T[] return array.filter((item) => item !== null && item !== undefined) as T[]
} }
export function buildArray<T>(
...params: (T | T[] | false | undefined | null)[]
) {
const array: T[] = []
for (const el of params) {
if (Array.isArray(el)) {
array.push(...el)
} else if (el) {
array.push(el)
}
}
return array
}
export function groupConsecutive<T, U>(xs: T[], key: (x: T) => U) {
if (!xs.length) {
return []
}
const result = []
let curr = { key: key(xs[0]), items: [xs[0]] }
for (const x of xs.slice(1)) {
const k = key(x)
if (!isEqual(key, curr.key)) {
result.push(curr)
curr = { key: k, items: [x] }
} else {
curr.items.push(x)
}
}
result.push(curr)
return result
}

View File

@ -1,24 +0,0 @@
export const interpolateColor = (color1: string, color2: string, p: number) => {
const rgb1 = parseInt(color1.replace('#', ''), 16)
const rgb2 = parseInt(color2.replace('#', ''), 16)
const [r1, g1, b1] = toArray(rgb1)
const [r2, g2, b2] = toArray(rgb2)
const q = 1 - p
const rr = Math.round(r1 * q + r2 * p)
const rg = Math.round(g1 * q + g2 * p)
const rb = Math.round(b1 * q + b2 * p)
const hexString = Number((rr << 16) + (rg << 8) + rb).toString(16)
const hex = `#${'0'.repeat(6 - hexString.length)}${hexString}`
return hex
}
function toArray(rgb: number) {
const r = rgb >> 16
const g = (rgb >> 8) % 256
const b = rgb % 256
return [r, g, b]
}

View File

@ -8,14 +8,7 @@ const formatter = new Intl.NumberFormat('en-US', {
}) })
export function formatMoney(amount: number) { export function formatMoney(amount: number) {
const newAmount = const newAmount = Math.round(amount) === 0 ? 0 : Math.floor(amount) // handle -0 case
// handle -0 case
Math.round(amount) === 0
? 0
: // Handle 499.9999999999999 case
(amount > 0 ? Math.floor : Math.ceil)(
amount + 0.00000000001 * Math.sign(amount)
)
return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '') return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
} }
@ -60,16 +53,6 @@ export function formatLargeNumber(num: number, sigfigs = 2): string {
return `${numStr}${suffix[i] ?? ''}` return `${numStr}${suffix[i] ?? ''}`
} }
export function shortFormatNumber(num: number): string {
if (num < 1000) return showPrecision(num, 3)
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
const i = Math.floor(Math.log10(num) / 3)
const numStr = showPrecision(num / Math.pow(10, 3 * i), 2)
return `${numStr}${suffix[i] ?? ''}`
}
export function toCamelCase(words: string) { export function toCamelCase(words: string) {
const camelCase = words const camelCase = words
.split(' ') .split(' ')

View File

@ -1,6 +1,6 @@
import { union } from 'lodash' import { union } from 'lodash'
export const removeUndefinedProps = <T extends object>(obj: T): T => { export const removeUndefinedProps = <T>(obj: T): T => {
const newObj: any = {} const newObj: any = {}
for (const key of Object.keys(obj)) { for (const key of Object.keys(obj)) {
@ -37,3 +37,4 @@ export const subtractObjects = <T extends { [key: string]: number }>(
return newObj as T return newObj as T
} }

View File

@ -1,5 +1,5 @@
import { generateText, JSONContent, Node } from '@tiptap/core' import { MAX_TAG_LENGTH } from '../contract'
import { generateJSON } from '@tiptap/html' import { generateText, JSONContent } from '@tiptap/core'
// Tiptap starter extensions // Tiptap starter extensions
import { Blockquote } from '@tiptap/extension-blockquote' import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold' import { Bold } from '@tiptap/extension-bold'
@ -20,17 +20,33 @@ import { Text } from '@tiptap/extension-text'
// other tiptap extensions // other tiptap extensions
import { Image } from '@tiptap/extension-image' import { Image } from '@tiptap/extension-image'
import { Link } from '@tiptap/extension-link' import { Link } from '@tiptap/extension-link'
import { Mention } from '@tiptap/extension-mention'
import Iframe from './tiptap-iframe'
import TiptapTweet from './tiptap-tweet-type'
import { find } from 'linkifyjs'
import { uniq } from 'lodash'
import { TiptapSpoiler } from './tiptap-spoiler'
/** get first url in text. like "notion.so " -> "http://notion.so"; "notion" -> null */ export function parseTags(text: string) {
export function getUrl(text: string) { const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
const results = find(text, 'url') const matches = (text.match(regex) || []).map((match) =>
return results.length ? results[0].href : null match.trim().substring(1).substring(0, MAX_TAG_LENGTH)
)
const tagSet = new Set()
const uniqueTags: string[] = []
// Keep casing of last tag.
matches.reverse()
for (const tag of matches) {
const lowercase = tag.toLowerCase()
if (!tagSet.has(lowercase)) {
tagSet.add(lowercase)
uniqueTags.push(tag)
}
}
uniqueTags.reverse()
return uniqueTags
}
export function parseWordsAsTags(text: string) {
const taggedText = text
.split(/\s+/)
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
.join(' ')
return parseTags(taggedText)
} }
// TODO: fuzzy matching // TODO: fuzzy matching
@ -43,37 +59,8 @@ const checkAgainstQuery = (query: string, corpus: string) =>
export const searchInAny = (query: string, ...fields: string[]) => export const searchInAny = (query: string, ...fields: string[]) =>
fields.some((field) => checkAgainstQuery(query, field)) fields.some((field) => checkAgainstQuery(query, field))
/** @return user ids of all \@mentions */ // can't just do [StarterKit, Image...] because it doesn't work with cjs imports
export function parseMentions(data: JSONContent): string[] { export const exhibitExts = [
const mentions = data.content?.flatMap(parseMentions) ?? [] //dfs
if (data.type === 'mention' && data.attrs) {
mentions.push(data.attrs.id as string)
}
return uniq(mentions)
}
// TODO: this is a hack to get around the fact that tiptap doesn't have a
// way to add a node view without bundling in tsx
function skippableComponent(name: string): Node<any, any> {
return Node.create({
name,
group: 'block',
content: 'inline*',
parseHTML() {
return [
{
tag: 'grid-cards-component',
},
]
},
})
}
const stringParseExts = [
// StarterKit extensions
Blockquote, Blockquote,
Bold, Bold,
BulletList, BulletList,
@ -90,26 +77,12 @@ const stringParseExts = [
Paragraph, Paragraph,
Strike, Strike,
Text, Text,
// other extensions
Image,
Link, Link,
Image.extend({ renderText: () => '[image]' }),
Mention, // user @mention
Mention.extend({ name: 'contract-mention' }), // market %mention
Iframe.extend({
renderText: ({ node }) =>
'[embed]' + node.attrs.src ? `(${node.attrs.src})` : '',
}),
skippableComponent('gridCardsComponent'),
skippableComponent('staticReactEmbedComponent'),
TiptapTweet.extend({ renderText: () => '[tweet]' }),
TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }),
] ]
// export const exhibitExts = [StarterKit as unknown as Extension, Image]
export function richTextToString(text?: JSONContent) { export function richTextToString(text?: JSONContent) {
if (!text) return '' return !text ? '' : generateText(text, exhibitExts)
return generateText(text, stringParseExts)
}
export function htmlToRichText(html: string) {
return generateJSON(html, stringParseExts)
} }

View File

@ -46,10 +46,3 @@ export const shuffle = (array: unknown[], rand: () => number) => {
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]] ;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]
} }
} }
export function chooseRandomSubset<T>(items: T[], count: number) {
const fiveMinutes = 5 * 60 * 1000
const seed = Math.round(Date.now() / fiveMinutes).toString()
shuffle(items, createRNG(seed))
return items.slice(0, count)
}

View File

@ -1,6 +1,2 @@
export const MINUTE_MS = 60 * 1000 export const HOUR_MS = 60 * 60 * 1000
export const HOUR_MS = 60 * MINUTE_MS
export const DAY_MS = 24 * HOUR_MS export const DAY_MS = 24 * HOUR_MS
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))

View File

@ -1,100 +0,0 @@
// Adopted from https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/iframe.ts
import { Node } from '@tiptap/core'
export interface IframeOptions {
allowFullscreen: boolean
HTMLAttributes: {
[key: string]: any
}
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
iframe: {
setIframe: (options: { src: string }) => ReturnType
}
}
}
// These classes style the outer wrapper and the inner iframe;
// Adopted from css in https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/index.vue
const wrapperClasses = 'relative h-auto w-full overflow-hidden'
const iframeClasses = 'absolute top-0 left-0 h-full w-full'
export default Node.create<IframeOptions>({
name: 'iframe',
group: 'block',
atom: true,
addOptions() {
return {
allowFullscreen: true,
HTMLAttributes: {
class: 'iframe-wrapper' + ' ' + wrapperClasses,
// Tailwind JIT doesn't seem to pick up `pb-[20rem]`, so we hack this in:
style: 'padding-bottom: 20rem; ',
},
}
},
addAttributes() {
return {
src: {
default: null,
},
frameborder: {
default: 0,
},
height: {
default: 0,
},
allowfullscreen: {
default: this.options.allowFullscreen,
parseHTML: () => this.options.allowFullscreen,
},
}
},
parseHTML() {
return [{ tag: 'iframe' }]
},
renderHTML({ HTMLAttributes }) {
this.options.HTMLAttributes.style =
this.options.HTMLAttributes.style +
' height: ' +
HTMLAttributes.height +
';'
return [
'div',
this.options.HTMLAttributes,
[
'iframe',
{
...HTMLAttributes,
class: HTMLAttributes.class + ' ' + iframeClasses,
},
],
]
},
addCommands() {
return {
setIframe:
(options: { src: string }) =>
({ tr, dispatch }) => {
const { selection } = tr
const node = this.type.create(options)
if (dispatch) {
tr.replaceRangeWith(selection.from, selection.to, node)
}
return true
},
}
},
})

View File

@ -1,116 +0,0 @@
// adapted from @n8body/tiptap-spoiler
import {
Mark,
markInputRule,
markPasteRule,
mergeAttributes,
} from '@tiptap/core'
import type { ElementType } from 'react'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
spoilerEditor: {
setSpoiler: () => ReturnType
toggleSpoiler: () => ReturnType
unsetSpoiler: () => ReturnType
}
}
}
export type SpoilerOptions = {
HTMLAttributes: Record<string, any>
spoilerOpenClass: string
spoilerCloseClass?: string
inputRegex: RegExp
pasteRegex: RegExp
as: ElementType
}
const spoilerInputRegex = /(?:^|\s)((?:\|\|)((?:[^||]+))(?:\|\|))$/
const spoilerPasteRegex = /(?:^|\s)((?:\|\|)((?:[^||]+))(?:\|\|))/g
export const TiptapSpoiler = Mark.create<SpoilerOptions>({
name: 'spoiler',
inline: true,
group: 'inline',
inclusive: false,
exitable: true,
content: 'inline*',
priority: 1001, // higher priority than other formatting so they go inside
addOptions() {
return {
HTMLAttributes: { 'aria-label': 'spoiler' },
spoilerOpenClass: '',
spoilerCloseClass: undefined,
inputRegex: spoilerInputRegex,
pasteRegex: spoilerPasteRegex,
as: 'span',
editing: false,
}
},
addCommands() {
return {
setSpoiler:
() =>
({ commands }) =>
commands.setMark(this.name),
toggleSpoiler:
() =>
({ commands }) =>
commands.toggleMark(this.name),
unsetSpoiler:
() =>
({ commands }) =>
commands.unsetMark(this.name),
}
},
addInputRules() {
return [
markInputRule({
find: this.options.inputRegex,
type: this.type,
}),
]
},
addPasteRules() {
return [
markPasteRule({
find: this.options.pasteRegex,
type: this.type,
}),
]
},
parseHTML() {
return [
{
tag: 'span',
getAttrs: (node) =>
(node as HTMLElement).ariaLabel?.toLowerCase() === 'spoiler' && null,
},
]
},
renderHTML({ HTMLAttributes }) {
const elem = document.createElement(this.options.as as string)
Object.entries(
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
class: this.options.spoilerCloseClass ?? this.options.spoilerOpenClass,
})
).forEach(([attr, val]) => elem.setAttribute(attr, val))
elem.addEventListener('click', () => {
elem.setAttribute('class', this.options.spoilerOpenClass)
})
return elem
},
})

View File

@ -1,37 +0,0 @@
import { Node, mergeAttributes } from '@tiptap/core'
export interface TweetOptions {
tweetId: string
}
// This is a version of the Tiptap Node config without addNodeView,
// since that would require bundling in tsx
export const TiptapTweetNode = {
name: 'tiptapTweet',
group: 'block',
atom: true,
addAttributes() {
return {
tweetId: {
default: null,
},
}
},
parseHTML() {
return [
{
tag: 'tiptap-tweet',
},
]
},
renderHTML(props: { HTMLAttributes: Record<string, any> }) {
return ['tiptap-tweet', mergeAttributes(props.HTMLAttributes)]
},
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export default Node.create<TweetOptions>(TiptapTweetNode)

43
dev.sh
View File

@ -1,43 +0,0 @@
#!/bin/bash
ENV=${1:-dev}
case $ENV in
dev)
FIREBASE_PROJECT=dev
NEXT_ENV=DEV ;;
prod)
FIREBASE_PROJECT=prod
NEXT_ENV=PROD ;;
localdb)
FIREBASE_PROJECT=dev
NEXT_ENV=DEV
EMULATOR=true ;;
*)
echo "Invalid environment; must be dev, prod, or localdb."
exit 1
esac
firebase use $FIREBASE_PROJECT
if [ ! -z $EMULATOR ]
then
npx concurrently \
-n FIRESTORE,FUNCTIONS,NEXT,TS \
-c green,white,magenta,cyan \
"yarn --cwd=functions localDbScript" \
"cross-env FIRESTORE_EMULATOR_HOST=localhost:8080 yarn --cwd=functions dev" \
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
NEXT_PUBLIC_FIREBASE_EMULATE=TRUE \
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
yarn --cwd=web serve" \
"cross-env yarn --cwd=web ts-watch"
else
npx concurrently \
-n FUNCTIONS,NEXT,TS \
-c white,magenta,cyan \
"yarn --cwd=functions dev" \
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
yarn --cwd=web serve" \
"cross-env yarn --cwd=web ts-watch"
fi

View File

@ -46,42 +46,6 @@ Gets a user by their unique ID. Many other API endpoints return this as the `use
Requires no authorization. Requires no authorization.
### GET /v0/me
Returns the authenticated user.
### `GET /v0/groups`
Gets all groups, in no particular order.
Parameters:
- `availableToUserId`: Optional. if specified, only groups that the user can
join and groups they've already joined will be returned.
Requires no authorization.
### `GET /v0/group/[slug]`
Gets a group by its slug.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]`
Gets a group by its unique ID.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]/markets`
Gets a group's markets by its unique ID.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/markets` ### `GET /v0/markets`
Lists all markets, ordered by creation date descending. Lists all markets, ordered by creation date descending.
@ -111,6 +75,7 @@ Requires no authorization.
"creatorAvatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c", "creatorAvatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c",
"closeTime":1653893940000, "closeTime":1653893940000,
"question":"Will I write a new blog post today?", "question":"Will I write a new blog post today?",
"description":"I'm supposed to, or else Beeminder charges me $90.\nTentative topic ideas:\n- \"Manifold funding, a history\"\n- \"Markets and bounties allow trades through time\"\n- \"equity vs money vs time\"\n\nClose date updated to 2022-05-29 11:59 pm",
"tags":[ "tags":[
"personal", "personal",
"commitments" "commitments"
@ -148,6 +113,7 @@ Requires no authorization.
// Market attributes. All times are in milliseconds since epoch // Market attributes. All times are in milliseconds since epoch
closeTime?: number // Min of creator's chosen date, and resolutionTime closeTime?: number // Min of creator's chosen date, and resolutionTime
question: string question: string
description: string
// A list of tags on each market. Any user can add tags to any market. // A list of tags on each market. Any user can add tags to any market.
// This list also includes the predefined categories shown as filters on the home page. // This list also includes the predefined categories shown as filters on the home page.
@ -158,16 +124,13 @@ Requires no authorization.
// i.e. https://manifold.markets/Austin/test-market is the same as https://manifold.markets/foo/test-market // i.e. https://manifold.markets/Austin/test-market is the same as https://manifold.markets/foo/test-market
url: string url: string
outcomeType: string // BINARY, FREE_RESPONSE, MULTIPLE_CHOICE, NUMERIC, or PSEUDO_NUMERIC outcomeType: string // BINARY, FREE_RESPONSE, or NUMERIC
mechanism: string // dpm-2 or cpmm-1 mechanism: string // dpm-2 or cpmm-1
probability: number probability: number
pool: { outcome: number } // For CPMM markets, the number of shares in the liquidity pool. For DPM markets, the amount of mana invested in each answer. pool: { outcome: number } // For CPMM markets, the number of shares in the liquidity pool. For DPM markets, the amount of mana invested in each answer.
p?: number // CPMM markets only, probability constant in y^p * n^(1-p) = k p?: number // CPMM markets only, probability constant in y^p * n^(1-p) = k
totalLiquidity?: number // CPMM markets only, the amount of mana deposited into the liquidity pool totalLiquidity?: number // CPMM markets only, the amount of mana deposited into the liquidity pool
min?: number // PSEUDO_NUMERIC markets only, the minimum resolvable value
max?: number // PSEUDO_NUMERIC markets only, the maximum resolvable value
isLogScale?: bool // PSEUDO_NUMERIC markets only, if true `number = (max - min + 1)^probability + minstart - 1`, otherwise `number = min + (max - min) * probability`
volume: number volume: number
volume7Days: number volume7Days: number
@ -177,8 +140,6 @@ Requires no authorization.
resolutionTime?: number resolutionTime?: number
resolution?: string resolution?: string
resolutionProbability?: number // Used for BINARY markets resolved to MKT resolutionProbability?: number // Used for BINARY markets resolved to MKT
lastUpdatedTime?: number
} }
``` ```
@ -411,9 +372,7 @@ Requires no authorization.
type FullMarket = LiteMarket & { type FullMarket = LiteMarket & {
bets: Bet[] bets: Bet[]
comments: Comment[] comments: Comment[]
answers?: Answer[] // dpm-2 markets only answers?: Answer[]
description: JSONContent // Rich text content. See https://tiptap.dev/guide/output#option-1-json
textDescription: string // string description without formatting, images, or embeds
} }
type Bet = { type Bet = {
@ -522,20 +481,6 @@ Parameters:
answer. For numeric markets, this is a string representing the target bucket, answer. For numeric markets, this is a string representing the target bucket,
and an additional `value` parameter is required which is a number representing and an additional `value` parameter is required which is a number representing
the target value. (Bet on numeric markets at your own peril.) the target value. (Bet on numeric markets at your own peril.)
- `limitProb`: Optional. A number between `0.001` and `0.999` inclusive representing
the limit probability for your bet (i.e. 0.1% to 99.9% — multiply by 100 for the
probability percentage).
The bet will execute immediately in the direction of `outcome`, but not beyond this
specified limit. If not all the bet is filled, the bet will remain as an open offer
that can later be matched against an opposite direction bet.
- For example, if the current market probability is `50%`:
- A `M$10` bet on `YES` with `limitProb=0.4` would not be filled until the market
probability moves down to `40%` and someone bets `M$15` of `NO` to match your
bet odds.
- A `M$100` bet on `YES` with `limitProb=0.6` would fill partially or completely
depending on current unfilled limit bets and the AMM's liquidity. Any remaining
portion of the bet not filled would remain to be matched against in the future.
- An unfilled limit order bet can be cancelled using the cancel API.
Example request: Example request:
@ -547,20 +492,15 @@ $ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application
"contractId":"{...}"}' "contractId":"{...}"}'
``` ```
### `POST /v0/bet/cancel/[id]`
Cancel the limit order of a bet with the specified id. If the bet was unfilled, it will be cancelled so that no other bets will match with it. This is action irreversable.
### `POST /v0/market` ### `POST /v0/market`
Creates a new market on behalf of the authorized user. Creates a new market on behalf of the authorized user.
Parameters: Parameters:
- `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, `MULTIPLE_CHOICE`, or `PSEUDO_NUMERIC`. - `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, or `NUMERIC`.
- `question`: Required. The headline question for the market. - `question`: Required. The headline question for the market.
- `description`: Required. A long description describing the rules for the market. - `description`: Required. A long description describing the rules for the market.
- Note: string descriptions do **not** turn into links, mentions, formatted text. Instead, rich text descriptions must be in [TipTap json](https://tiptap.dev/guide/output#option-1-json).
- `closeTime`: Required. The time at which the market will close, represented as milliseconds since the epoch. - `closeTime`: Required. The time at which the market will close, represented as milliseconds since the epoch.
- `tags`: Optional. An array of string tags for the market. - `tags`: Optional. An array of string tags for the market.
@ -572,12 +512,6 @@ For numeric markets, you must also provide:
- `min`: The minimum value that the market may resolve to. - `min`: The minimum value that the market may resolve to.
- `max`: The maximum value that the market may resolve to. - `max`: The maximum value that the market may resolve to.
- `isLogScale`: If true, your numeric market will increase exponentially from min to max.
- `initialValue`: An initial value for the market, between min and max, exclusive.
For multiple choice markets, you must also provide:
- `answers`: An array of strings, each of which will be a valid answer for the market.
Example request: Example request:
@ -591,18 +525,6 @@ $ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: applicat
"initialProb":25}' "initialProb":25}'
``` ```
### `POST /v0/market/[marketId]/add-liquidity`
Adds a specified amount of liquidity into the market.
- `amount`: Required. The amount of liquidity to add, in M$.
### `POST /v0/market/[marketId]/close`
Closes a market on behalf of the authorized user.
- `closeTime`: Optional. Milliseconds since the epoch to close the market at. If not provided, the market will be closed immediately. Cannot provide close time in past.
### `POST /v0/market/[marketId]/resolve` ### `POST /v0/market/[marketId]/resolve`
Resolves a market on behalf of the authorized user. Resolves a market on behalf of the authorized user.
@ -614,18 +536,15 @@ For binary markets:
- `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`. - `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`.
- `probabilityInt`: Optional. The probability to use for `MKT` resolution. - `probabilityInt`: Optional. The probability to use for `MKT` resolution.
For free response or multiple choice markets: For free response markets:
- `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the answer index. - `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the answer index.
- `resolutions`: An array of `{ answer, pct }` objects to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome. Note that the total weights must add to 100. - `resolutions`: An array of `{ answer, pct }` objects to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome.
For numeric markets: For numeric markets:
- `outcome`: Required. One of `CANCEL`, or a `number` indicating the selected numeric bucket ID. - `outcome`: Required. One of `CANCEL`, or a `number` indicating the selected numeric bucket ID.
- `value`: The value that the market may resolves to. - `value`: The value that the market may resolves to.
- `probabilityInt`: Required if `value` is present. Should be equal to
- If log scale: `log10(value - min + 1) / log10(max - min + 1)`
- Otherwise: `(value - min) / (max - min)`
Example request: Example request:
@ -660,37 +579,6 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
]}' ]}'
``` ```
### `POST /v0/market/[marketId]/sell`
Sells some quantity of shares in a binary market on behalf of the authorized user.
Parameters:
- `outcome`: Optional. One of `YES`, or `NO`. If you leave it off, and you only
own one kind of shares, you will sell that kind of shares.
- `shares`: Optional. The amount of shares to sell of the outcome given
above. If not provided, all the shares you own will be sold.
Example request:
```
$ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Key {...}' \
--data-raw '{"outcome": "YES", "shares": 10}'
```
### `POST /v0/comment`
Creates a comment in the specified market. Only supports top-level comments for now.
Parameters:
- `contractId`: Required. The ID of the market to comment on.
- `content`: The comment to post, formatted as [TipTap json](https://tiptap.dev/guide/output#option-1-json), OR
- `html`: The comment to post, formatted as an HTML string, OR
- `markdown`: The comment to post, formatted as a markdown string.
### `GET /v0/bets` ### `GET /v0/bets`
Gets a list of bets, ordered by creation date descending. Gets a list of bets, ordered by creation date descending.
@ -709,7 +597,7 @@ Requires no authorization.
- Example request - Example request
``` ```
https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-i-be-able-to-place-a-limit-ord https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-california-abolish-daylight-sa
``` ```
- Response type: A `Bet[]`. - Response type: A `Bet[]`.
@ -717,60 +605,31 @@ Requires no authorization.
```json ```json
[ [
// Limit bet, partially filled.
{ {
"isFilled": false, "probAfter": 0.44418877319153904,
"amount": 15.596681605353808, "shares": -645.8346334931828,
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
"contractId": "Tz5dA01GkK5QKiQfZeDL",
"probBefore": 0.5730753474948571,
"isCancelled": false,
"outcome": "YES", "outcome": "YES",
"fees": { "creatorFee": 0, "liquidityFee": 0, "platformFee": 0 }, "contractId": "tgB1XmvFXZNhjr3xMNLp",
"shares": 31.193363210707616, "sale": {
"limitProb": 0.5, "betId": "RcOtarI3d1DUUTjiE0rx",
"id": "yXB8lVbs86TKkhWA1FVi", "amount": 474.9999999999998
"loanAmount": 0,
"orderAmount": 100,
"probAfter": 0.5730753474948571,
"createdTime": 1659482775970,
"fills": [
{
"timestamp": 1659483249648,
"matchedBetId": "MfrMd5HTiGASDXzqibr7",
"amount": 15.596681605353808,
"shares": 31.193363210707616
}
]
}, },
// Normal bet (no limitProb specified). "createdTime": 1644602886293,
{ "userId": "94YYTk1AFWfbWMpfYcvnnwI1veP2",
"shares": 17.350459904608414, "probBefore": 0.7229189477449224,
"probBefore": 0.5304358279113885, "id": "x9eNmCaqQeXW8AgJ8Zmp",
"isFilled": true, "amount": -499.9999999999998
"probAfter": 0.5730753474948571,
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
"amount": 10,
"contractId": "Tz5dA01GkK5QKiQfZeDL",
"id": "1LPJHNz5oAX4K6YtJlP1",
"fees": {
"platformFee": 0,
"liquidityFee": 0,
"creatorFee": 0.4251333951457593
}, },
"isCancelled": false,
"loanAmount": 0,
"orderAmount": 10,
"fills": [
{ {
"amount": 10, "probAfter": 0.9901970375647697,
"matchedBetId": null, "contractId": "zdeaYVAfHlo9jKzWh57J",
"shares": 17.350459904608414, "outcome": "YES",
"timestamp": 1659482757271 "amount": 1,
} "id": "8PqxKYwXCcLYoXy2m2Nm",
], "shares": 1.0049875638533763,
"createdTime": 1659482757271, "userId": "94YYTk1AFWfbWMpfYcvnnwI1veP2",
"outcome": "YES" "probBefore": 0.9900000000000001,
"createdTime": 1644705818872
} }
] ]
``` ```
@ -780,7 +639,6 @@ Requires no authorization.
## Changelog ## Changelog
- 2022-09-24: Expand market POST docs to include new market types (`PSEUDO_NUMERIC`, `MULTIPLE_CHOICE`)
- 2022-07-15: Add user by username and user by ID APIs - 2022-07-15: Add user by username and user by ID APIs
- 2022-06-08: Add paging to markets endpoint - 2022-06-08: Add paging to markets endpoint
- 2022-06-05: Add new authorized write endpoints - 2022-06-05: Add new authorized write endpoints

View File

@ -8,40 +8,15 @@ A list of community-created projects built on, or related to, Manifold Markets.
## Sites using Manifold ## Sites using Manifold
- [WagerWith.me](https://www.wagerwith.me/) — Bet with your friends, with full Manifold integration to bet with M$. - [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government
- [Alignment Markets](https://alignmentmarkets.com/) - Bet on the progress of benchmarks in ML safety! - [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
## API / Dev ## API / Dev
- [PyManifold](https://github.com/bcongdon/PyManifold) - Python client for the Manifold API - [PyManifold](https://github.com/bcongdon/PyManifold) - Python client for the Manifold API
- [PyManifold fork](https://github.com/gappleto97/PyManifold/) - Fork maintained by [@LivInTheLookingGlass](https://manifold.markets/LivInTheLookingGlass)
- [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics) - [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics)
- [ManifoldMarketManager](https://github.com/gappleto97/ManifoldMarketManager) - Python script and library to automatically manage markets
- [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets - [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets
- [manifold-sdk](https://github.com/keriwarr/manifold-sdk) - TypeScript/JavaScript client for the Manifold API
## Bots ## Bots
- [@manifold@botsin.space](https://botsin.space/@manifold) - Posts new Manifold markets to Mastodon - [@manifold@botsin.space](https://botsin.space/@manifold) - Posts new Manifold markets to Mastodon
- [James' Bot](https://github.com/manifoldmarkets/market-maker) — Simple trading bot that makes markets
- [mana](https://github.com/AnnikaCodes/mana) - A Discord bot for Manifold by [@arae](https://manifold.markets/arae)
## Writeups
- [Information Markets, Decision Markets, Attention Markets, Action Markets](https://astralcodexten.substack.com/p/information-markets-decision-markets) by Scott Alexander
- [Mismatched Monetary Motivation in Manifold Markets](https://kevin.zielnicki.com/2022/02/17/manifold/) by Kevin Zielnicki
- [Introducing the Salem/CSPI Forecasting Tournament](https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting) by Richard Hanania
- [What I learned about running a betting market game night contest](https://shakeddown.wordpress.com/2022/08/04/what-i-learned-about-running-a-betting-market-game-night-contest/) by shakeddown
- [Free-riding on prediction markets](https://pedunculate.substack.com/p/free-riding-on-prediction-markets) by John Roxton
## Art
- Folded origami and doodles by [@hamnox](https://manifold.markets/hamnox) ![](https://i.imgur.com/nVGY4pL.png)
- Laser-cut Foldy by [@wasabipesto](https://manifold.markets/wasabipesto) ![](https://i.imgur.com/g9S6v3P.jpg)
## Alumni
_These projects are no longer active, but were really really cool!_
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government

View File

@ -15,22 +15,6 @@ Our community is the beating heart of Manifold; your individual contributions ar
## Awarded bounties ## Awarded bounties
💥 *Awarded on 2022-10-07*
**[Pepe](https://manifold.markets/Pepe): M$10,000**
**[Jack](https://manifold.markets/jack): M$2,000**
**[Martin](https://manifold.markets/MartinRandall): M$2,000**
**[Yev](https://manifold.markets/Yev): M$2,000**
**[Michael](https://manifold.markets/MichaelWheatley): M$2,000**
- For discovering an infinite mana exploit using limit orders, and informing the Manifold team of it privately.
**[Matt](https://manifold.markets/MattP): M$5,000**
**[Adrian](https://manifold.markets/ahalekelly): M$5,000**
**[Yev](https://manifold.markets/Yev): M$5,000**
- For discovering an AMM liquidity exploit and informing the Manifold team of it privately.
🎈 *Awarded on 2022-06-14* 🎈 *Awarded on 2022-06-14*
**[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000** **[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000**

View File

@ -4,7 +4,11 @@
### Do I have to pay real money in order to participate? ### Do I have to pay real money in order to participate?
Nope! Each account starts with a free 1000 mana (or M$1000 for short). If you invest it wisely, you can increase your total without ever needing to put any real money into the site. Nope! Each account starts with a free M$1000. If you invest it wisely, you can increase your total without ever needing to put any real money into the site.
### What is the name for the currency Manifold uses, represented by M$?
Manifold Dollars, or mana for short.
### Can M$ be sold for real money? ### Can M$ be sold for real money?

View File

@ -30,8 +30,7 @@
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.17", "@docusaurus/module-type-aliases": "2.0.0-beta.17",
"@tsconfig/docusaurus": "^1.0.4", "@tsconfig/docusaurus": "^1.0.4"
"@types/react": "^17.0.2"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@ -2,30 +2,10 @@
"functions": { "functions": {
"predeploy": "cd functions && yarn build", "predeploy": "cd functions && yarn build",
"runtime": "nodejs16", "runtime": "nodejs16",
"source": "functions/dist", "source": "functions/dist"
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log"
]
}, },
"firestore": { "firestore": {
"rules": "firestore.rules", "rules": "firestore.rules",
"indexes": "firestore.indexes.json" "indexes": "firestore.indexes.json"
},
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"pubsub": {
"port": 8085
},
"ui": {
"enabled": true
}
} }
} }

View File

@ -22,20 +22,6 @@
} }
] ]
}, },
{
"collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "isFilled",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "bets", "collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP", "queryScope": "COLLECTION_GROUP",
@ -50,84 +36,6 @@
} }
] ]
}, },
{
"collectionGroup": "bets",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isCancelled",
"order": "ASCENDING"
},
{
"fieldPath": "isFilled",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "challenges",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "creatorId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "commentType",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "comments", "collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP", "queryScope": "COLLECTION_GROUP",
@ -170,42 +78,6 @@
} }
] ]
}, },
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "creatorId",
"order": "ASCENDING"
},
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "groupSlugs",
"arrayConfig": "CONTAINS"
},
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{ {
"collectionGroup": "contracts", "collectionGroup": "contracts",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -252,46 +124,6 @@
} }
] ]
}, },
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "closeTime",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{ {
"collectionGroup": "contracts", "collectionGroup": "contracts",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -664,28 +496,6 @@
} }
] ]
}, },
{
"collectionGroup": "comments",
"fieldPath": "contractId",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{ {
"collectionGroup": "comments", "collectionGroup": "comments",
"fieldPath": "createdTime", "fieldPath": "createdTime",

View File

@ -10,12 +10,7 @@ service cloud.firestore {
'akrolsmir@gmail.com', 'akrolsmir@gmail.com',
'jahooma@gmail.com', 'jahooma@gmail.com',
'taowell@gmail.com', 'taowell@gmail.com',
'abc.sinclair@gmail.com', 'manticmarkets@gmail.com'
'manticmarkets@gmail.com',
'iansphilips@gmail.com',
'd4vidchee@gmail.com',
'federicoruizcassarino@gmail.com',
'ingawei@gmail.com'
] ]
} }
@ -23,25 +18,19 @@ service cloud.firestore {
allow read; allow read;
} }
match /globalConfig/globalConfig {
allow read;
allow update: if isAdmin()
allow create: if isAdmin()
}
match /users/{userId} { match /users/{userId} {
allow read; allow read;
allow update: if userId == request.auth.uid allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['bio', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome', 'hasSeenContractFollowModal', 'homeSections']); .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime']);
// User referral rules // User referral rules
allow update: if userId == request.auth.uid allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['referredByUserId', 'referredByContractId', 'referredByGroupId']) .hasOnly(['referredByUserId', 'referredByContractId', 'referredByGroupId'])
// only one referral allowed per user // only one referral allowed per user
&& !("referredByUserId" in resource.data) && !("referredByUserId" in resource.data)
// user can't refer themselves // user can't refer themselves
&& !(userId == request.resource.data.referredByUserId); && !(resource.data.id == request.resource.data.referredByUserId);
// quid pro quos enabled (only once though so nbd) - bc I can't make this work: // quid pro quos enabled (only once though so nbd) - bc I can't make this work:
// && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id); // && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id);
} }
@ -50,45 +39,20 @@ service cloud.firestore {
allow read; allow read;
} }
match /{somePath=**}/contract-metrics/{contractId} {
allow read;
}
match /{somePath=**}/challenges/{challengeId}{
allow read;
}
match /contracts/{contractId}/follows/{userId} {
allow read;
allow create, delete: if userId == request.auth.uid;
}
match /contracts/{contractId}/challenges/{challengeId}{
allow read;
allow create: if request.auth.uid == request.resource.data.creatorId;
// allow update if there have been no claims yet and if the challenge is still open
allow update: if request.auth.uid == resource.data.creatorId;
}
match /users/{userId}/follows/{followUserId} { match /users/{userId}/follows/{followUserId} {
allow read; allow read;
allow write: if request.auth.uid == userId; allow write: if request.auth.uid == userId;
} }
match /users/{userId}/likes/{likeId} {
allow read;
allow write: if request.auth.uid == userId;
}
match /{somePath=**}/follows/{followUserId} { match /{somePath=**}/follows/{followUserId} {
allow read; allow read;
} }
match /private-users/{userId} { match /private-users/{userId} {
allow read: if userId == request.auth.uid || isAdmin(); allow read: if resource.data.id == request.auth.uid || isAdmin();
allow update: if (userId == request.auth.uid || isAdmin()) allow update: if (resource.data.id == request.auth.uid || isAdmin())
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']); .hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences' ]);
} }
match /private-users/{userId}/views/{viewId} { match /private-users/{userId}/views/{viewId} {
@ -110,9 +74,9 @@ service cloud.firestore {
match /contracts/{contractId} { match /contracts/{contractId} {
allow read; allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks', 'flaggedByUsernames']); .hasOnly(['tags', 'lowercaseTags', 'groupSlugs']);
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'closeTime', 'question', 'visibility', 'unlistedById']) .hasOnly(['description', 'closeTime', 'question'])
&& resource.data.creatorId == request.auth.uid; && resource.data.creatorId == request.auth.uid;
allow update: if isAdmin(); allow update: if isAdmin();
match /comments/{commentId} { match /comments/{commentId} {
@ -173,51 +137,24 @@ service cloud.firestore {
.hasOnly(['isSeen', 'viewTime']); .hasOnly(['isSeen', 'viewTime']);
} }
match /{somePath=**}/groupMembers/{memberId} {
allow read;
}
match /{somePath=**}/groupContracts/{contractId} {
allow read;
}
match /groups/{groupId} { match /groups/{groupId} {
allow read; allow read;
allow update: if (request.auth.uid == resource.data.creatorId || isAdmin()) allow update: if request.auth.uid == resource.data.creatorId
&& request.resource.data.diff(resource.data) && request.resource.data.diff(resource.data)
.affectedKeys() .affectedKeys()
.hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId', 'pinnedItems' ]); .hasOnly(['name', 'about', 'contractIds', 'memberIds', 'anyoneCanJoin' ]);
allow update: if (request.auth.uid in resource.data.memberIds || resource.data.anyoneCanJoin)
&& request.resource.data.diff(resource.data)
.affectedKeys()
.hasOnly([ 'contractIds', 'memberIds' ]);
allow delete: if request.auth.uid == resource.data.creatorId; allow delete: if request.auth.uid == resource.data.creatorId;
match /groupContracts/{contractId} { function isMember() {
allow write: if isGroupMember() || request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId return request.auth.uid in get(/databases/$(database)/documents/groups/$(groupId)).data.memberIds;
} }
match /groupMembers/{memberId}{
allow create: if request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId || (request.auth.uid == request.resource.data.userId && get(/databases/$(database)/documents/groups/$(groupId)).data.anyoneCanJoin);
allow delete: if request.auth.uid == resource.data.userId;
}
function isGroupMember() {
return exists(/databases/$(database)/documents/groups/$(groupId)/groupMembers/$(request.auth.uid));
}
match /comments/{commentId} { match /comments/{commentId} {
allow read; allow read;
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isGroupMember(); allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isMember();
}
}
match /posts/{postId} {
allow read;
allow update: if isAdmin() || request.auth.uid == resource.data.creatorId
&& request.resource.data.diff(resource.data)
.affectedKeys()
.hasOnly(['name', 'content']);
allow delete: if isAdmin() || request.auth.uid == resource.data.creatorId;
match /comments/{commentId} {
allow read;
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) ;
} }
} }
} }

View File

@ -1,3 +0,0 @@
# This sets which EnvConfig is deployed to Firebase Cloud Functions
NEXT_PUBLIC_FIREBASE_ENV=DEV

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
plugins: ['lodash', 'unused-imports'], plugins: ['lodash'],
extends: ['eslint:recommended'], extends: ['eslint:recommended'],
ignorePatterns: ['dist', 'lib'], ignorePatterns: ['dist', 'lib'],
env: { env: {
@ -26,7 +26,6 @@ module.exports = {
caughtErrorsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_',
}, },
], ],
'unused-imports/no-unused-imports': 'warn',
}, },
}, },
], ],

View File

@ -17,5 +17,4 @@ package-lock.json
ui-debug.log ui-debug.log
firebase-debug.log firebase-debug.log
firestore-debug.log firestore-debug.log
pubsub-debug.log
firestore_export/ firestore_export/

View File

@ -20,7 +20,7 @@ Adapted from https://firebase.google.com/docs/functions/get-started
3. `$ firebase login` to authenticate the CLI tools to Firebase 3. `$ firebase login` to authenticate the CLI tools to Firebase
4. `$ firebase use dev` to choose the dev project 4. `$ firebase use dev` to choose the dev project
#### (Installing) For local development ### For local development
0. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI 0. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
1. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`): 1. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`):
@ -35,10 +35,10 @@ Adapted from https://firebase.google.com/docs/functions/get-started
## Developing locally ## Developing locally
0. `$ ./dev.sh localdb` to start the local emulator and front end 0. `$ firebase use dev` if you haven't already
1. If you change db trigger code, you have to start (doesn't have to complete) the deploy of it to dev to cause a hard emulator code refresh `$ firebase deploy --only functions:dbTriggerNameHere` 1. `$ yarn serve` to spin up the emulators 0. The Emulator UI is at http://localhost:4000; the functions are hosted on :5001.
- There's surely a better way to cause/react to a db trigger update but just adding this here for now as it works Note: You have to kill and restart emulators when you change code; no hot reload =(
2. If you want to test a scheduled function replace your function in `test-scheduled-function.ts` and send a GET to `http://localhost:8088/testscheduledfunction` (Best user experience is via [Postman](https://www.postman.com/downloads/)!) 2. `$ yarn dev:emulate` in `/web` to connect to emulators with the frontend 0. Note: emulated database is cleared after every shutdown
## Firestore Commands ## Firestore Commands
@ -65,6 +65,5 @@ Adapted from https://firebase.google.com/docs/functions/get-started
Secrets are strings that shouldn't be checked into Git (eg API keys, passwords). We store these using [Google Secret Manager](https://console.cloud.google.com/security/secret-manager), which provides them as environment variables to functions that require them. Some useful workflows: Secrets are strings that shouldn't be checked into Git (eg API keys, passwords). We store these using [Google Secret Manager](https://console.cloud.google.com/security/secret-manager), which provides them as environment variables to functions that require them. Some useful workflows:
- Set a secret: `$ firebase functions:secrets:set STRIPE_APIKEY` - Set a secret: `$ firebase functions:secrets:set stripe.test_secret="THE-API-KEY"`
- Then, enter the secret in the prompt.
- Read a secret: `$ firebase functions:secrets:access STRIPE_APIKEY` - Read a secret: `$ firebase functions:secrets:access STRIPE_APIKEY`

View File

@ -5,19 +5,17 @@
"firestore": "dev-mantic-markets.appspot.com" "firestore": "dev-mantic-markets.appspot.com"
}, },
"scripts": { "scripts": {
"build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist && cp .env.prod dist && cp .env.dev dist", "build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist && cp .env dist",
"compile": "tsc -b", "compile": "tsc -b",
"watch": "tsc -w", "watch": "tsc -w",
"shell": "yarn build && firebase functions:shell", "shell": "yarn build && firebase functions:shell",
"start": "yarn shell", "start": "yarn shell",
"deploy": "firebase deploy --only functions", "deploy": "firebase deploy --only functions",
"logs": "firebase functions:log", "logs": "firebase functions:log",
"dev": "nodemon src/serve.ts", "serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export",
"localDbScript": "firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export", "db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
"db:update-local-from-remote": "yarn db:backup-remote && gsutil -m rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
"db:backup-local": "firebase emulators:export --force ./firestore_export", "db:backup-local": "firebase emulators:export --force ./firestore_export",
"db:rename-remote-backup-folder": "gsutil -m mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)", "db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)",
"db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/", "db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/",
"verify": "(cd .. && yarn verify)", "verify": "(cd .. && yarn verify)",
"verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty" "verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty"
@ -26,33 +24,22 @@
"dependencies": { "dependencies": {
"@amplitude/node": "1.10.0", "@amplitude/node": "1.10.0",
"@google-cloud/functions-framework": "3.1.2", "@google-cloud/functions-framework": "3.1.2",
"@tiptap/core": "2.0.0-beta.199", "@tiptap/core": "2.0.0-beta.181",
"@tiptap/extension-image": "2.0.0-beta.199", "@tiptap/extension-image": "2.0.0-beta.30",
"@tiptap/extension-link": "2.0.0-beta.199", "@tiptap/extension-link": "2.0.0-beta.43",
"@tiptap/extension-mention": "2.0.0-beta.199", "@tiptap/starter-kit": "2.0.0-beta.190",
"@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"cors": "2.8.5",
"dayjs": "1.11.4",
"express": "4.18.1",
"firebase-admin": "10.0.0", "firebase-admin": "10.0.0",
"firebase-functions": "3.21.2", "firebase-functions": "3.21.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"mailgun-js": "0.22.0", "mailgun-js": "0.22.0",
"marked": "4.1.1",
"module-alias": "2.2.2", "module-alias": "2.2.2",
"node-fetch": "2",
"stripe": "8.194.0", "stripe": "8.194.0",
"zod": "3.17.2" "zod": "3.17.2"
}, },
"devDependencies": { "devDependencies": {
"@types/mailgun-js": "0.22.12", "@types/mailgun-js": "0.22.12",
"@types/marked": "4.0.7",
"@types/module-alias": "2.0.1", "@types/module-alias": "2.0.1",
"@types/node-fetch": "2.6.2", "firebase-functions-test": "0.3.3"
"firebase-functions-test": "0.3.3",
"puppeteer": "18.0.5"
}, },
"private": true "private": true
} }

View File

@ -1,170 +0,0 @@
import { z } from 'zod'
import { APIError, newEndpoint, validate } from './api'
import { log } from './utils'
import { Contract, CPMMBinaryContract } from '../../common/contract'
import { User } from '../../common/user'
import * as admin from 'firebase-admin'
import { FieldValue } from 'firebase-admin/firestore'
import { removeUndefinedProps } from '../../common/util/object'
import { Acceptance, Challenge } from '../../common/challenge'
import { CandidateBet } from '../../common/new-bet'
import { createChallengeAcceptedNotification } from './create-notification'
import { noFees } from '../../common/fees'
import { formatMoney, formatPercent } from '../../common/util/format'
import { redeemShares } from './redeem-shares'
const bodySchema = z.object({
contractId: z.string(),
challengeSlug: z.string(),
outcomeType: z.literal('BINARY'),
closeTime: z.number().gte(Date.now()),
})
const firestore = admin.firestore()
export const acceptchallenge = newEndpoint({}, async (req, auth) => {
const { challengeSlug, contractId } = validate(bodySchema, req.body)
const result = await firestore.runTransaction(async (trans) => {
const contractDoc = firestore.doc(`contracts/${contractId}`)
const userDoc = firestore.doc(`users/${auth.uid}`)
const challengeDoc = firestore.doc(
`contracts/${contractId}/challenges/${challengeSlug}`
)
const [contractSnap, userSnap, challengeSnap] = await trans.getAll(
contractDoc,
userDoc,
challengeDoc
)
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
if (!userSnap.exists) throw new APIError(400, 'User not found.')
if (!challengeSnap.exists) throw new APIError(400, 'Challenge not found.')
const anyContract = contractSnap.data() as Contract
const user = userSnap.data() as User
const challenge = challengeSnap.data() as Challenge
if (challenge.acceptances.length > 0)
throw new APIError(400, 'Challenge already accepted.')
const creatorDoc = firestore.doc(`users/${challenge.creatorId}`)
const creatorSnap = await trans.get(creatorDoc)
if (!creatorSnap.exists) throw new APIError(400, 'Creator not found.')
const creator = creatorSnap.data() as User
const {
creatorAmount,
acceptorOutcome,
creatorOutcome,
creatorOutcomeProb,
acceptorAmount,
} = challenge
if (user.balance < acceptorAmount)
throw new APIError(400, 'Insufficient balance.')
if (creator.balance < creatorAmount)
throw new APIError(400, 'Creator has insufficient balance.')
const contract = anyContract as CPMMBinaryContract
const shares = (1 / creatorOutcomeProb) * creatorAmount
const createdTime = Date.now()
const probOfYes =
creatorOutcome === 'YES' ? creatorOutcomeProb : 1 - creatorOutcomeProb
log(
'Creating challenge bet for',
user.username,
shares,
acceptorOutcome,
'shares',
'at',
formatPercent(creatorOutcomeProb),
'for',
formatMoney(acceptorAmount)
)
const yourNewBet: CandidateBet = removeUndefinedProps({
orderAmount: acceptorAmount,
amount: acceptorAmount,
shares,
isCancelled: false,
contractId: contract.id,
outcome: acceptorOutcome,
probBefore: probOfYes,
probAfter: probOfYes,
loanAmount: 0,
createdTime,
fees: noFees,
challengeSlug: challenge.slug,
})
const yourNewBetDoc = contractDoc.collection('bets').doc()
trans.create(yourNewBetDoc, {
id: yourNewBetDoc.id,
userId: user.id,
...yourNewBet,
})
trans.update(userDoc, { balance: FieldValue.increment(-yourNewBet.amount) })
const creatorNewBet: CandidateBet = removeUndefinedProps({
orderAmount: creatorAmount,
amount: creatorAmount,
shares,
isCancelled: false,
contractId: contract.id,
outcome: creatorOutcome,
probBefore: probOfYes,
probAfter: probOfYes,
loanAmount: 0,
createdTime,
fees: noFees,
challengeSlug: challenge.slug,
})
const creatorBetDoc = contractDoc.collection('bets').doc()
trans.create(creatorBetDoc, {
id: creatorBetDoc.id,
userId: creator.id,
...creatorNewBet,
})
trans.update(creatorDoc, {
balance: FieldValue.increment(-creatorNewBet.amount),
})
const volume = contract.volume + yourNewBet.amount + creatorNewBet.amount
trans.update(contractDoc, { volume })
trans.update(
challengeDoc,
removeUndefinedProps({
acceptedByUserIds: [user.id],
acceptances: [
{
userId: user.id,
betId: yourNewBetDoc.id,
createdTime,
amount: acceptorAmount,
userUsername: user.username,
userName: user.name,
userAvatarUrl: user.avatarUrl,
} as Acceptance,
],
})
)
await createChallengeAcceptedNotification(
user,
creator,
challenge,
acceptorAmount,
contract
)
log('Done, sent notification.')
return yourNewBetDoc
})
await redeemShares(auth.uid, contractId)
return { betId: result.id }
})

View File

@ -1,8 +1,9 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod' import { z } from 'zod'
import { Contract, CPMMContract } from '../../common/contract' import { Contract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
import { removeUndefinedProps } from '../../common/util/object'
import { getNewLiquidityProvision } from '../../common/add-liquidity' import { getNewLiquidityProvision } from '../../common/add-liquidity'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
@ -11,10 +12,10 @@ const bodySchema = z.object({
amount: z.number().gt(0), amount: z.number().gt(0),
}) })
export const addsubsidy = newEndpoint({}, async (req, auth) => { export const addliquidity = newEndpoint({}, async (req, auth) => {
const { amount, contractId } = validate(bodySchema, req.body) const { amount, contractId } = validate(bodySchema, req.body)
if (!isFinite(amount) || amount < 1) throw new APIError(400, 'Invalid amount') if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
// run as transaction to prevent race conditions // run as transaction to prevent race conditions
return await firestore.runTransaction(async (transaction) => { return await firestore.runTransaction(async (transaction) => {
@ -44,18 +45,29 @@ export const addsubsidy = newEndpoint({}, async (req, auth) => {
.collection(`contracts/${contractId}/liquidity`) .collection(`contracts/${contractId}/liquidity`)
.doc() .doc()
const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } = const { newLiquidityProvision, newPool, newP, newTotalLiquidity } =
getNewLiquidityProvision( getNewLiquidityProvision(
user.id, user,
amount, amount,
contract, contract,
newLiquidityProvisionDoc.id newLiquidityProvisionDoc.id
) )
transaction.update(contractDoc, { if (newP !== undefined && !isFinite(newP)) {
subsidyPool: newSubsidyPool, return {
status: 'error',
message: 'Liquidity injection rejected due to overflow error.',
}
}
transaction.update(
contractDoc,
removeUndefinedProps({
pool: newPool,
p: newP,
totalLiquidity: newTotalLiquidity, totalLiquidity: newTotalLiquidity,
} as Partial<CPMMContract>) })
)
const newBalance = user.balance - amount const newBalance = user.balance - amount
const newTotalDeposits = user.totalDeposits - amount const newTotalDeposits = user.totalDeposits - amount

View File

@ -3,7 +3,7 @@ import * as Amplitude from '@amplitude/node'
import { DEV_CONFIG } from '../../common/envs/dev' import { DEV_CONFIG } from '../../common/envs/dev'
import { PROD_CONFIG } from '../../common/envs/prod' import { PROD_CONFIG } from '../../common/envs/prod'
import { isProd, tryOrLogError } from './utils' import { isProd } from './utils'
const key = isProd() ? PROD_CONFIG.amplitudeApiKey : DEV_CONFIG.amplitudeApiKey const key = isProd() ? PROD_CONFIG.amplitudeApiKey : DEV_CONFIG.amplitudeApiKey
@ -15,12 +15,10 @@ export const track = async (
eventProperties?: any, eventProperties?: any,
amplitudeProperties?: Partial<Amplitude.Event> amplitudeProperties?: Partial<Amplitude.Event>
) => { ) => {
return await tryOrLogError( await amp.logEvent({
amp.logEvent({
event_type: eventName, event_type: eventName,
user_id: userId, user_id: userId,
event_properties: eventProperties, event_properties: eventProperties,
...amplitudeProperties, ...amplitudeProperties,
}) })
)
} }

View File

@ -1,7 +1,6 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { Request, RequestHandler, Response } from 'express' import { logger } from 'firebase-functions/v2'
import { error } from 'firebase-functions/logger' import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https'
import { HttpsOptions } from 'firebase-functions/v2/https'
import { log } from './utils' import { log } from './utils'
import { z } from 'zod' import { z } from 'zod'
import { APIError } from '../../common/api' import { APIError } from '../../common/api'
@ -14,7 +13,7 @@ import {
export { APIError } from '../../common/api' export { APIError } from '../../common/api'
type Output = Record<string, unknown> type Output = Record<string, unknown>
export type AuthedUser = { type AuthedUser = {
uid: string uid: string
creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser }) creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser })
} }
@ -23,8 +22,13 @@ type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
type KeyCredentials = { kind: 'key'; data: string } type KeyCredentials = { kind: 'key'; data: string }
type Credentials = JwtCredentials | KeyCredentials type Credentials = JwtCredentials | KeyCredentials
export const parseCredentials = async (req: Request): Promise<Credentials> => {
const auth = admin.auth() const auth = admin.auth()
const firestore = admin.firestore()
const privateUsers = firestore.collection(
'private-users'
) as admin.firestore.CollectionReference<PrivateUser>
export const parseCredentials = async (req: Request): Promise<Credentials> => {
const authHeader = req.get('Authorization') const authHeader = req.get('Authorization')
if (!authHeader) { if (!authHeader) {
throw new APIError(403, 'Missing Authorization header.') throw new APIError(403, 'Missing Authorization header.')
@ -41,7 +45,7 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
return { kind: 'jwt', data: await auth.verifyIdToken(payload) } return { kind: 'jwt', data: await auth.verifyIdToken(payload) }
} catch (err) { } catch (err) {
// This is somewhat suspicious, so get it into the firebase console // This is somewhat suspicious, so get it into the firebase console
error('Error verifying Firebase JWT: ', err) logger.error('Error verifying Firebase JWT: ', err)
throw new APIError(403, 'Error validating token.') throw new APIError(403, 'Error validating token.')
} }
case 'Key': case 'Key':
@ -52,8 +56,6 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
} }
export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => { export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
const firestore = admin.firestore()
const privateUsers = firestore.collection('private-users')
switch (creds.kind) { switch (creds.kind) {
case 'jwt': { case 'jwt': {
if (typeof creds.data.user_id !== 'string') { if (typeof creds.data.user_id !== 'string') {
@ -67,7 +69,7 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
if (privateUserQ.empty) { if (privateUserQ.empty) {
throw new APIError(403, `No private user exists with API key ${key}.`) throw new APIError(403, `No private user exists with API key ${key}.`)
} }
const privateUser = privateUserQ.docs[0].data() as PrivateUser const privateUser = privateUserQ.docs[0].data()
return { uid: privateUser.id, creds: { privateUser, ...creds } } return { uid: privateUser.id, creds: { privateUser, ...creds } }
} }
default: default:
@ -75,30 +77,12 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
} }
} }
export const writeResponseError = (e: unknown, res: Response) => {
if (e instanceof APIError) {
const output: { [k: string]: unknown } = { message: e.message }
if (e.details != null) {
output.details = e.details
}
res.status(e.code).json(output)
} else {
error(e)
res.status(500).json({ message: 'An unknown error occurred.' })
}
}
export const zTimestamp = () => { export const zTimestamp = () => {
return z.preprocess((arg) => { return z.preprocess((arg) => {
return typeof arg == 'number' ? new Date(arg) : undefined return typeof arg == 'number' ? new Date(arg) : undefined
}, z.date()) }, z.date())
} }
export type EndpointDefinition = {
opts: EndpointOptions & { method: string }
handler: RequestHandler
}
export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => { export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
const result = schema.safeParse(val) const result = schema.safeParse(val)
if (!result.success) { if (!result.success) {
@ -115,12 +99,12 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
} }
} }
export interface EndpointOptions extends HttpsOptions { interface EndpointOptions extends HttpsOptions {
method?: string methods?: string[]
} }
const DEFAULT_OPTS = { const DEFAULT_OPTS = {
method: 'POST', methods: ['POST'],
minInstances: 1, minInstances: 1,
concurrency: 100, concurrency: 100,
memory: '2GiB', memory: '2GiB',
@ -129,41 +113,28 @@ const DEFAULT_OPTS = {
} }
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => { export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts) const opts = Object.assign(endpointOpts, DEFAULT_OPTS)
return { return onRequest(opts, async (req, res) => {
opts, log('Request processing started.')
handler: async (req: Request, res: Response) => {
log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`)
try { try {
if (opts.method !== req.method) { if (!opts.methods.includes(req.method)) {
throw new APIError(405, `This endpoint supports only ${opts.method}.`) const allowed = opts.methods.join(', ')
throw new APIError(405, `This endpoint supports only ${allowed}.`)
} }
const authedUser = await lookupUser(await parseCredentials(req)) const authedUser = await lookupUser(await parseCredentials(req))
log('User credentials processed.')
res.status(200).json(await fn(req, authedUser)) res.status(200).json(await fn(req, authedUser))
} catch (e) { } catch (e) {
writeResponseError(e, res) if (e instanceof APIError) {
const output: { [k: string]: unknown } = { message: e.message }
if (e.details != null) {
output.details = e.details
} }
}, res.status(e.code).json(output)
} as EndpointDefinition } else {
logger.error(e)
res.status(500).json({ message: 'An unknown error occurred.' })
} }
export const newEndpointNoAuth = (
endpointOpts: EndpointOptions,
fn: (req: Request) => Promise<Output>
) => {
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
return {
opts,
handler: async (req: Request, res: Response) => {
log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`)
try {
if (opts.method !== req.method) {
throw new APIError(405, `This endpoint supports only ${opts.method}.`)
} }
res.status(200).json(await fn(req)) })
} catch (e) {
writeResponseError(e, res)
}
},
} as EndpointDefinition
} }

View File

@ -10,7 +10,7 @@ const bodySchema = z.object({
export const cancelbet = newEndpoint({}, async (req, auth) => { export const cancelbet = newEndpoint({}, async (req, auth) => {
const { betId } = validate(bodySchema, req.body) const { betId } = validate(bodySchema, req.body)
return await firestore.runTransaction(async (trans) => { const result = await firestore.runTransaction(async (trans) => {
const snap = await trans.get( const snap = await trans.get(
firestore.collectionGroup('bets').where('id', '==', betId) firestore.collectionGroup('bets').where('id', '==', betId)
) )
@ -28,6 +28,8 @@ export const cancelbet = newEndpoint({}, async (req, auth) => {
return { ...bet, isCancelled: true } return { ...bet, isCancelled: true }
}) })
return result
}) })
const firestore = admin.firestore() const firestore = admin.firestore()

View File

@ -2,7 +2,6 @@ import * as admin from 'firebase-admin'
import { z } from 'zod' import { z } from 'zod'
import { getUser } from './utils' import { getUser } from './utils'
import { Bet } from '../../common/bet'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { Comment } from '../../common/comment' import { Comment } from '../../common/comment'
import { User } from '../../common/user' import { User } from '../../common/user'
@ -38,56 +37,6 @@ export const changeUser = async (
avatarUrl?: string avatarUrl?: string
} }
) => { ) => {
// Update contracts, comments, and answers outside of a transaction to avoid contention.
// Using bulkWriter to supports >500 writes at a time
const contractsRef = firestore
.collection('contracts')
.where('creatorId', '==', user.id)
const contracts = await contractsRef.get()
const contractUpdate: Partial<Contract> = removeUndefinedProps({
creatorName: update.name,
creatorUsername: update.username,
creatorAvatarUrl: update.avatarUrl,
})
const commentSnap = await firestore
.collectionGroup('comments')
.where('userUsername', '==', user.username)
.get()
const commentUpdate: Partial<Comment> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
const answerSnap = await firestore
.collectionGroup('answers')
.where('username', '==', user.username)
.get()
const answerUpdate: Partial<Answer> = removeUndefinedProps(update)
const betsSnap = await firestore
.collectionGroup('bets')
.where('userId', '==', user.id)
.get()
const betsUpdate: Partial<Bet> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
const bulkWriter = firestore.bulkWriter()
commentSnap.docs.forEach((d) => bulkWriter.update(d.ref, commentUpdate))
answerSnap.docs.forEach((d) => bulkWriter.update(d.ref, answerUpdate))
contracts.docs.forEach((d) => bulkWriter.update(d.ref, contractUpdate))
betsSnap.docs.forEach((d) => bulkWriter.update(d.ref, betsUpdate))
await bulkWriter.flush()
console.log('Done writing!')
// Update the username inside a transaction
return await firestore.runTransaction(async (transaction) => { return await firestore.runTransaction(async (transaction) => {
if (update.username) { if (update.username) {
update.username = cleanUsername(update.username) update.username = cleanUsername(update.username)
@ -109,7 +58,42 @@ export const changeUser = async (
const userRef = firestore.collection('users').doc(user.id) const userRef = firestore.collection('users').doc(user.id)
const userUpdate: Partial<User> = removeUndefinedProps(update) const userUpdate: Partial<User> = removeUndefinedProps(update)
const contractsRef = firestore
.collection('contracts')
.where('creatorId', '==', user.id)
const contracts = await transaction.get(contractsRef)
const contractUpdate: Partial<Contract> = removeUndefinedProps({
creatorName: update.name,
creatorUsername: update.username,
creatorAvatarUrl: update.avatarUrl,
})
const commentSnap = await transaction.get(
firestore
.collectionGroup('comments')
.where('userUsername', '==', user.username)
)
const commentUpdate: Partial<Comment> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
const answerSnap = await transaction.get(
firestore
.collectionGroup('answers')
.where('username', '==', user.username)
)
const answerUpdate: Partial<Answer> = removeUndefinedProps(update)
transaction.update(userRef, userUpdate) transaction.update(userRef, userUpdate)
commentSnap.docs.forEach((d) => transaction.update(d.ref, commentUpdate))
answerSnap.docs.forEach((d) => transaction.update(d.ref, answerUpdate))
contracts.docs.forEach((d) => transaction.update(d.ref, contractUpdate))
}) })
} }

View File

@ -1,58 +0,0 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Contract } from '../../common/contract'
import { getUser } from './utils'
import { isAdmin, isManifoldId } from '../../common/envs/constants'
import { APIError, newEndpoint, validate } from './api'
const bodySchema = z.object({
contractId: z.string(),
closeTime: z.number().int().nonnegative().optional(),
})
export const closemarket = newEndpoint({}, async (req, auth) => {
const { contractId, closeTime } = validate(bodySchema, req.body)
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await contractDoc.get()
if (!contractSnap.exists)
throw new APIError(404, 'No contract exists with the provided ID')
const contract = contractSnap.data() as Contract
const { creatorId } = contract
const firebaseUser = await admin.auth().getUser(auth.uid)
if (
creatorId !== auth.uid &&
!isManifoldId(auth.uid) &&
!isAdmin(firebaseUser.email)
)
throw new APIError(403, 'User is not creator of contract')
const now = Date.now()
if (!closeTime && contract.closeTime && contract.closeTime < now)
throw new APIError(400, 'Contract already closed')
if (closeTime && closeTime < now)
throw new APIError(
400,
'Close time must be in the future. ' +
'Alternatively, do not provide a close time to close immediately.'
)
const creator = await getUser(creatorId)
if (!creator) throw new APIError(500, 'Creator not found')
const updatedContract = {
...contract,
closeTime: closeTime ? closeTime : now,
}
await contractDoc.update(updatedContract)
console.log('contract ', contractId, 'closed')
return updatedContract
})
const firestore = admin.firestore()

View File

@ -5,9 +5,9 @@ import { Contract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
import { getNewMultiBetInfo } from '../../common/new-bet' import { getNewMultiBetInfo } from '../../common/new-bet'
import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer' import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
import { getValues } from './utils' import { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
import { addUserToContractFollowers } from './follow-market'
const bodySchema = z.object({ const bodySchema = z.object({
contractId: z.string().max(MAX_ANSWER_LENGTH), contractId: z.string().max(MAX_ANSWER_LENGTH),
@ -75,8 +75,10 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
} }
transaction.create(newAnswerDoc, answer) transaction.create(newAnswerDoc, answer)
const loanAmount = 0
const { newBet, newPool, newTotalShares, newTotalBets } = const { newBet, newPool, newTotalShares, newTotalBets } =
getNewMultiBetInfo(answerId, amount, contract) getNewMultiBetInfo(answerId, amount, contract, loanAmount)
const newBalance = user.balance - amount const newBalance = user.balance - amount
const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc() const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
@ -97,7 +99,9 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
return answer return answer
}) })
await addUserToContractFollowers(contractId, auth.uid) const contract = await getContract(contractId)
if (answer && contract) await sendNewAnswerEmail(answer, contract)
return answer return answer
}) })

View File

@ -1,105 +0,0 @@
import * as admin from 'firebase-admin'
import { getContract, getUser, log } from './utils'
import { APIError, newEndpoint, validate } from './api'
import { JSONContent } from '@tiptap/core'
import { z } from 'zod'
import { removeUndefinedProps } from '../../common/util/object'
import { htmlToRichText } from '../../common/util/parse'
import { marked } from 'marked'
const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(contentSchema).optional(),
marks: z
.array(
z.intersection(
z.record(z.any()),
z.object({
type: z.string(),
attrs: z.record(z.any()).optional(),
})
)
)
.optional(),
text: z.string().optional(),
})
)
)
const postSchema = z.object({
contractId: z.string(),
content: contentSchema.optional(),
html: z.string().optional(),
markdown: z.string().optional(),
})
const MAX_COMMENT_JSON_LENGTH = 20000
// For now, only supports creating a new top-level comment on a contract.
// Replies, posts, chats are not supported yet.
export const createcomment = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { contractId, content, html, markdown } = validate(postSchema, req.body)
const creator = await getUser(auth.uid)
const contract = await getContract(contractId)
if (!creator) {
throw new APIError(400, 'No user exists with the authenticated user ID.')
}
if (!contract) {
throw new APIError(400, 'No contract exists with the given ID.')
}
let contentJson = null
if (content) {
contentJson = content
} else if (html) {
console.log('html', html)
contentJson = htmlToRichText(html)
} else if (markdown) {
const markedParse = marked.parse(markdown)
log('parsed', markedParse)
contentJson = htmlToRichText(markedParse)
log('json', contentJson)
}
if (!contentJson) {
throw new APIError(400, 'No comment content provided.')
}
if (JSON.stringify(contentJson).length > MAX_COMMENT_JSON_LENGTH) {
throw new APIError(
400,
`Comment is too long; should be less than ${MAX_COMMENT_JSON_LENGTH} as a JSON string.`
)
}
const ref = firestore.collection(`contracts/${contractId}/comments`).doc()
const comment = removeUndefinedProps({
id: ref.id,
content: contentJson,
createdTime: Date.now(),
userId: creator.id,
userName: creator.name,
userUsername: creator.username,
userAvatarUrl: creator.avatarUrl,
// OnContract fields
commentType: 'contract',
contractId: contractId,
contractSlug: contract.slug,
contractQuestion: contract.question,
})
await ref.set(comment)
return { status: 'success', comment }
})

View File

@ -0,0 +1,242 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import {
CPMMBinaryContract,
Contract,
FreeResponseContract,
MAX_QUESTION_LENGTH,
MAX_TAG_LENGTH,
NumericContract,
OUTCOME_TYPES,
} from '../../common/contract'
import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random'
import { chargeUser } from './utils'
import { APIError, newEndpoint, validate, zTimestamp } from './api'
import {
FIXED_ANTE,
getCpmmInitialLiquidity,
getFreeAnswerAnte,
getNumericAnte,
} from '../../common/antes'
import { getNoneAnswer } from '../../common/answer'
import { getNewContract } from '../../common/new-contract'
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
import { User } from '../../common/user'
import { Group, MAX_ID_LENGTH } from '../../common/group'
import { getPseudoProbability } from '../../common/pseudo-numeric'
import { JSONContent } from '@tiptap/core'
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(descScehma).optional(),
marks: z
.array(
z.intersection(
z.record(z.any()),
z.object({
type: z.string(),
attrs: z.record(z.any()).optional(),
})
)
)
.optional(),
text: z.string().optional(),
})
)
)
const bodySchema = z.object({
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
description: descScehma.optional(),
tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
closeTime: zTimestamp().refine(
(date) => date.getTime() > new Date().getTime(),
'Close time must be in the future.'
),
outcomeType: z.enum(OUTCOME_TYPES),
groupId: z.string().min(1).max(MAX_ID_LENGTH).optional(),
})
const binarySchema = z.object({
initialProb: z.number().min(1).max(99),
})
const finite = () =>
z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER)
const numericSchema = z.object({
min: finite(),
max: finite(),
initialValue: finite(),
isLogScale: z.boolean().optional(),
})
export const createmarket = newEndpoint({}, async (req, auth) => {
const { question, description, tags, closeTime, outcomeType, groupId } =
validate(bodySchema, req.body)
let min, max, initialProb, isLogScale
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
let initialValue
;({ min, max, initialValue, isLogScale } = validate(
numericSchema,
req.body
))
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
throw new APIError(400, 'Invalid range.')
initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100
if (initialProb < 1 || initialProb > 99)
throw new APIError(400, 'Invalid initial value.')
}
if (outcomeType === 'BINARY') {
;({ initialProb } = validate(binarySchema, req.body))
}
const userDoc = await firestore.collection('users').doc(auth.uid).get()
if (!userDoc.exists) {
throw new APIError(400, 'No user exists with the authenticated user ID.')
}
const user = userDoc.data() as User
const ante = FIXED_ANTE
// TODO: this is broken because it's not in a transaction
if (ante > user.balance)
throw new APIError(400, `Balance must be at least ${ante}.`)
const slug = await getSlug(question)
const contractRef = firestore.collection('contracts').doc()
let group = null
if (groupId) {
const groupDocRef = await firestore.collection('groups').doc(groupId)
const groupDoc = await groupDocRef.get()
if (!groupDoc.exists) {
throw new APIError(400, 'No group exists with the given group ID.')
}
group = groupDoc.data() as Group
if (!group.memberIds.includes(user.id)) {
throw new APIError(
400,
'User must be a member of the group to add markets to it.'
)
}
if (!group.contractIds.includes(contractRef.id))
await groupDocRef.update({
contractIds: [...group.contractIds, contractRef.id],
})
}
console.log(
'creating contract for',
user.username,
'on',
question,
'ante:',
ante || 0
)
const contract = getNewContract(
contractRef.id,
slug,
user,
question,
outcomeType,
description ?? {},
initialProb ?? 0,
ante,
closeTime.getTime(),
tags ?? [],
NUMERIC_BUCKET_COUNT,
min ?? 0,
max ?? 0,
isLogScale ?? false
)
if (ante) await chargeUser(user.id, ante, true)
await contractRef.create(contract)
const providerId = user.id
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
const liquidityDoc = firestore
.collection(`contracts/${contract.id}/liquidity`)
.doc()
const lp = getCpmmInitialLiquidity(
providerId,
contract as CPMMBinaryContract,
liquidityDoc.id,
ante
)
await liquidityDoc.set(lp)
} else if (outcomeType === 'FREE_RESPONSE') {
const noneAnswerDoc = firestore
.collection(`contracts/${contract.id}/answers`)
.doc('0')
const noneAnswer = getNoneAnswer(contract.id, user)
await noneAnswerDoc.set(noneAnswer)
const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`)
.doc()
const anteBet = getFreeAnswerAnte(
providerId,
contract as FreeResponseContract,
anteBetDoc.id
)
await anteBetDoc.set(anteBet)
} else if (outcomeType === 'NUMERIC') {
const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`)
.doc()
const anteBet = getNumericAnte(
providerId,
contract as NumericContract,
ante,
anteBetDoc.id
)
await anteBetDoc.set(anteBet)
}
return contract
})
const getSlug = async (question: string) => {
const proposedSlug = slugify(question)
const preexistingContract = await getContractFromSlug(proposedSlug)
return preexistingContract
? proposedSlug + '-' + randomString()
: proposedSlug
}
const firestore = admin.firestore()
export async function getContractFromSlug(slug: string) {
const snap = await firestore
.collection('contracts')
.where('slug', '==', slug)
.get()
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
}

View File

@ -10,7 +10,7 @@ import {
MAX_GROUP_NAME_LENGTH, MAX_GROUP_NAME_LENGTH,
MAX_ID_LENGTH, MAX_ID_LENGTH,
} from '../../common/group' } from '../../common/group'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from '../../functions/src/api'
import { z } from 'zod' import { z } from 'zod'
const bodySchema = z.object({ const bodySchema = z.object({
@ -21,7 +21,6 @@ const bodySchema = z.object({
}) })
export const creategroup = newEndpoint({}, async (req, auth) => { export const creategroup = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { name, about, memberIds, anyoneCanJoin } = validate( const { name, about, memberIds, anyoneCanJoin } = validate(
bodySchema, bodySchema,
req.body req.body
@ -58,29 +57,17 @@ export const creategroup = newEndpoint({}, async (req, auth) => {
createdTime: Date.now(), createdTime: Date.now(),
mostRecentActivityTime: Date.now(), mostRecentActivityTime: Date.now(),
// TODO: allow users to add contract ids on group creation // TODO: allow users to add contract ids on group creation
contractIds: [],
anyoneCanJoin, anyoneCanJoin,
totalContracts: 0, memberIds,
totalMembers: memberIds.length,
postIds: [],
pinnedItems: [],
} }
await groupRef.create(group) await groupRef.create(group)
// create a GroupMemberDoc for each member
await Promise.all(
memberIds.map((memberId) =>
groupRef.collection('groupMembers').doc(memberId).create({
userId: memberId,
createdTime: Date.now(),
})
)
)
return { status: 'success', group: group } return { status: 'success', group: group }
}) })
export const getSlug = async (name: string) => { const getSlug = async (name: string) => {
const proposedSlug = slugify(name) const proposedSlug = slugify(name)
const preexistingGroup = await getGroupFromSlug(proposedSlug) const preexistingGroup = await getGroupFromSlug(proposedSlug)
@ -88,8 +75,9 @@ export const getSlug = async (name: string) => {
return preexistingGroup ? proposedSlug + '-' + randomString() : proposedSlug return preexistingGroup ? proposedSlug + '-' + randomString() : proposedSlug
} }
export async function getGroupFromSlug(slug: string) {
const firestore = admin.firestore() const firestore = admin.firestore()
export async function getGroupFromSlug(slug: string) {
const snap = await firestore const snap = await firestore
.collection('groups') .collection('groups')
.where('slug', '==', slug) .where('slug', '==', slug)

View File

@ -1,383 +0,0 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import {
Contract,
CPMMBinaryContract,
FreeResponseContract,
MAX_QUESTION_LENGTH,
MAX_TAG_LENGTH,
MultipleChoiceContract,
NumericContract,
OUTCOME_TYPES,
VISIBILITIES,
} from '../../common/contract'
import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random'
import { chargeUser, getContract, isProd } from './utils'
import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api'
import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy'
import {
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
getCpmmInitialLiquidity,
getFreeAnswerAnte,
getMultipleChoiceAntes,
getNumericAnte,
HOUSE_LIQUIDITY_PROVIDER_ID,
} from '../../common/antes'
import { Answer, getNoneAnswer } from '../../common/answer'
import { getNewContract } from '../../common/new-contract'
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
import { User } from '../../common/user'
import { Group, GroupLink, MAX_ID_LENGTH } from '../../common/group'
import { getPseudoProbability } from '../../common/pseudo-numeric'
import { JSONContent } from '@tiptap/core'
import { uniq, zip } from 'lodash'
import { Bet } from '../../common/bet'
import { FieldValue } from 'firebase-admin/firestore'
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(descScehma).optional(),
marks: z
.array(
z.intersection(
z.record(z.any()),
z.object({
type: z.string(),
attrs: z.record(z.any()).optional(),
})
)
)
.optional(),
text: z.string().optional(),
})
)
)
const bodySchema = z.object({
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
description: descScehma.or(z.string()).optional(),
tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
closeTime: zTimestamp().refine(
(date) => date.getTime() > new Date().getTime(),
'Close time must be in the future.'
),
outcomeType: z.enum(OUTCOME_TYPES),
groupId: z.string().min(1).max(MAX_ID_LENGTH).optional(),
visibility: z.enum(VISIBILITIES).optional(),
})
const binarySchema = z.object({
initialProb: z.number().min(1).max(99),
})
const finite = () =>
z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER)
const numericSchema = z.object({
min: finite(),
max: finite(),
initialValue: finite(),
isLogScale: z.boolean().optional(),
})
const multipleChoiceSchema = z.object({
answers: z.string().trim().min(1).array().min(2),
})
export const createmarket = newEndpoint({}, (req, auth) => {
return createMarketHelper(req.body, auth)
})
export async function createMarketHelper(body: any, auth: AuthedUser) {
const {
question,
description,
tags,
closeTime,
outcomeType,
groupId,
visibility = 'public',
} = validate(bodySchema, body)
let min, max, initialProb, isLogScale, answers
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
let initialValue
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
throw new APIError(400, 'Invalid range.')
initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100
if (initialProb < 1 || initialProb > 99)
if (outcomeType === 'PSEUDO_NUMERIC')
throw new APIError(
400,
`Initial value is too ${initialProb < 1 ? 'low' : 'high'}`
)
else throw new APIError(400, 'Invalid initial probability.')
}
if (outcomeType === 'BINARY') {
;({ initialProb } = validate(binarySchema, body))
}
if (outcomeType === 'MULTIPLE_CHOICE') {
;({ answers } = validate(multipleChoiceSchema, body))
}
const userDoc = await firestore.collection('users').doc(auth.uid).get()
if (!userDoc.exists) {
throw new APIError(400, 'No user exists with the authenticated user ID.')
}
const user = userDoc.data() as User
const ante = FIXED_ANTE
const deservesFreeMarket =
(user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX
// TODO: this is broken because it's not in a transaction
if (ante > user.balance && !deservesFreeMarket)
throw new APIError(400, `Balance must be at least ${ante}.`)
let group: Group | null = null
if (groupId) {
const groupDocRef = firestore.collection('groups').doc(groupId)
const groupDoc = await groupDocRef.get()
if (!groupDoc.exists) {
throw new APIError(400, 'No group exists with the given group ID.')
}
group = groupDoc.data() as Group
const groupMembersSnap = await firestore
.collection(`groups/${groupId}/groupMembers`)
.get()
const groupMemberDocs = groupMembersSnap.docs.map(
(doc) => doc.data() as { userId: string; createdTime: number }
)
if (
!groupMemberDocs.map((m) => m.userId).includes(user.id) &&
!group.anyoneCanJoin &&
group.creatorId !== user.id
) {
throw new APIError(
400,
'User must be a member/creator of the group or group must be open to add markets to it.'
)
}
}
const slug = await getSlug(question)
const contractRef = firestore.collection('contracts').doc()
console.log(
'creating contract for',
user.username,
'on',
question,
'ante:',
ante || 0
)
// convert string descriptions into JSONContent
const newDescription =
!description || typeof description === 'string'
? {
type: 'doc',
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: description || ' ' }],
},
],
}
: description
const contract = getNewContract(
contractRef.id,
slug,
user,
question,
outcomeType,
newDescription,
initialProb ?? 0,
ante,
closeTime.getTime(),
tags ?? [],
NUMERIC_BUCKET_COUNT,
min ?? 0,
max ?? 0,
isLogScale ?? false,
answers ?? [],
visibility
)
const providerId = deservesFreeMarket
? isProd()
? HOUSE_LIQUIDITY_PROVIDER_ID
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
: user.id
if (ante) await chargeUser(providerId, ante, true)
if (deservesFreeMarket)
await firestore
.collection('users')
.doc(user.id)
.update({ freeMarketsCreated: FieldValue.increment(1) })
await contractRef.create(contract)
if (group != null) {
const groupContractsSnap = await firestore
.collection(`groups/${groupId}/groupContracts`)
.get()
const groupContracts = groupContractsSnap.docs.map(
(doc) => doc.data() as { contractId: string; createdTime: number }
)
if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) {
await createGroupLinks(group, [contractRef.id], auth.uid)
const groupContractRef = firestore
.collection(`groups/${groupId}/groupContracts`)
.doc(contract.id)
await groupContractRef.set({
contractId: contract.id,
createdTime: Date.now(),
})
}
}
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
const liquidityDoc = firestore
.collection(`contracts/${contract.id}/liquidity`)
.doc()
const lp = getCpmmInitialLiquidity(
providerId,
contract as CPMMBinaryContract,
liquidityDoc.id,
ante
)
await liquidityDoc.set(lp)
} else if (outcomeType === 'MULTIPLE_CHOICE') {
const betCol = firestore.collection(`contracts/${contract.id}/bets`)
const betDocs = (answers ?? []).map(() => betCol.doc())
const answerCol = firestore.collection(`contracts/${contract.id}/answers`)
const answerDocs = (answers ?? []).map((_, i) =>
answerCol.doc(i.toString())
)
const { bets, answerObjects } = getMultipleChoiceAntes(
user,
contract as MultipleChoiceContract,
answers ?? [],
betDocs.map((bd) => bd.id)
)
await Promise.all(
zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet))
)
await Promise.all(
zip(answerObjects, answerDocs).map(([answer, doc]) =>
doc?.create(answer as Answer)
)
)
await contractRef.update({ answers: answerObjects })
} else if (outcomeType === 'FREE_RESPONSE') {
const noneAnswerDoc = firestore
.collection(`contracts/${contract.id}/answers`)
.doc('0')
const noneAnswer = getNoneAnswer(contract.id, user)
await noneAnswerDoc.set(noneAnswer)
const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`)
.doc()
const anteBet = getFreeAnswerAnte(
providerId,
contract as FreeResponseContract,
anteBetDoc.id
)
await anteBetDoc.set(anteBet)
} else if (outcomeType === 'NUMERIC') {
const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`)
.doc()
const anteBet = getNumericAnte(
providerId,
contract as NumericContract,
ante,
anteBetDoc.id
)
await anteBetDoc.set(anteBet)
}
return contract
}
const getSlug = async (question: string) => {
const proposedSlug = slugify(question)
const preexistingContract = await getContractFromSlug(proposedSlug)
return preexistingContract
? proposedSlug + '-' + randomString()
: proposedSlug
}
const firestore = admin.firestore()
export async function getContractFromSlug(slug: string) {
const snap = await firestore
.collection('contracts')
.where('slug', '==', slug)
.get()
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
}
async function createGroupLinks(
group: Group,
contractIds: string[],
userId: string
) {
for (const contractId of contractIds) {
const contract = await getContract(contractId)
if (!contract?.groupSlugs?.includes(group.slug)) {
await firestore
.collection('contracts')
.doc(contractId)
.update({
groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]),
})
}
if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) {
await firestore
.collection('contracts')
.doc(contractId)
.update({
groupLinks: [
{
groupId: group.id,
name: group.name,
slug: group.slug,
userId,
createdTime: Date.now(),
} as GroupLink,
...(contract?.groupLinks ?? []),
],
})
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,142 +0,0 @@
import * as admin from 'firebase-admin'
import { getUser } from './utils'
import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random'
import {
Post,
MAX_POST_TITLE_LENGTH,
MAX_POST_SUBTITLE_LENGTH,
} from '../../common/post'
import { APIError, newEndpoint, validate } from './api'
import { JSONContent } from '@tiptap/core'
import { z } from 'zod'
import { removeUndefinedProps } from '../../common/util/object'
import { createMarketHelper } from './create-market'
import { DAY_MS } from '../../common/util/time'
const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(contentSchema).optional(),
marks: z
.array(
z.intersection(
z.record(z.any()),
z.object({
type: z.string(),
attrs: z.record(z.any()).optional(),
})
)
)
.optional(),
text: z.string().optional(),
})
)
)
const postSchema = z.object({
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
subtitle: z.string().min(1).max(MAX_POST_SUBTITLE_LENGTH),
content: contentSchema,
groupId: z.string().optional(),
// Date doc fields:
bounty: z.number().optional(),
birthday: z.number().optional(),
type: z.string().optional(),
question: z.string().optional(),
})
export const createpost = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { title, subtitle, content, groupId, question, ...otherProps } =
validate(postSchema, req.body)
const creator = await getUser(auth.uid)
if (!creator)
throw new APIError(400, 'No user exists with the authenticated user ID.')
console.log('creating post owned by', creator.username, 'titled', title)
const slug = await getSlug(title)
const postRef = firestore.collection('posts').doc()
// If this is a date doc, create a market for it.
let contractSlug
if (question) {
const closeTime = Date.now() + DAY_MS * 30 * 3
try {
const result = await createMarketHelper(
{
question,
closeTime,
outcomeType: 'BINARY',
visibility: 'unlisted',
initialProb: 50,
// Dating group!
groupId: 'j3ZE8fkeqiKmRGumy3O1',
},
auth
)
contractSlug = result.slug
} catch (e) {
console.error(e)
}
}
const post: Post = removeUndefinedProps({
...otherProps,
id: postRef.id,
creatorId: creator.id,
slug,
title,
subtitle,
createdTime: Date.now(),
content: content,
contractSlug,
creatorName: creator.name,
creatorUsername: creator.username,
creatorAvatarUrl: creator.avatarUrl,
itemType: 'post',
})
await postRef.create(post)
if (groupId) {
const groupRef = firestore.collection('groups').doc(groupId)
const group = await groupRef.get()
if (group.exists) {
const groupData = group.data()
if (groupData) {
const postIds = groupData.postIds ?? []
postIds.push(postRef.id)
await groupRef.update({ postIds })
}
}
}
return { status: 'success', post }
})
export const getSlug = async (title: string) => {
const proposedSlug = slugify(title)
const preexistingPost = await getPostFromSlug(proposedSlug)
return preexistingPost ? proposedSlug + '-' + randomString() : proposedSlug
}
export async function getPostFromSlug(slug: string) {
const firestore = admin.firestore()
const snap = await firestore
.collection('posts')
.where('slug', '==', slug)
.get()
return snap.empty ? undefined : (snap.docs[0].data() as Post)
}

View File

@ -1,13 +1,20 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod' import { z } from 'zod'
import {
import { PrivateUser, User } from '../../common/user' MANIFOLD_AVATAR_URL,
import { getUser, getUserByUsername, getValues } from './utils' MANIFOLD_USERNAME,
PrivateUser,
STARTING_BALANCE,
SUS_STARTING_BALANCE,
User,
} from '../../common/user'
import { getUser, getUserByUsername, getValues, isProd } from './utils'
import { randomString } from '../../common/util/random' import { randomString } from '../../common/util/random'
import { import {
cleanDisplayName, cleanDisplayName,
cleanUsername, cleanUsername,
} from '../../common/util/clean-username' } from '../../common/util/clean-username'
import { sendWelcomeEmail } from './emails'
import { isWhitelisted } from '../../common/envs/constants' import { isWhitelisted } from '../../common/envs/constants'
import { import {
CATEGORIES_GROUP_SLUG_POSTFIX, CATEGORIES_GROUP_SLUG_POSTFIX,
@ -16,9 +23,12 @@ import {
import { track } from './analytics' import { track } from './analytics'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
import { Group } from '../../common/group' import { Group, NEW_USER_GROUP_SLUGS } from '../../common/group'
import { SUS_STARTING_BALANCE, STARTING_BALANCE } from '../../common/economy' import { uniq } from 'lodash'
import { getDefaultNotificationPreferences } from '../../common/user-notification-preferences' import {
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
HOUSE_LIQUIDITY_PROVIDER_ID,
} from '../../common/antes'
const bodySchema = z.object({ const bodySchema = z.object({
deviceToken: z.string().optional(), deviceToken: z.string().optional(),
@ -53,7 +63,10 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
const deviceUsedBefore = const deviceUsedBefore =
!deviceToken || (await isPrivateUserWithDeviceToken(deviceToken)) !deviceToken || (await isPrivateUserWithDeviceToken(deviceToken))
const balance = deviceUsedBefore ? SUS_STARTING_BALANCE : STARTING_BALANCE const ipCount = req.ip ? await numberUsersWithIp(req.ip) : 0
const balance =
deviceUsedBefore || ipCount > 2 ? SUS_STARTING_BALANCE : STARTING_BALANCE
const user: User = { const user: User = {
id: auth.uid, id: auth.uid,
@ -65,12 +78,8 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
createdTime: Date.now(), createdTime: Date.now(),
profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
nextLoanCached: 0,
followerCountCached: 0, followerCountCached: 0,
followedCategories: DEFAULT_CATEGORIES, followedCategories: DEFAULT_CATEGORIES,
shouldShowWelcome: true,
fractionResolvedCorrectly: 1,
achievements: {},
} }
await firestore.collection('users').doc(auth.uid).create(user) await firestore.collection('users').doc(auth.uid).create(user)
@ -82,15 +91,15 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
email, email,
initialIpAddress: req.ip, initialIpAddress: req.ip,
initialDeviceToken: deviceToken, initialDeviceToken: deviceToken,
notificationPreferences: getDefaultNotificationPreferences(auth.uid),
} }
await firestore.collection('private-users').doc(auth.uid).create(privateUser) await firestore.collection('private-users').doc(auth.uid).create(privateUser)
await addUserToDefaultGroups(user)
await sendWelcomeEmail(user, privateUser)
await addUserToDefaultGroups(user)
await track(auth.uid, 'create user', { username }, { ip: req.ip }) await track(auth.uid, 'create user', { username }, { ip: req.ip })
return { user, privateUser } return user
}) })
const firestore = admin.firestore() const firestore = admin.firestore()
@ -104,7 +113,7 @@ const isPrivateUserWithDeviceToken = async (deviceToken: string) => {
return !snap.empty return !snap.empty
} }
export const numberUsersWithIp = async (ipAddress: string) => { const numberUsersWithIp = async (ipAddress: string) => {
const snap = await firestore const snap = await firestore
.collection('private-users') .collection('private-users')
.where('initialIpAddress', '==', ipAddress) .where('initialIpAddress', '==', ipAddress)
@ -120,8 +129,42 @@ const addUserToDefaultGroups = async (user: User) => {
firestore.collection('groups').where('slug', '==', slug) firestore.collection('groups').where('slug', '==', slug)
) )
await firestore await firestore
.collection(`groups/${groups[0].id}/groupMembers`) .collection('groups')
.doc(user.id) .doc(groups[0].id)
.set({ userId: user.id, createdTime: Date.now() }) .update({
memberIds: uniq(groups[0].memberIds.concat(user.id)),
})
}
for (const slug of NEW_USER_GROUP_SLUGS) {
const groups = await getValues<Group>(
firestore.collection('groups').where('slug', '==', slug)
)
const group = groups[0]
await firestore
.collection('groups')
.doc(group.id)
.update({
memberIds: uniq(group.memberIds.concat(user.id)),
})
const manifoldAccount = isProd()
? HOUSE_LIQUIDITY_PROVIDER_ID
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
if (slug === 'welcome') {
const welcomeCommentDoc = firestore
.collection(`groups/${group.id}/comments`)
.doc()
await welcomeCommentDoc.create({
id: welcomeCommentDoc.id,
groupId: group.id,
userId: manifoldAccount,
text: `Welcome, ${user.name} (@${user.username})!`,
createdTime: Date.now(),
userName: 'Manifold Markets',
userUsername: MANIFOLD_USERNAME,
userAvatarUrl: MANIFOLD_AVATAR_URL,
})
}
} }
} }

View File

@ -1,69 +0,0 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { CPMMContract } from '../../common/contract'
import { batchedWaitAll } from '../../common/util/promise'
import { APIError } from '../../common/api'
import { addCpmmLiquidity } from '../../common/calculate-cpmm'
import { formatMoneyWithDecimals } from '../../common/util/format'
const firestore = admin.firestore()
export const drizzleLiquidity = async () => {
const snap = await firestore
.collection('contracts')
.where('subsidyPool', '>', 1e-7)
.get()
const contractIds = snap.docs.map((doc) => doc.id)
console.log('found', contractIds.length, 'markets to drizzle')
console.log()
await batchedWaitAll(
contractIds.map((cid) => () => drizzleMarket(cid)),
10
)
}
export const drizzleLiquidityScheduler = functions.pubsub
.schedule('* * * * *') // every minute
.onRun(drizzleLiquidity)
const drizzleMarket = async (contractId: string) => {
await firestore.runTransaction(async (trans) => {
const snap = await trans.get(firestore.doc(`contracts/${contractId}`))
const contract = snap.data() as CPMMContract
const { subsidyPool, pool, p, slug, popularityScore } = contract
if ((subsidyPool ?? 0) < 1e-7) return
const r = Math.random()
const logPopularity = Math.log10((popularityScore ?? 0) + 1)
const v = Math.max(1, Math.min(5, logPopularity))
const amount = subsidyPool <= 0.5 ? subsidyPool : r * v * 0.01 * subsidyPool
const { newPool, newP } = addCpmmLiquidity(pool, p, amount)
if (!isFinite(newP)) {
throw new APIError(
500,
'Liquidity injection rejected due to overflow error.'
)
}
await trans.update(firestore.doc(`contracts/${contract.id}`), {
pool: newPool,
p: newP,
subsidyPool: subsidyPool - amount,
})
console.log(
'added subsidy',
formatMoneyWithDecimals(amount),
'of',
formatMoneyWithDecimals(subsidyPool),
'pool to',
slug
)
console.log()
})
}

File diff suppressed because one or more lines are too long

View File

@ -1,526 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Manifold Market Creation Guide</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto" src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align: top"
width="100%">
<tbody>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Hi {{name}},</span></p>
</div>
</td>
</tr>
<tr>
<td align="left" style="
font-size: 0px;
padding: 0px 25px 20px 25px;
padding-top: 0px;
padding-right: 25px;
padding-bottom: 20px;
padding-left: 25px;
word-break: break-word;
">
<div style="
font-family: Arial, sans-serif;
font-size: 17px;
letter-spacing: normal;
line-height: 1;
text-align: left;
color: #000000;
">
<p class="text-build-content" style="
line-height: 23px;
margin: 10px 0;
margin-top: 10px;
" data-testid="3Q8BP69fq">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">Did you know you can create your own prediction market on <a
class="link-build-content" style="color: #55575d" target="_blank"
href="https://manifold.markets">Manifold</a> on
any question you care about?</span>
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">Whether it's current events like <a class="link-build-content" style="color: #55575d"
target="_blank"
href="https://manifold.markets/SG/will-elon-musk-buy-twitter-this-yea">Musk buying
Twitter</a> or <a class="link-build-content" style="color: #55575d" target="_blank"
href="https://manifold.markets/NathanpmYoung/will-biden-be-the-2024-democratic-n">2024
elections</a> or personal matters
like <a class="link-build-content" style="color: #55575d" target="_blank"
href="https://manifold.markets/dreev/which-book-will-i-like-best">book
recommendations</a> or <a class="link-build-content" style="color: #55575d"
target="_blank"
href="https://manifold.markets/agentydragon/will-my-weight-go-under-115-kg-in-2">losing
weight</a>,
Manifold can help you find the answer.</span>
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0;margin-bottom: 20px;"
data-testid="3Q8BP69fq">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">The following is a
short guide to creating markets.</span>
</p>
<table cellspacing="0" cellpadding="0" align="center">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="https://manifold.markets/create" target="_blank"
style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
Create a market
</a>
</td>
</tr>
</table>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
&nbsp;
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
<span style="
color: #292fd7;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 20px;
"><b>What makes a good market?</b></span>
</p>
<ul>
<li style="line-height: 23px; margin-bottom: 8px;">
<span
style="font-family: Readex Pro, Arial, Helvetica, sans-serif;font-size: 17px;"><b>Interesting
topic. </b>Manifold gives
creators M$10 for
each unique trader that bets on your
market, so it pays to ask a question people are interested in!</span>
</li>
<li style="line-height: 23px; margin-bottom: 8px;">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><b>Clear resolution criteria. </b>Any ambiguities or edge cases in your description
will drive traders away from your markets.</span>
</li>
<li style="line-height: 23px; margin-bottom: 8px;">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><b>Detailed description. </b>Include images/videos/tweets and any context or
background
information that could be useful to people who
are interested in learning more that are
uneducated on the subject.</span>
</li>
<li style="line-height: 23px; margin-bottom: 8px;">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><b>Part of a group. </b>Groups are the
primary way users filter for relevant markets.
Also, consider making your own groups and
inviting friends/interested communities to
them from other sites!</span>
</li>
<li style="line-height: 23px; margin-bottom: 8px;">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><b>Sharing it on social media</b>. You'll earn the <a class="link-build-content"
style="color: inherit; text-decoration: none" target="_blank"
href="https://manifold.markets/referrals"><span style="
color: #55575d;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><u>M$500
referral bonus</u></span></a> if you get new users to sign up!</span>
</li>
</ul>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
&nbsp;
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
<span style="
color: #000000;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">Why not </span>
<a class="link-build-content" style="color: inherit; text-decoration: none" target="_blank"
href="https://manifold.markets/create"><span style="
color: #55575d;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><u>create a market</u></span></a><span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">
while it is still fresh on your mind?
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
&nbsp;
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
<span style="
color: #000000;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">Thanks for reading!</span>
</p>
<p class="text-build-content" style="
line-height: 23px;
margin: 10px 0;
margin-bottom: 10px;
" data-testid="3Q8BP69fq">
<span style="
color: #000000;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">David from Manifold</span>
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<div style="background-color: #f4f4f4">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align: top"
width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 0px 25px 0px 25px;
padding-top: 0px;
padding-right: 25px;
padding-bottom: 0px;
padding-left: 25px;
word-break: break-word;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="
border-collapse: collapse;
border-spacing: 0px;
">
<tbody>
<tr>
<td style="width: 550px">
<a href="https://manifold.markets/create" target="_blank"><img alt="" height="auto"
src="https://03jlj.mjt.lu/img/03jlj/b/96u/omk8.gif" style="
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
" width="550" /></a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0 0 20px 0;
text-align: center;
">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
">
<div style="
font-family: Ubuntu, Helvetica, Arial,
sans-serif;
font-size: 11px;
line-height: 22px;
text-align: center;
color: #000000;
">
<p style="margin: 10px 0">
This e-mail has been sent to {{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@ -1,475 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Interesting markets on Manifold</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center"
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto"
src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align: top" width="100%">
<tbody>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Here is a selection of markets on Manifold you might find
interesting!</span></p>
</div>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question1Link}}">
<img alt="{{question1Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question1ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question1Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question2Link}}">
<img alt="{{question2Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question2ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question2Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question3Link}}">
<img alt="{{question3Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question3ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question3Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question4Link}}">
<img alt="{{question4Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question4ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question4Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question5Link}}">
<img alt="{{question5Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question5ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question5Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question6Link}}">
<img alt="{{question6Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question6ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question6Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0 0 20px 0;
text-align: center;
">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
width="100%">
<tbody>
<tr>
<td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0"
role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
">
<div style="
font-family: Ubuntu, Helvetica, Arial,
sans-serif;
font-size: 11px;
line-height: 22px;
text-align: center;
color: #000000;
">
<p style="margin: 10px 0">
This e-mail has been sent to
{{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@ -526,10 +526,19 @@
" "
>our Discord</a >our Discord</a
>! Or, >! Or,
<a href="{{unsubscribeUrl}}" style=" <a
color: inherit; href="{{unsubscribeUrl}}"
text-decoration: none; style="
" target="_blank">click here to unsubscribe from this type of notification</a>. 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> </td>
</tr> </tr>
</table> </table>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html style=" <html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
>
<head> <head>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
@ -32,52 +33,41 @@
body { body {
padding: 0 !important; padding: 0 !important;
} }
h1 { h1 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h2 { h2 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h3 { h3 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h4 { h4 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h1 { h1 {
font-size: 22px !important; font-size: 22px !important;
} }
h2 { h2 {
font-size: 18px !important; font-size: 18px !important;
} }
h3 { h3 {
font-size: 16px !important; font-size: 16px !important;
} }
.container { .container {
padding: 0 !important; padding: 0 !important;
width: 100% !important; width: 100% !important;
} }
.content { .content {
padding: 0 !important; padding: 0 !important;
} }
.content-wrap { .content-wrap {
padding: 10px !important; padding: 10px !important;
} }
.invoice { .invoice {
width: 100% !important; width: 100% !important;
} }
@ -85,7 +75,10 @@
</style> </style>
</head> </head>
<body itemscope itemtype="http://schema.org/EmailMessage" style=" <body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -96,29 +89,43 @@
line-height: 1.6em; line-height: 1.6em;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" bgcolor="#f6f6f6"> "
<table class="body-wrap" style=" bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" bgcolor="#f6f6f6"> "
<tr style=" bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" valign="top"></td> "
<td class="container" width="600" style=" valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -127,8 +134,12 @@
max-width: 600px !important; max-width: 600px !important;
clear: both !important; clear: both !important;
margin: 0 auto; margin: 0 auto;
" valign="top"> "
<div class="content" style=" valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -136,8 +147,14 @@
display: block; display: block;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
"> "
<table class="main" width="100%" cellpadding="0" cellspacing="0" style=" >
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -145,14 +162,20 @@
background-color: #fff; background-color: #fff;
margin: 0; margin: 0;
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
" bgcolor="#fff"> "
<tr style=" bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-wrap aligncenter" style=" >
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -160,23 +183,35 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" align="center" valign="top"> "
<table width="100%" cellpadding="0" cellspacing="0" style=" align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
width: 90%; width: 90%;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -185,21 +220,29 @@
margin: 0; margin: 0;
padding: 0 0 0px 0; padding: 0 0 0px 0;
text-align: left; text-align: left;
" valign="top"> "
<a href="https://manifold.markets" target="_blank"> valign="top"
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto" >
alt="Manifold Markets" /> <img
</a> src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td class="content-block aligncenter" style=" >
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -208,8 +251,13 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
" align="center" valign="top"> "
<table class="invoice" style=" align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -218,15 +266,19 @@
width: 80%; width: 80%;
margin: 40px auto; margin: 40px auto;
margin-top: 10px; margin-top: 10px;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -234,26 +286,37 @@
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
font-weight: bold; font-weight: bold;
" valign="top"> "
valign="top"
>
<div> <div>
<img src="{{avatarUrl}}" width="30" height="30" style=" <img
src="{{avatarUrl}}"
width="30"
height="30"
style="
border-radius: 30px; border-radius: 30px;
overflow: hidden; overflow: hidden;
vertical-align: middle; vertical-align: middle;
margin-right: 4px; margin-right: 4px;
" alt="" /> "
alt=""
/>
{{name}} {{name}}
</div> </div>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -261,29 +324,40 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" valign="top"> "
<div style=" valign="top"
>
<div
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<span style="white-space: pre-line">{{answer}}</span> >
<span style="white-space: pre-line"
>{{answer}}</span
>
</div> </div>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
>
<td style="padding: 20px 0 0 0; margin: 0"> <td style="padding: 20px 0 0 0; margin: 0">
<div align="center"> <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]--> <!--[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=" <a
href="{{marketUrl}}"
target="_blank"
style="
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
@ -301,16 +375,23 @@
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
mso-border-alt: none; mso-border-alt: none;
"> "
<span style=" >
<span
style="
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
line-height: 120%; line-height: 120%;
"><span style=" "
><span
style="
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
line-height: 18.8px; line-height: 18.8px;
">View answer</span></span> "
>View answer</span
></span
>
</a> </a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]--> <!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div> </div>
@ -323,7 +404,9 @@
</td> </td>
</tr> </tr>
</table> </table>
<div class="footer" style=" <div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -332,20 +415,28 @@
color: #999; color: #999;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
"> "
<table width="100%" style=" >
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="aligncenter content-block" style=" >
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -355,9 +446,14 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" align="center" valign="top"> "
align="center"
valign="top"
>
Questions? Come ask in Questions? Come ask in
<a href="https://discord.gg/eHQBNBqXuh" style=" <a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -365,26 +461,39 @@
color: #999; color: #999;
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
">our Discord</a>! Or, "
<a href="{{unsubscribeUrl}}" style=" >our Discord</a
color: inherit; >! Or,
text-decoration: none; <a
" target="_blank">click here to unsubscribe from this type of notification</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> </td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
</td> </td>
<td style=" <td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" valign="top"></td> "
valign="top"
></td>
</tr> </tr>
</table> </table>
</body> </body>
</html> </html>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html style=" <html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
>
<head> <head>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
@ -32,52 +33,41 @@
body { body {
padding: 0 !important; padding: 0 !important;
} }
h1 { h1 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h2 { h2 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h3 { h3 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h4 { h4 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h1 { h1 {
font-size: 22px !important; font-size: 22px !important;
} }
h2 { h2 {
font-size: 18px !important; font-size: 18px !important;
} }
h3 { h3 {
font-size: 16px !important; font-size: 16px !important;
} }
.container { .container {
padding: 0 !important; padding: 0 !important;
width: 100% !important; width: 100% !important;
} }
.content { .content {
padding: 0 !important; padding: 0 !important;
} }
.content-wrap { .content-wrap {
padding: 10px !important; padding: 10px !important;
} }
.invoice { .invoice {
width: 100% !important; width: 100% !important;
} }
@ -85,7 +75,10 @@
</style> </style>
</head> </head>
<body itemscope itemtype="http://schema.org/EmailMessage" style=" <body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -96,29 +89,43 @@
line-height: 1.6em; line-height: 1.6em;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" bgcolor="#f6f6f6"> "
<table class="body-wrap" style=" bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" bgcolor="#f6f6f6"> "
<tr style=" bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" valign="top"></td> "
<td class="container" width="600" style=" valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -127,8 +134,12 @@
max-width: 600px !important; max-width: 600px !important;
clear: both !important; clear: both !important;
margin: 0 auto; margin: 0 auto;
" valign="top"> "
<div class="content" style=" valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -136,8 +147,14 @@
display: block; display: block;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
"> "
<table class="main" width="100%" cellpadding="0" cellspacing="0" style=" >
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -145,14 +162,20 @@
background-color: #fff; background-color: #fff;
margin: 0; margin: 0;
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
" bgcolor="#fff"> "
<tr style=" bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-wrap aligncenter" style=" >
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -160,23 +183,35 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" align="center" valign="top"> "
<table width="100%" cellpadding="0" cellspacing="0" style=" align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
width: 90%; width: 90%;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -185,22 +220,30 @@
margin: 0; margin: 0;
padding: 0 0 40px 0; padding: 0 0 40px 0;
text-align: left; text-align: left;
" valign="top"> "
<a href="https://manifold.markets" target="_blank"> valign="top"
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto" >
alt="Manifold Markets" /> <img
</a> src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
padding: 0; padding: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -209,18 +252,24 @@
margin: 0; margin: 0;
padding: 0 0 6px 0; padding: 0 0 6px 0;
text-align: left; text-align: left;
" valign="top"> "
valign="top"
>
You asked You asked
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -228,8 +277,12 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" valign="top"> "
<a href="{{url}}" style=" valign="top"
>
<a
href="{{url}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif; 'Lucida Grande', sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -242,18 +295,24 @@
color: #4337c9; color: #4337c9;
display: block; display: block;
text-decoration: none; text-decoration: none;
"> "
{{question}}</a> >
{{question}}</a
>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -261,8 +320,12 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 0 0 0px; padding: 0 0 0px;
" valign="top"> "
<h2 class="aligncenter" style=" valign="top"
>
<h2
class="aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif; 'Lucida Grande', sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -272,19 +335,25 @@
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
margin: 10px 0 0; margin: 10px 0 0;
" align="center"> "
align="center"
>
Market closed Market closed
</h2> </h2>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td class="content-block aligncenter" style=" >
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -293,8 +362,13 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
" align="center" valign="top"> "
<table class="invoice" style=" align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -302,15 +376,19 @@
text-align: left; text-align: left;
width: 80%; width: 80%;
margin: 40px auto; margin: 40px auto;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -318,90 +396,116 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" valign="top"> "
valign="top"
>
Hi {{name}}, Hi {{name}},
<br style=" <br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
<br style=" />
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
A market you created has closed. It's attracted A market you created has closed. It's attracted
<span style="font-weight: bold">{{volume}}</span> <span style="font-weight: bold">{{volume}}</span>
in bets — congrats! in bets — congrats!
<br style=" <br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
<br style=" />
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
Please resolve your market. />
<br style=" Resolve your market to earn {{creatorFee}} as the
creator commission.
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
<br style=" />
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
Thanks, Thanks,
<br style=" <br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
Manifold Team Manifold Team
<br style=" <br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
<br style=" />
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
>
<td style="padding: 10px 0 0 0; margin: 0"> <td style="padding: 10px 0 0 0; margin: 0">
<div align="center"> <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]--> <!--[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=" <a
href="{{url}}"
target="_blank"
style="
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
@ -419,16 +523,23 @@
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
mso-border-alt: none; mso-border-alt: none;
"> "
<span style=" >
<span
style="
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
line-height: 120%; line-height: 120%;
"><span style=" "
><span
style="
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
line-height: 18.8px; line-height: 18.8px;
">View market</span></span> "
>View market</span
></span
>
</a> </a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]--> <!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div> </div>
@ -441,7 +552,9 @@
</td> </td>
</tr> </tr>
</table> </table>
<div class="footer" style=" <div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -450,20 +563,28 @@
color: #999; color: #999;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
"> "
<table width="100%" style=" >
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="aligncenter content-block" style=" >
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -473,9 +594,14 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" align="center" valign="top"> "
align="center"
valign="top"
>
Questions? Come ask in Questions? Come ask in
<a href="https://discord.gg/eHQBNBqXuh" style=" <a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -483,22 +609,39 @@
color: #999; color: #999;
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
">our Discord</a>! "
>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> </td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
</td> </td>
<td style=" <td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" valign="top"></td> "
valign="top"
></td>
</tr> </tr>
</table> </table>
</body> </body>
</html> </html>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html style=" <html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
>
<head> <head>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
@ -32,52 +33,41 @@
body { body {
padding: 0 !important; padding: 0 !important;
} }
h1 { h1 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h2 { h2 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h3 { h3 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h4 { h4 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h1 { h1 {
font-size: 22px !important; font-size: 22px !important;
} }
h2 { h2 {
font-size: 18px !important; font-size: 18px !important;
} }
h3 { h3 {
font-size: 16px !important; font-size: 16px !important;
} }
.container { .container {
padding: 0 !important; padding: 0 !important;
width: 100% !important; width: 100% !important;
} }
.content { .content {
padding: 0 !important; padding: 0 !important;
} }
.content-wrap { .content-wrap {
padding: 10px !important; padding: 10px !important;
} }
.invoice { .invoice {
width: 100% !important; width: 100% !important;
} }
@ -85,7 +75,10 @@
</style> </style>
</head> </head>
<body itemscope itemtype="http://schema.org/EmailMessage" style=" <body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -96,29 +89,43 @@
line-height: 1.6em; line-height: 1.6em;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" bgcolor="#f6f6f6"> "
<table class="body-wrap" style=" bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" bgcolor="#f6f6f6"> "
<tr style=" bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" valign="top"></td> "
<td class="container" width="600" style=" valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -127,8 +134,12 @@
max-width: 600px !important; max-width: 600px !important;
clear: both !important; clear: both !important;
margin: 0 auto; margin: 0 auto;
" valign="top"> "
<div class="content" style=" valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -136,8 +147,14 @@
display: block; display: block;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
"> "
<table class="main" width="100%" cellpadding="0" cellspacing="0" style=" >
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -145,14 +162,20 @@
background-color: #fff; background-color: #fff;
margin: 0; margin: 0;
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
" bgcolor="#fff"> "
<tr style=" bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-wrap aligncenter" style=" >
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -160,23 +183,35 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" align="center" valign="top"> "
<table width="100%" cellpadding="0" cellspacing="0" style=" align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
width: 90%; width: 90%;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -185,21 +220,29 @@
margin: 0; margin: 0;
padding: 0 0 0px 0; padding: 0 0 0px 0;
text-align: left; text-align: left;
" valign="top"> "
<a href="https://manifold.markets" target="_blank"> valign="top"
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto" >
alt="Manifold Markets" /> <img
</a> src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td class="content-block aligncenter" style=" >
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -208,8 +251,13 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
" align="center" valign="top"> "
<table class="invoice" style=" align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -218,42 +266,59 @@
width: 80%; width: 80%;
margin: 40px auto; margin: 40px auto;
margin-top: 10px; margin-top: 10px;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" valign="top"> "
valign="top"
>
<div> <div>
<img src="{{commentorAvatarUrl}}" width="30" height="30" style=" <img
src="{{commentorAvatarUrl}}"
width="30"
height="30"
style="
border-radius: 30px; border-radius: 30px;
overflow: hidden; overflow: hidden;
vertical-align: middle; vertical-align: middle;
margin-right: 4px; margin-right: 4px;
" alt="" /> "
<span style="font-weight: bold">{{commentorName}}</span> alt=""
/>
<span style="font-weight: bold"
>{{commentorName}}</span
>
{{betDescription}} {{betDescription}}
</div> </div>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -261,29 +326,40 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" valign="top"> "
<div style=" valign="top"
>
<div
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<span style="white-space: pre-line">{{comment}}</span> >
<span style="white-space: pre-line"
>{{comment}}</span
>
</div> </div>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
>
<td style="padding: 20px 0 0 0; margin: 0"> <td style="padding: 20px 0 0 0; margin: 0">
<div align="center"> <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]--> <!--[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=" <a
href="{{marketUrl}}"
target="_blank"
style="
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
@ -301,16 +377,23 @@
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
mso-border-alt: none; mso-border-alt: none;
"> "
<span style=" >
<span
style="
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
line-height: 120%; line-height: 120%;
"><span style=" "
><span
style="
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
line-height: 18.8px; line-height: 18.8px;
">View comment</span></span> "
>View comment</span
></span
>
</a> </a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]--> <!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div> </div>
@ -323,7 +406,9 @@
</td> </td>
</tr> </tr>
</table> </table>
<div class="footer" style=" <div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -332,20 +417,28 @@
color: #999; color: #999;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
"> "
<table width="100%" style=" >
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="aligncenter content-block" style=" >
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -355,9 +448,14 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" align="center" valign="top"> "
align="center"
valign="top"
>
Questions? Come ask in Questions? Come ask in
<a href="https://discord.gg/eHQBNBqXuh" style=" <a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -365,26 +463,39 @@
color: #999; color: #999;
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
">our Discord</a>! Or, "
<a href="{{unsubscribeUrl}}" style=" >our Discord</a
color: inherit; >! Or,
text-decoration: none; <a
" target="_blank">click here to unsubscribe from this type of notification</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> </td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
</td> </td>
<td style=" <td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" valign="top"></td> "
valign="top"
></td>
</tr> </tr>
</table> </table>
</body> </body>
</html> </html>

View File

@ -1,491 +0,0 @@
<!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">
<a href="https://manifold.markets" target="_blank">
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
alt="Manifold Markets" />
</a>
</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 were following 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;
" />
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="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</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,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html style=" <html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
>
<head> <head>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
@ -32,52 +33,41 @@
body { body {
padding: 0 !important; padding: 0 !important;
} }
h1 { h1 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h2 { h2 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h3 { h3 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h4 { h4 {
font-weight: 800 !important; font-weight: 800 !important;
margin: 20px 0 5px !important; margin: 20px 0 5px !important;
} }
h1 { h1 {
font-size: 22px !important; font-size: 22px !important;
} }
h2 { h2 {
font-size: 18px !important; font-size: 18px !important;
} }
h3 { h3 {
font-size: 16px !important; font-size: 16px !important;
} }
.container { .container {
padding: 0 !important; padding: 0 !important;
width: 100% !important; width: 100% !important;
} }
.content { .content {
padding: 0 !important; padding: 0 !important;
} }
.content-wrap { .content-wrap {
padding: 10px !important; padding: 10px !important;
} }
.invoice { .invoice {
width: 100% !important; width: 100% !important;
} }
@ -85,7 +75,10 @@
</style> </style>
</head> </head>
<body itemscope itemtype="http://schema.org/EmailMessage" style=" <body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -96,29 +89,43 @@
line-height: 1.6em; line-height: 1.6em;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" bgcolor="#f6f6f6"> "
<table class="body-wrap" style=" bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" bgcolor="#f6f6f6"> "
<tr style=" bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" valign="top"></td> "
<td class="container" width="600" style=" valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -127,8 +134,12 @@
max-width: 600px !important; max-width: 600px !important;
clear: both !important; clear: both !important;
margin: 0 auto; margin: 0 auto;
" valign="top"> "
<div class="content" style=" valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -136,8 +147,14 @@
display: block; display: block;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
"> "
<table class="main" width="100%" cellpadding="0" cellspacing="0" style=" >
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -145,14 +162,20 @@
background-color: #fff; background-color: #fff;
margin: 0; margin: 0;
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
" bgcolor="#fff"> "
<tr style=" bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-wrap aligncenter" style=" >
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -160,23 +183,35 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" align="center" valign="top"> "
<table width="100%" cellpadding="0" cellspacing="0" style=" align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
width: 90%; width: 90%;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -185,22 +220,30 @@
margin: 0; margin: 0;
padding: 0 0 40px 0; padding: 0 0 40px 0;
text-align: left; text-align: left;
" valign="top"> "
<a href="https://manifold.markets" target="_blank"> valign="top"
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto" >
alt="Manifold Markets" /> <img
</a> src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
padding: 0; padding: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -209,18 +252,24 @@
margin: 0; margin: 0;
padding: 0 0 6px 0; padding: 0 0 6px 0;
text-align: left; text-align: left;
" valign="top"> "
valign="top"
>
{{creatorName}} asked {{creatorName}} asked
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -228,8 +277,12 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" valign="top"> "
<a href="{{url}}" style=" valign="top"
>
<a
href="{{url}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif; 'Lucida Grande', sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -242,18 +295,24 @@
color: #4337c9; color: #4337c9;
display: block; display: block;
text-decoration: none; text-decoration: none;
"> "
{{question}}</a> >
{{question}}</a
>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="content-block" style=" >
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -261,8 +320,12 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 0 0 0px; padding: 0 0 0px;
" valign="top"> "
<h2 class="aligncenter" style=" valign="top"
>
<h2
class="aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif; 'Lucida Grande', sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -272,19 +335,25 @@
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
margin: 10px 0 0; margin: 10px 0 0;
" align="center"> "
align="center"
>
Resolved {{outcome}} Resolved {{outcome}}
</h2> </h2>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td class="content-block aligncenter" style=" >
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -293,8 +362,13 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
" align="center" valign="top"> "
<table class="invoice" style=" align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -302,15 +376,19 @@
text-align: left; text-align: left;
width: 80%; width: 80%;
margin: 40px auto; margin: 40px auto;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
<td style=" >
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -318,105 +396,138 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" valign="top"> "
valign="top"
>
Dear {{name}}, Dear {{name}},
<br style=" <br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
<br style=" />
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
A market you bet in has been resolved! A market you bet in has been resolved!
<br style=" <br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
<br style=" />
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
Your investment was Your investment was
<span style="font-weight: bold">{{investment}}</span>. <span style="font-weight: bold"
<br style=" >M$ {{investment}}</span
>.
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
<br style=" />
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
Your payout is Your payout is
<span style="font-weight: bold">{{payout}}</span>. <span style="font-weight: bold"
<br style=" >M$ {{payout}}</span
>.
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
<br style=" />
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
Thanks, Thanks,
<br style=" <br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
Manifold Team Manifold Team
<br style=" <br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
<br style=" />
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" /> "
/>
</td> </td>
</tr> </tr>
<tr style=" <tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
"> "
>
<td style="padding: 10px 0 0 0; margin: 0"> <td style="padding: 10px 0 0 0; margin: 0">
<div align="center"> <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]--> <!--[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=" <a
href="{{url}}"
target="_blank"
style="
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
@ -434,16 +545,23 @@
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
mso-border-alt: none; mso-border-alt: none;
"> "
<span style=" >
<span
style="
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
line-height: 120%; line-height: 120%;
"><span style=" "
><span
style="
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
line-height: 18.8px; line-height: 18.8px;
">View market</span></span> "
>View market</span
></span
>
</a> </a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]--> <!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div> </div>
@ -456,7 +574,9 @@
</td> </td>
</tr> </tr>
</table> </table>
<div class="footer" style=" <div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -465,20 +585,28 @@
color: #999; color: #999;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
"> "
<table width="100%" style=" >
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<tr style=" >
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
"> "
<td class="aligncenter content-block" style=" >
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -488,9 +616,14 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" align="center" valign="top"> "
align="center"
valign="top"
>
Questions? Come ask in Questions? Come ask in
<a href="https://discord.gg/eHQBNBqXuh" style=" <a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -498,26 +631,39 @@
color: #999; color: #999;
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
">our Discord</a>! Or, "
<a href="{{unsubscribeUrl}}" style=" >our Discord</a
color: inherit; >! Or,
text-decoration: none; <a
" target="_blank">click here to unsubscribe from this type of notification</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> </td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
</td> </td>
<td style=" <td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" valign="top"></td> "
valign="top"
></td>
</tr> </tr>
</table> </table>
</body> </body>
</html> </html>

View File

@ -1,354 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>New market from {{creatorName}}</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center"
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto"
src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align: top" width="100%">
<tbody>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
{{creatorName}}, (who you're following) just created a new market, check it out!</span></p>
</div>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{questionUrl}}">
<img alt="{{questionTitle}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{questionImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{questionUrl}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0 0 20px 0;
text-align: center;
">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
width="100%">
<tbody>
<tr>
<td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0"
role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
">
<div style="
font-family: Ubuntu, Helvetica, Arial,
sans-serif;
font-size: 11px;
line-height: 22px;
text-align: center;
color: #000000;
">
<p style="margin: 10px 0">
This e-mail has been sent to
{{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View File

@ -1,397 +0,0 @@
<!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>New unique traders on your market</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">
<a href="https://manifold.markets" target="_blank">
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
alt="Manifold Markets" />
</a>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Your market <a href='{{marketUrl}}'>{{marketTitle}}</a> just got its first trade from a user!
<br/>
<br/>
We sent you a <span style='color: #11b981'>{{bonusString}}</span> bonus for
creating a market that appeals to others, and we'll do so for each new trader.
<br/>
<br/>
Keep up the good work and check out your newest trader below!
</span></p>
</div>
</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-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor1AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor1Name}}</span>
{{bet1Description}}
</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 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="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</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,501 +0,0 @@
<!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>New unique traders on your market</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">
<a href="https://manifold.markets" target="_blank">
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
alt="Manifold Markets" />
</a>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Your market <a href='{{marketUrl}}'>{{marketTitle}}</a> has attracted {{totalPredictors}} total traders!
<br/>
<br/>
We sent you a <span style='color: #11b981'>{{bonusString}}</span> bonus for getting {{newPredictors}} new traders,
and we'll continue to do so for each new trader, (although we won't send you any more emails about it for this market).
<br/>
<br/>
Keep up the good work and check out your newest traders below!
</span></p>
</div>
</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-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor1AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor1Name}}</span>
{{bet1Description}}
</div>
</td>
</tr><tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor2AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor2Name}}</span>
{{bet2Description}}
</div>
</td>
</tr><tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor3AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor3Name}}</span>
{{bet3Description}}
</div>
</td>
</tr><tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor4AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor4Name}}</span>
{{bet4Description}}
</div>
</td>
</tr><tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor5AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor5Name}}</span>
{{bet5Description}}
</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 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="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</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,14 +1,16 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" <html
xmlns:o="urn:schemas-microsoft-com:office:office"> xmlns="http://www.w3.org/1999/xhtml"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<head> <head>
<title>Manifold Markets 7th Day Anniversary Gift!</title> <title>7th Day Anniversary Gift!</title>
<!--[if !mso]><!--> <!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]--> <!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css"> <style type="text/css">
#outlook a { #outlook a {
padding: 0; padding: 0;
@ -49,12 +51,14 @@
<o:AllowPNG /> <o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch> <o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml> </noscript
</noscript> >z
<![endif]--> <![endif]-->
<!--[if lte mso 11]> <!--[if lte mso 11]>
<style type="text/css"> <style type="text/css">
.mj-outlook-group-fix { width:100% !important; } .mj-outlook-group-fix {
width: 100% !important;
}
</style> </style>
<![endif]--> <![endif]-->
<style type="text/css"> <style type="text/css">
@ -90,135 +94,314 @@
</style> </style>
</head> </head>
<body style="word-spacing:normal;background-color:#F4F4F4;"> <body style="word-spacing: normal; background-color: #f4f4f4">
<div style="background-color:#F4F4F4;"> <div style="background-color: #f4f4f4">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;"> <div
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="
style="background:#ffffff;background-color:#ffffff;width:100%;"> background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
"
>
<table
align="center"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%"
>
<tbody> <tbody>
<tr> <tr>
<td <td
style="direction:ltr;font-size:0px;padding:0px 0px 0px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;text-align:center;"> style="
direction: ltr;
font-size: 0px;
padding: 0px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 0px;
text-align: center;
"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--> <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" <div
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" style="
width="100%"> font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="vertical-align: top"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td align="center" <td
style="font-size:0px;padding:0px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;"> align="center"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="
style="border-collapse:collapse;border-spacing:0px;"> font-size: 0px;
padding: 0px 25px 0px 25px;
padding-top: 0px;
padding-right: 25px;
padding-bottom: 0px;
padding-left: 25px;
word-break: break-word;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-collapse: collapse;
border-spacing: 0px;
"
>
<tbody> <tbody>
<tr> <tr>
<td style="width:550px;"><a href="https://manifold.markets/home" target="_blank"><img <td style="width: 550px">
alt="" height="auto" src="https://i.imgur.com/8EP8Y8q.gif" <a
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" href="https://manifold.markets/home"
width="550"></a></td> target="_blank"
><img
alt=""
height="auto"
src="https://03jlj.mjt.lu/img/03jlj/b/u71/sjvu.gif"
style="
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
"
width="550"
/></a>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" <td
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;"> align="left"
style="
font-size: 0px;
padding: 10px 25px;
padding-top: 0px;
padding-bottom: 0px;
word-break: break-word;
"
>
<div <div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;"> style="
<p class="text-build-content" font-family: Arial, sans-serif;
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;" font-size: 18px;
data-testid="4XoHRGw1Y"><span letter-spacing: normal;
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;"> line-height: 1;
Hi {{name}},</span></p> text-align: left;
</div> color: #000000;
</td> "
</tr> >
<tr> <p
<td align="left" class="text-build-content"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;"> style="
<div text-align: center;
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;"> margin: 10px 0;
<p class="text-build-content" margin-top: 10px;
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;" margin-bottom: 10px;
data-testid="4XoHRGw1Y"><span "
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">Thanks for data-testid="4XoHRGw1Y"
using Manifold Markets. Running low >
on mana (M$)? Click the link below to receive a one time gift of M$500!</span></p> <span
</div> style="
</td> color: #000000;
</tr> font-family: Arial, Helvetica, sans-serif;
<tr> font-size: 18px;
<td> "
<p></p> >Hopefully you haven&#39;t gambled all your M$
</td> away already... but if you have I bring good
</tr> news! Click the link below to recieve a one time
<tr> gift of M$ 500 to your account!</span
<td align="center"> >
<table cellspacing="0" cellpadding="0">
<tr>
<td>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 2px;" bgcolor="#4337c9">
<a href="{{manalink}}" target="_blank"
style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:bold;display: inline-block;">
Claim M$500
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
data-testid="3Q8BP69fq"><span style="font-family:Arial, sans-serif;font-size:18px;">Did
you know, besides making correct predictions, there are
plenty of other ways to earn mana?</span></p>
<ul>
<li style="line-height:23px;"><span
style="font-family:Arial, sans-serif;font-size:18px;">Predicting
consecutive days to earn streak rewards</span></li>
<li style="line-height:23px;"><span
style="font-family:Arial, sans-serif;font-size:18px;">Receiving
tips on comments and markets</span></li>
<li style="line-height:23px;"><span
style="font-family:Arial, sans-serif;font-size:18px;">Unique
trader bonus for each user who trades on your
markets</span></li>
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
class="link-build-content" style="color:inherit;; text-decoration: none;"
target="_blank" href="https://manifold.markets/referrals"><span
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Referring
friends</u></span></a></span></li>
<li style="line-height:23px;"><a class="link-build-content"
style="color:inherit;; text-decoration: none;" target="_blank"
href="https://manifold.markets/group/bugs?s=most-traded"><span
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Reporting
bugs</u></span></a><span style="font-family:Arial, sans-serif;font-size:18px;">
and </span><a class="link-build-content" style="color:inherit;; text-decoration: none;"
target="_blank"
href="https://manifold.markets/group/manifold-features-25bad7c7792e/chat?s=most-traded"><span
style="color:#55575d;font-family:Arial;font-size:18px;"><u>giving
feedback</u></span></a></li>
</ul>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;">&nbsp;</p>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span>
</p> </p>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span </div>
style="color:#000000;font-family:Arial;font-size:18px;">David </td>
from Manifold</span></p> </tr>
<p class="text-build-content" data-testid="3Q8BP69fq" <tr>
style="margin: 10px 0; margin-bottom: 10px;">&nbsp;</p> <td
align="center"
style="
font-size: 0px;
padding: 10px 25px 25px 25px;
padding-top: 10px;
padding-right: 25px;
padding-bottom: 25px;
padding-left: 25px;
word-break: break-word;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-collapse: collapse;
border-spacing: 0px;
"
>
<tbody>
<tr>
<td style="width: 550px">
<a href="{{manalink}}" target="_blank">
<img
alt="Get M$500"
height="auto"
src="https://03jlj.mjt.lu/img/03jlj/b/u71/sjgt.png"
style="
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
"
width="550"
/></a>
<< /td>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td
align="left"
style="
font-size: 0px;
padding: 10px 25px;
padding-top: 0px;
padding-bottom: 0px;
word-break: break-word;
"
>
<div
style="
font-family: Arial, sans-serif;
font-size: 18px;
letter-spacing: normal;
line-height: 1;
text-align: left;
color: #000000;
"
>
<p
class="text-build-content"
style="
line-height: 23px;
text-align: center;
margin: 10px 0;
margin-top: 10px;
"
data-testid="3Q8BP69fq"
>
<span
style="
color: #000000;
font-family: Arial, Helvetica, sans-serif;
font-size: 18px;
"
>If you are still engaging with our markets then
at this point you might as well join our </span
><a
class="link-build-content"
style="color: inherit; text-decoration: none"
target="_blank"
href="https://discord.gg/VARzUpyCSa"
><span
style="
color: #0c21bf;
font-family: Arial;
font-size: 18px;
"
><u>Discord server</u></span
><span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
><u>.</u>
</span></a
><span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
>You can always leave if you dont like it but
I&#39;d be willing to make a market betting
you&#39;ll stay.</span
>
</p>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0"
></p>
<br />
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0"
>
<span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
>Cheers,</span
>
</p>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0"
>
<span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
>David from Manifold</span
>
</p>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0; margin-bottom: 10px"
></p>
</div> </div>
</td> </td>
</tr> </tr>
@ -232,70 +415,91 @@
</table> </table>
</div> </div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;"> <div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"> <table
align="center"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="width: 100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="direction:ltr;font-size:0px;padding:0 0 20px 0;text-align:center;"> <td
style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--> <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" <div
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" style="
width="100%"> font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td align="center" style="font-size:0px;padding:0px;word-break:break-word;"> <td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" <table
style="border-collapse:collapse;border-spacing:0px;"> border="0"
</div> cellpadding="0"
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> cellspacing="0"
<div style="margin:0px auto;max-width:600px;"> role="presentation"
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%"
style="width:100%;"> >
<tbody> <tbody>
<tr> <tr>
<td style="direction:ltr;font-size:0px;padding:20px 0px 20px 0px;text-align:center;"> <td
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--> align="center"
<div class="mj-column-per-100 mj-outlook-group-fix" style="
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding:0;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px; font-size: 0px;
padding: 10px 25px; padding: 10px 25px;
padding-top: 0px;
padding-bottom: 0px;
word-break: break-word; word-break: break-word;
"> "
<div style=" >
font-family: Ubuntu, Helvetica, Arial, <div
sans-serif; style="
font-family: Arial, sans-serif;
font-size: 11px; font-size: 11px;
letter-spacing: normal;
line-height: 22px; line-height: 22px;
text-align: center; text-align: center;
color: #000000; color: #000000;
"> "
>
<p style="margin: 10px 0"> <p style="margin: 10px 0">
This e-mail has been sent to {{name}}, This e-mail has been sent to {{name}},
<a href="{{unsubscribeUrl}}" style=" <a
href="{{unsubscribeLink}}"
style="
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>. "
target="_blank"
>click here to unsubscribe</a
>.
</p> </p>
</div> </div>
</td> </td>
</tr> </tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</td> </td>
@ -312,5 +516,4 @@
<!--[if mso | IE]></td></tr></table><![endif]--> <!--[if mso | IE]></td></tr></table><![endif]-->
</div> </div>
</body> </body>
</html> </html>

Some files were not shown because too many files have changed in this diff Show More