Compare commits
57 Commits
main
...
twitch-lan
Author | SHA1 | Date | |
---|---|---|---|
|
34ad32430a | ||
|
1ba1acdd1d | ||
|
bfef873b53 | ||
|
6b05561517 | ||
|
050c80609b | ||
|
d710a9b669 | ||
|
74765281b3 | ||
|
b2362769a5 | ||
|
488b442f66 | ||
|
04a8509212 | ||
|
eb630dfa46 | ||
|
e3c307bbda | ||
|
539ef017e0 | ||
|
ba106a258a | ||
|
4af5ed4bd4 | ||
|
346d24868e | ||
|
dd42b641ac | ||
|
937ff991df | ||
|
2156784348 | ||
|
0cd9e52689 | ||
|
934156c770 | ||
|
cfb2e1fc3a | ||
|
c8fd6940c1 | ||
|
b4394c8663 | ||
|
57090d8480 | ||
|
2282cb544f | ||
|
a7a45d1968 | ||
|
5333f881ef | ||
|
0428708e82 | ||
|
85473b17c3 | ||
|
c45faf6b80 | ||
|
830ef498d8 | ||
|
ab3c9d250d | ||
|
32113a29e3 | ||
|
33f593ebf7 | ||
|
e79235afef | ||
|
fd0aa30195 | ||
|
310d9e0ccc | ||
|
b48cd939bf | ||
|
ed9d0cfd95 | ||
|
c753313546 | ||
|
2e0957d739 | ||
|
acf9640b7a | ||
|
33c55f43bf | ||
|
2fcfeb786a | ||
|
0743d4ae1c | ||
|
d44482aae3 | ||
|
7899d71af6 | ||
|
1ed46948b1 | ||
|
edb8591cab | ||
|
18b0051450 | ||
|
54f7b740dc | ||
|
3ce2bdc9f1 | ||
|
5904038890 | ||
|
6929d643e1 | ||
|
50279bd864 | ||
|
2e95ac449d |
43
.github/workflows/lint.yml
vendored
43
.github/workflows/lint.yml
vendored
|
@ -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 }}
|
17
.github/workflows/merge-main-into-main2.yml
vendored
17
.github/workflows/merge-main-into-main2.yml
vendored
|
@ -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 }}
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: ['lodash', 'unused-imports'],
|
||||
plugins: ['lodash'],
|
||||
extends: ['eslint:recommended'],
|
||||
ignorePatterns: ['lib'],
|
||||
env: {
|
||||
|
@ -26,7 +26,6 @@ module.exports = {
|
|||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,30 +1,33 @@
|
|||
import { getCpmmLiquidity } from './calculate-cpmm'
|
||||
import { addCpmmLiquidity, getCpmmLiquidity } from './calculate-cpmm'
|
||||
import { CPMMContract } from './contract'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { User } from './user'
|
||||
|
||||
export const getNewLiquidityProvision = (
|
||||
userId: string,
|
||||
user: User,
|
||||
amount: number,
|
||||
contract: CPMMContract,
|
||||
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 = {
|
||||
id: newLiquidityProvisionId,
|
||||
userId: userId,
|
||||
userId: user.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
pool,
|
||||
p,
|
||||
pool: newPool,
|
||||
p: newP,
|
||||
liquidity,
|
||||
createdTime: Date.now(),
|
||||
}
|
||||
|
||||
const newTotalLiquidity = (totalLiquidity ?? 0) + amount
|
||||
const newSubsidyPool = (subsidyPool ?? 0) + amount
|
||||
|
||||
return { newLiquidityProvision, newTotalLiquidity, newSubsidyPool }
|
||||
return { newLiquidityProvision, newPool, newP, newTotalLiquidity }
|
||||
}
|
||||
|
|
|
@ -15,12 +15,6 @@ import { Answer } from './answer'
|
|||
|
||||
export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @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(
|
||||
providerId: string,
|
||||
|
@ -57,7 +51,7 @@ export function getAnteBets(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const yesBet: NormalizedBet = {
|
||||
const yesBet: Bet = {
|
||||
id: yesAnteId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -71,7 +65,7 @@ export function getAnteBets(
|
|||
fees: noFees,
|
||||
}
|
||||
|
||||
const noBet: NormalizedBet = {
|
||||
const noBet: Bet = {
|
||||
id: noAnteId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -99,7 +93,7 @@ export function getFreeAnswerAnte(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const anteBet: NormalizedBet = {
|
||||
const anteBet: Bet = {
|
||||
id: anteBetId,
|
||||
userId: anteBettorId,
|
||||
contractId: contract.id,
|
||||
|
@ -129,7 +123,7 @@ export function getMultipleChoiceAntes(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const bets: NormalizedBet[] = answers.map((answer, i) => ({
|
||||
const bets: Bet[] = answers.map((answer, i) => ({
|
||||
id: betDocIds[i],
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -179,7 +173,7 @@ export function getNumericAnte(
|
|||
range(0, bucketCount).map((_, i) => [i, betAnte])
|
||||
)
|
||||
|
||||
const anteBet: NormalizedBet<NumericBet> = {
|
||||
const anteBet: NumericBet = {
|
||||
id: newBetId,
|
||||
userId: anteBettorId,
|
||||
contractId: contract.id,
|
||||
|
|
123
common/badge.ts
123
common/badge.ts
|
@ -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
|
||||
}
|
|
@ -3,12 +3,6 @@ import { Fees } from './fees'
|
|||
export type Bet = {
|
||||
id: string
|
||||
userId: string
|
||||
|
||||
// denormalized for bet lists
|
||||
userAvatarUrl?: string
|
||||
userUsername: string
|
||||
userName: string
|
||||
|
||||
contractId: string
|
||||
createdTime: number
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { groupBy, mapValues, sumBy } from 'lodash'
|
||||
import { sum, groupBy, mapValues, sumBy } from 'lodash'
|
||||
import { LimitBet } from './bet'
|
||||
|
||||
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { computeFills } from './new-bet'
|
||||
import { binarySearch } from './util/algos'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export type CpmmState = {
|
||||
pool: { [outcome: string]: number }
|
||||
|
@ -146,8 +147,7 @@ function calculateAmountToBuyShares(
|
|||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
// Search for amount between bounds (0, shares).
|
||||
// Min share price is M$0, and max is M$1 each.
|
||||
|
@ -157,8 +157,7 @@ function calculateAmountToBuyShares(
|
|||
amount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
const totalShares = sumBy(takers, (taker) => taker.shares)
|
||||
|
@ -170,8 +169,7 @@ export function calculateCpmmSale(
|
|||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
if (Math.round(shares) < 0) {
|
||||
throw new Error('Cannot sell non-positive shares')
|
||||
|
@ -182,17 +180,15 @@ export function calculateCpmmSale(
|
|||
state,
|
||||
shares,
|
||||
oppositeOutcome,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills(
|
||||
const { cpmmState, makers, takers, totalFees } = computeFills(
|
||||
oppositeOutcome,
|
||||
buyAmount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
// Transform buys of opposite outcome into sells.
|
||||
|
@ -215,7 +211,6 @@ export function calculateCpmmSale(
|
|||
fees: totalFees,
|
||||
makers,
|
||||
takers: saleTakers,
|
||||
ordersToCancel,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,16 +218,9 @@ export function getCpmmProbabilityAfterSale(
|
|||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
const { cpmmState } = calculateCpmmSale(
|
||||
state,
|
||||
shares,
|
||||
outcome,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets)
|
||||
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
}
|
||||
|
||||
|
@ -266,22 +254,48 @@ export function addCpmmLiquidity(
|
|||
return { newPool, liquidity, newP }
|
||||
}
|
||||
|
||||
export function getCpmmLiquidityPoolWeights(liquidities: LiquidityProvision[]) {
|
||||
const userAmounts = groupBy(liquidities, (w) => w.userId)
|
||||
const totalAmount = sumBy(liquidities, (w) => w.amount)
|
||||
const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => {
|
||||
const oldLiquidity = getCpmmLiquidity(l.pool, p)
|
||||
|
||||
return mapValues(
|
||||
userAmounts,
|
||||
(amounts) => sumBy(amounts, (w) => w.amount) / totalAmount
|
||||
const newPool = addObjects(l.pool, { YES: l.amount, NO: l.amount })
|
||||
const newLiquidity = getCpmmLiquidity(newPool, p)
|
||||
|
||||
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(
|
||||
userId: string,
|
||||
state: CpmmState,
|
||||
liquidities: LiquidityProvision[]
|
||||
liquidities: LiquidityProvision[],
|
||||
excludeAntes: boolean
|
||||
) {
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const weights = getCpmmLiquidityPoolWeights(state, liquidities, excludeAntes)
|
||||
const userWeight = weights[userId] ?? 0
|
||||
|
||||
return mapValues(state.pool, (shares) => userWeight * shares)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { maxBy, partition, sortBy, sum, sumBy } from 'lodash'
|
||||
import { maxBy, sortBy, sum, sumBy } from 'lodash'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import {
|
||||
calculateCpmmSale,
|
||||
|
@ -78,8 +78,7 @@ export function calculateShares(
|
|||
export function calculateSaleAmount(
|
||||
contract: Contract,
|
||||
bet: Bet,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
|
@ -88,8 +87,7 @@ export function calculateSaleAmount(
|
|||
contract,
|
||||
Math.abs(bet.shares),
|
||||
bet.outcome as 'YES' | 'NO',
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
unfilledBets
|
||||
).saleValue
|
||||
: calculateDpmSaleAmount(contract, bet)
|
||||
}
|
||||
|
@ -104,16 +102,14 @@ export function getProbabilityAfterSale(
|
|||
contract: Contract,
|
||||
outcome: string,
|
||||
shares: number,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
unfilledBets: LimitBet[]
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmProbabilityAfterSale(
|
||||
contract,
|
||||
shares,
|
||||
outcome as 'YES' | 'NO',
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
unfilledBets
|
||||
)
|
||||
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
||||
}
|
||||
|
@ -144,22 +140,17 @@ function getCpmmInvested(yourBets: Bet[]) {
|
|||
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
|
||||
totalShares[outcome] = (totalShares[outcome] ?? 0) + shares
|
||||
totalSpent[outcome] = (totalSpent[outcome] ?? 0) + amount
|
||||
} else if (amount < 0) {
|
||||
const averagePrice = position === 0 ? 0 : spent / position
|
||||
totalShares[outcome] = position + shares
|
||||
totalSpent[outcome] = spent + averagePrice * shares
|
||||
const averagePrice = totalSpent[outcome] / totalShares[outcome]
|
||||
totalShares[outcome] = totalShares[outcome] + shares
|
||||
totalSpent[outcome] = totalSpent[outcome] + averagePrice * shares
|
||||
}
|
||||
}
|
||||
|
||||
return sum([0, ...Object.values(totalSpent)])
|
||||
return sum(Object.values(totalSpent))
|
||||
}
|
||||
|
||||
function getDpmInvested(yourBets: Bet[]) {
|
||||
|
@ -178,8 +169,6 @@ function getDpmInvested(yourBets: Bet[]) {
|
|||
})
|
||||
}
|
||||
|
||||
export type ContractBetMetrics = ReturnType<typeof getContractBetMetrics>
|
||||
|
||||
export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
||||
const { resolution } = contract
|
||||
const isCpmm = contract.mechanism === 'cpmm-1'
|
||||
|
@ -216,8 +205,9 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
}
|
||||
}
|
||||
|
||||
const netPayout = payout - loan
|
||||
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(
|
||||
|
@ -226,8 +216,8 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
|
||||
return {
|
||||
invested,
|
||||
loan,
|
||||
payout,
|
||||
netPayout,
|
||||
profit,
|
||||
profitPercent,
|
||||
totalShares,
|
||||
|
@ -238,8 +228,8 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
export function getContractBetNullMetrics() {
|
||||
return {
|
||||
invested: 0,
|
||||
loan: 0,
|
||||
payout: 0,
|
||||
netPayout: 0,
|
||||
profit: 0,
|
||||
profitPercent: 0,
|
||||
totalShares: {} as { [outcome: string]: number },
|
||||
|
@ -260,43 +250,3 @@ export function getTopAnswer(
|
|||
)
|
||||
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 }
|
||||
}
|
||||
|
|
|
@ -576,7 +576,7 @@ Work towards sustainable, systemic change.`,
|
|||
|
||||
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',
|
||||
|
@ -589,15 +589,6 @@ CaRLA uses legal advocacy and education to ensure all cities comply with their o
|
|||
|
||||
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) => {
|
||||
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
export type AnyCommentType = OnContract | OnGroup | OnPost
|
||||
export type AnyCommentType = OnContract | OnGroup
|
||||
|
||||
// Currently, comments are created after the bet, not atomically with the bet.
|
||||
// They're uniquely identified by the pair contractId/betId.
|
||||
|
@ -18,39 +18,21 @@ export type Comment<T extends AnyCommentType = AnyCommentType> = {
|
|||
userName: string
|
||||
userUsername: string
|
||||
userAvatarUrl?: string
|
||||
bountiesAwarded?: number
|
||||
} & T
|
||||
|
||||
export type OnContract = {
|
||||
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
|
||||
answerOutcome?: string
|
||||
betId?: string
|
||||
}
|
||||
|
||||
export type OnGroup = {
|
||||
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>
|
||||
|
|
|
@ -27,10 +27,10 @@ export function contractMetrics(contract: Contract) {
|
|||
export function contractTextDetails(contract: Contract) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const dayjs = require('dayjs')
|
||||
const { closeTime, groupLinks } = contract
|
||||
const { closeTime, tags } = contract
|
||||
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
||||
|
||||
const groupHashtags = groupLinks?.map((g) => `#${g.name.replace(/ /g, '')}`)
|
||||
const hashtags = tags.map((tag) => `#${tag}`)
|
||||
|
||||
return (
|
||||
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
||||
|
@ -40,7 +40,7 @@ export function contractTextDetails(contract: Contract) {
|
|||
).format('MMM D, h:mma')}`
|
||||
: '') +
|
||||
` • ${volumeLabel}` +
|
||||
(groupHashtags ? ` • ${groupHashtags.join(' ')}` : '')
|
||||
(hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '')
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,6 @@ export const getOpenGraphProps = (contract: Contract) => {
|
|||
creatorAvatarUrl,
|
||||
description,
|
||||
numericValue,
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +103,6 @@ export type OgCardProps = {
|
|||
creatorUsername: string
|
||||
creatorAvatarUrl?: string
|
||||
numericValue?: string
|
||||
resolution?: string
|
||||
}
|
||||
|
||||
export function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
|
||||
|
@ -115,32 +113,22 @@ export function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
|
|||
creatorOutcome,
|
||||
acceptorOutcome,
|
||||
} = challenge || {}
|
||||
const {
|
||||
probability,
|
||||
numericValue,
|
||||
resolution,
|
||||
creatorAvatarUrl,
|
||||
question,
|
||||
metadata,
|
||||
creatorUsername,
|
||||
creatorName,
|
||||
} = props
|
||||
const { userName, userAvatarUrl } = acceptances?.[0] ?? {}
|
||||
|
||||
const probabilityParam =
|
||||
probability === undefined
|
||||
props.probability === undefined
|
||||
? ''
|
||||
: `&probability=${encodeURIComponent(probability ?? '')}`
|
||||
: `&probability=${encodeURIComponent(props.probability ?? '')}`
|
||||
|
||||
const numericValueParam =
|
||||
numericValue === undefined
|
||||
props.numericValue === undefined
|
||||
? ''
|
||||
: `&numericValue=${encodeURIComponent(numericValue ?? '')}`
|
||||
: `&numericValue=${encodeURIComponent(props.numericValue ?? '')}`
|
||||
|
||||
const creatorAvatarUrlParam =
|
||||
creatorAvatarUrl === undefined
|
||||
props.creatorAvatarUrl === undefined
|
||||
? ''
|
||||
: `&creatorAvatarUrl=${encodeURIComponent(creatorAvatarUrl ?? '')}`
|
||||
: `&creatorAvatarUrl=${encodeURIComponent(props.creatorAvatarUrl ?? '')}`
|
||||
|
||||
const challengeUrlParams = challenge
|
||||
? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` +
|
||||
|
@ -148,21 +136,16 @@ export function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
|
|||
`&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)}` +
|
||||
`?question=${encodeURIComponent(props.question)}` +
|
||||
probabilityParam +
|
||||
numericValueParam +
|
||||
`&metadata=${encodeURIComponent(metadata)}` +
|
||||
`&creatorName=${encodeURIComponent(creatorName)}` +
|
||||
`&metadata=${encodeURIComponent(props.metadata)}` +
|
||||
`&creatorName=${encodeURIComponent(props.creatorName)}` +
|
||||
creatorAvatarUrlParam +
|
||||
`&creatorUsername=${encodeURIComponent(creatorUsername)}` +
|
||||
challengeUrlParams +
|
||||
resolutionUrlParam
|
||||
`&creatorUsername=${encodeURIComponent(props.creatorUsername)}` +
|
||||
challengeUrlParams
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ export type AnyOutcomeType =
|
|||
| PseudoNumeric
|
||||
| FreeResponse
|
||||
| Numeric
|
||||
|
||||
export type AnyContractType =
|
||||
| (CPMM & Binary)
|
||||
| (CPMM & PseudoNumeric)
|
||||
|
@ -50,7 +49,6 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
volume: number
|
||||
volume24Hours: number
|
||||
volume7Days: number
|
||||
elasticity: number
|
||||
|
||||
collectedFees: Fees
|
||||
|
||||
|
@ -59,14 +57,8 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
uniqueBettorIds?: string[]
|
||||
uniqueBettorCount?: number
|
||||
popularityScore?: number
|
||||
dailyScore?: number
|
||||
followerCount?: number
|
||||
featuredOnHomeRank?: number
|
||||
likedByUserIds?: string[]
|
||||
likedByUserCount?: number
|
||||
flaggedByUsernames?: string[]
|
||||
openCommentBounties?: number
|
||||
unlistedById?: string
|
||||
} & T
|
||||
|
||||
export type BinaryContract = Contract & Binary
|
||||
|
@ -92,14 +84,7 @@ export type CPMM = {
|
|||
mechanism: 'cpmm-1'
|
||||
pool: { [outcome: string]: number }
|
||||
p: number // probability constant in y^p * n^(1-p) = k
|
||||
totalLiquidity: number // for historical reasons, this the total subsidy amount added in M$
|
||||
subsidyPool: number // current value of subsidy pool in M$
|
||||
prob: number
|
||||
probChanges: {
|
||||
day: number
|
||||
week: number
|
||||
month: number
|
||||
}
|
||||
totalLiquidity: number // in M$
|
||||
}
|
||||
|
||||
export type Binary = {
|
||||
|
@ -155,7 +140,7 @@ export const OUTCOME_TYPES = [
|
|||
'NUMERIC',
|
||||
] as const
|
||||
|
||||
export const MAX_QUESTION_LENGTH = 240
|
||||
export const MAX_QUESTION_LENGTH = 480
|
||||
export const MAX_DESCRIPTION_LENGTH = 16000
|
||||
export const MAX_TAG_LENGTH = 60
|
||||
|
||||
|
|
|
@ -7,14 +7,11 @@ 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 REFERRAL_AMOUNT = econ?.REFERRAL_AMOUNT ?? 500
|
||||
|
||||
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
|
||||
econ?.BETTING_STREAK_BONUS_AMOUNT ?? 10
|
||||
export const BETTING_STREAK_BONUS_MAX = econ?.BETTING_STREAK_BONUS_MAX ?? 50
|
||||
export const BETTING_STREAK_RESET_HOUR = econ?.BETTING_STREAK_RESET_HOUR ?? 0
|
||||
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
|
||||
|
|
|
@ -21,10 +21,7 @@ export function isWhitelisted(email?: string) {
|
|||
}
|
||||
|
||||
// TODO: Before open sourcing, we should turn these into env vars
|
||||
export function isAdmin(email?: string) {
|
||||
if (!email) {
|
||||
return false
|
||||
}
|
||||
export function isAdmin(email: string) {
|
||||
return ENV_CONFIG.adminEmails.includes(email)
|
||||
}
|
||||
|
||||
|
@ -37,11 +34,6 @@ export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
|||
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
||||
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
|
||||
export const CORS_ORIGIN_MANIFOLD = new RegExp(
|
||||
'^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$'
|
||||
|
|
|
@ -16,6 +16,4 @@ export const DEV_CONFIG: EnvConfig = {
|
|||
cloudRunId: 'w3txbmd3ba',
|
||||
cloudRunRegion: 'uc',
|
||||
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
|
||||
twitchBotEndpoint: 'https://dev-twitch-bot.manifold.markets',
|
||||
sprigEnvironmentId: 'Tu7kRZPm7daP',
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ export type EnvConfig = {
|
|||
domain: string
|
||||
firebaseConfig: FirebaseConfig
|
||||
amplitudeApiKey?: string
|
||||
twitchBotEndpoint?: string
|
||||
sprigEnvironmentId?: string
|
||||
|
||||
// IDs for v2 cloud functions -- find these by deploying a cloud function and
|
||||
// examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app
|
||||
|
@ -17,9 +15,6 @@ export type EnvConfig = {
|
|||
|
||||
// Branding
|
||||
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
|
||||
navbarLogoPath?: string
|
||||
newQuestionPlaceholders: string[]
|
||||
|
@ -41,7 +36,6 @@ export type Economy = {
|
|||
BETTING_STREAK_BONUS_MAX?: number
|
||||
BETTING_STREAK_RESET_HOUR?: number
|
||||
FREE_MARKETS_PER_USER_MAX?: number
|
||||
COMMENT_BOUNTY_AMOUNT?: number
|
||||
}
|
||||
|
||||
type FirebaseConfig = {
|
||||
|
@ -58,7 +52,6 @@ type FirebaseConfig = {
|
|||
export const PROD_CONFIG: EnvConfig = {
|
||||
domain: 'manifold.markets',
|
||||
amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15',
|
||||
sprigEnvironmentId: 'sQcrq9TDqkib',
|
||||
|
||||
firebaseConfig: {
|
||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||
|
@ -70,7 +63,6 @@ export const PROD_CONFIG: EnvConfig = {
|
|||
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
|
||||
measurementId: 'G-SSFK1Q138D',
|
||||
},
|
||||
twitchBotEndpoint: 'https://twitch-bot.manifold.markets',
|
||||
cloudRunId: 'nggbo3neva',
|
||||
cloudRunRegion: 'uc',
|
||||
adminEmails: [
|
||||
|
@ -81,15 +73,10 @@ export const PROD_CONFIG: EnvConfig = {
|
|||
'manticmarkets@gmail.com', // Manifold
|
||||
'iansphilips@gmail.com', // Ian
|
||||
'd4vidchee@gmail.com', // D4vid
|
||||
'federicoruizcassarino@gmail.com', // Fede
|
||||
'ingawei@gmail.com', //Inga
|
||||
],
|
||||
visibility: 'PUBLIC',
|
||||
|
||||
moneyMoniker: 'M$',
|
||||
bettor: 'trader',
|
||||
pastBet: 'trade',
|
||||
presentBet: 'trade',
|
||||
navbarLogoPath: '',
|
||||
faviconPath: '/favicon.ico',
|
||||
newQuestionPlaceholders: [
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
export const FLAT_TRADE_FEE = 0.1 // M$0.1
|
||||
|
||||
export const PLATFORM_FEE = 0
|
||||
export const CREATOR_FEE = 0
|
||||
export const LIQUIDITY_FEE = 0
|
||||
|
|
|
@ -2,8 +2,3 @@ export type Follow = {
|
|||
userId: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export type ContractFollow = {
|
||||
id: string // user id
|
||||
createdTime: number
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export type GlobalConfig = {
|
||||
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
|
||||
}
|
|
@ -6,26 +6,14 @@ export type Group = {
|
|||
creatorId: string // User id
|
||||
createdTime: number
|
||||
mostRecentActivityTime: number
|
||||
memberIds: string[] // User ids
|
||||
anyoneCanJoin: boolean
|
||||
totalContracts: number
|
||||
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' }[]
|
||||
}
|
||||
contractIds: string[]
|
||||
|
||||
chatDisabled?: boolean
|
||||
mostRecentChatActivityTime?: number
|
||||
mostRecentContractAddedTime?: number
|
||||
}
|
||||
export const MAX_GROUP_NAME_LENGTH = 75
|
||||
export const MAX_ABOUT_LENGTH = 140
|
||||
export const MAX_ID_LENGTH = 60
|
||||
|
@ -39,4 +27,3 @@ export type GroupLink = {
|
|||
createdTime: number
|
||||
userId?: string
|
||||
}
|
||||
export type GroupContractDoc = { contractId: string; createdTime: number }
|
||||
|
|
|
@ -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
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { PortfolioMetrics, User } from './user'
|
||||
import { filterDefined } from './util/array'
|
||||
|
||||
const LOAN_DAILY_RATE = 0.02
|
||||
const LOAN_DAILY_RATE = 0.01
|
||||
|
||||
const calculateNewLoan = (investedValue: number, loanTotal: number) => {
|
||||
const netValue = investedValue - loanTotal
|
||||
|
@ -118,7 +118,7 @@ const getFreeResponseContractLoanUpdate = (
|
|||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
bets: Bet[]
|
||||
) => {
|
||||
const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
|
||||
const openBets = bets.filter((bet) => bet.isSold || bet.sale)
|
||||
|
||||
return openBets.map((bet) => {
|
||||
const loanAmount = bet.loanAmount ?? 0
|
||||
|
|
|
@ -17,7 +17,8 @@ import {
|
|||
import {
|
||||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
DPMContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
|
@ -30,10 +31,7 @@ import {
|
|||
floatingLesserEqual,
|
||||
} from './util/math'
|
||||
|
||||
export type CandidateBet<T extends Bet = Bet> = Omit<
|
||||
T,
|
||||
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
|
||||
>
|
||||
export type CandidateBet<T extends Bet = Bet> = Omit<T, 'id' | 'userId'>
|
||||
export type BetInfo = {
|
||||
newBet: CandidateBet
|
||||
newPool?: { [outcome: string]: number }
|
||||
|
@ -143,8 +141,7 @@ export const computeFills = (
|
|||
betAmount: number,
|
||||
state: CpmmState,
|
||||
limitProb: number | undefined,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
unfilledBets: LimitBet[]
|
||||
) => {
|
||||
if (isNaN(betAmount)) {
|
||||
throw new Error('Invalid bet amount: ${betAmount}')
|
||||
|
@ -166,12 +163,10 @@ export const computeFills = (
|
|||
shares: number
|
||||
timestamp: number
|
||||
}[] = []
|
||||
const ordersToCancel: LimitBet[] = []
|
||||
|
||||
let amount = betAmount
|
||||
let cpmmState = { pool: state.pool, p: state.p }
|
||||
let totalFees = noFees
|
||||
const currentBalanceByUserId = { ...balanceByUserId }
|
||||
|
||||
let i = 0
|
||||
while (true) {
|
||||
|
@ -188,20 +183,9 @@ export const computeFills = (
|
|||
takers.push(taker)
|
||||
} else {
|
||||
// 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)
|
||||
makers.push(maker)
|
||||
i++
|
||||
}
|
||||
|
||||
amount -= taker.amount
|
||||
|
@ -209,7 +193,7 @@ export const computeFills = (
|
|||
if (floatingEqual(amount, 0)) break
|
||||
}
|
||||
|
||||
return { takers, makers, totalFees, cpmmState, ordersToCancel }
|
||||
return { takers, makers, totalFees, cpmmState }
|
||||
}
|
||||
|
||||
export const getBinaryCpmmBetInfo = (
|
||||
|
@ -217,17 +201,15 @@ export const getBinaryCpmmBetInfo = (
|
|||
betAmount: number,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
limitProb: number | undefined,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
unfilledBets: LimitBet[]
|
||||
) => {
|
||||
const { pool, p } = contract
|
||||
const { takers, makers, cpmmState, totalFees, ordersToCancel } = computeFills(
|
||||
const { takers, makers, cpmmState, totalFees } = computeFills(
|
||||
outcome,
|
||||
betAmount,
|
||||
{ pool, p },
|
||||
limitProb,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
unfilledBets
|
||||
)
|
||||
const probBefore = getCpmmProbability(contract.pool, contract.p)
|
||||
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
@ -262,7 +244,6 @@ export const getBinaryCpmmBetInfo = (
|
|||
newP: cpmmState.p,
|
||||
newTotalLiquidity,
|
||||
makers,
|
||||
ordersToCancel,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,16 +252,14 @@ export const getBinaryBetStats = (
|
|||
betAmount: number,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
limitProb: number,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
unfilledBets: LimitBet[]
|
||||
) => {
|
||||
const { newBet } = getBinaryCpmmBetInfo(
|
||||
outcome,
|
||||
betAmount ?? 0,
|
||||
contract,
|
||||
limitProb,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
unfilledBets as LimitBet[]
|
||||
)
|
||||
const remainingMatched =
|
||||
((newBet.orderAmount ?? 0) - newBet.amount) /
|
||||
|
@ -343,7 +322,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
export const getNewMultiBetInfo = (
|
||||
outcome: string,
|
||||
amount: number,
|
||||
contract: DPMContract
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
visibility,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { parseTags, richTextToString } from './util/parse'
|
||||
import { removeUndefinedProps } from './util/object'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
|
||||
|
@ -37,6 +38,15 @@ export function getNewContract(
|
|||
answers: string[],
|
||||
visibility: visibility
|
||||
) {
|
||||
const tags = parseTags(
|
||||
[
|
||||
question,
|
||||
richTextToString(description),
|
||||
...extraTags.map((tag) => `#${tag}`),
|
||||
].join(' ')
|
||||
)
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||
|
||||
const propsByOutcomeType =
|
||||
outcomeType === 'BINARY'
|
||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||
|
@ -60,10 +70,9 @@ export function getNewContract(
|
|||
|
||||
question: question.trim(),
|
||||
description,
|
||||
tags: [],
|
||||
lowercaseTags: [],
|
||||
tags,
|
||||
lowercaseTags,
|
||||
visibility,
|
||||
unlistedById: visibility === 'unlisted' ? creator.id : undefined,
|
||||
isResolved: false,
|
||||
createdTime: Date.now(),
|
||||
closeTime,
|
||||
|
@ -71,7 +80,6 @@ export function getNewContract(
|
|||
volume: 0,
|
||||
volume24Hours: 0,
|
||||
volume7Days: 0,
|
||||
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
|
||||
|
||||
collectedFees: {
|
||||
creatorFee: 0,
|
||||
|
@ -112,12 +120,9 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
|||
mechanism: 'cpmm-1',
|
||||
outcomeType: 'BINARY',
|
||||
totalLiquidity: ante,
|
||||
subsidyPool: 0,
|
||||
initialProbability: p,
|
||||
p,
|
||||
pool: pool,
|
||||
prob: initialProb,
|
||||
probChanges: { day: 0, week: 0, month: 0 },
|
||||
}
|
||||
|
||||
return system
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { notification_preference } from './user-notification-preferences'
|
||||
|
||||
export type Notification = {
|
||||
id: string
|
||||
userId: string
|
||||
reasonText?: string
|
||||
reason?: notification_reason_types | notification_preference
|
||||
reason?: notification_reason_types
|
||||
createdTime: number
|
||||
viewTime?: number
|
||||
isSeen: boolean
|
||||
|
@ -17,7 +15,6 @@ export type Notification = {
|
|||
sourceUserUsername?: string
|
||||
sourceUserAvatarUrl?: string
|
||||
sourceText?: string
|
||||
data?: { [key: string]: any }
|
||||
|
||||
sourceContractTitle?: string
|
||||
sourceContractCreatorUsername?: string
|
||||
|
@ -28,7 +25,6 @@ export type Notification = {
|
|||
|
||||
isSeenOnHref?: string
|
||||
}
|
||||
|
||||
export type notification_source_types =
|
||||
| 'contract'
|
||||
| 'comment'
|
||||
|
@ -44,9 +40,6 @@ export type notification_source_types =
|
|||
| 'challenge'
|
||||
| 'betting_streak_bonus'
|
||||
| 'loan'
|
||||
| 'like'
|
||||
| 'tip_and_like'
|
||||
| 'badge'
|
||||
|
||||
export type notification_source_update_types =
|
||||
| 'created'
|
||||
|
@ -55,216 +48,26 @@ export type notification_source_update_types =
|
|||
| 'deleted'
|
||||
| 'closed'
|
||||
|
||||
/* Optional - if possible use a notification_preference */
|
||||
export type notification_reason_types =
|
||||
| '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'
|
||||
| 'contract_from_followed_user'
|
||||
| 'you_follow_user'
|
||||
| 'added_you_to_group'
|
||||
| 'you_referred_user'
|
||||
| 'user_joined_to_bet_on_your_market'
|
||||
| 'unique_bettors_on_your_contract'
|
||||
| 'on_group_you_are_member_of'
|
||||
| 'tip_received'
|
||||
| 'bet_fill'
|
||||
| '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
|
||||
}
|
||||
| 'you_follow_contract'
|
||||
|
|
|
@ -8,13 +8,11 @@
|
|||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@tiptap/core": "2.0.0-beta.199",
|
||||
"@tiptap/extension-image": "2.0.0-beta.199",
|
||||
"@tiptap/extension-link": "2.0.0-beta.199",
|
||||
"@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",
|
||||
"@tiptap/core": "2.0.0-beta.182",
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.191",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -13,6 +13,7 @@ import { addObjects } from './util/object'
|
|||
export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => {
|
||||
const { pool } = contract
|
||||
const poolTotal = sum(Object.values(pool))
|
||||
console.log('resolved N/A, pool M$', poolTotal)
|
||||
|
||||
const betSum = sumBy(bets, (b) => b.amount)
|
||||
|
||||
|
@ -57,6 +58,17 @@ export const getDpmStandardPayouts = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
outcome,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
@ -98,6 +110,17 @@ export const getNumericDpmPayouts = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved numeric bucket: ',
|
||||
outcome,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
@ -140,6 +163,17 @@ export const getDpmMktPayouts = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved MKT',
|
||||
p,
|
||||
'pool',
|
||||
pool,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
@ -168,7 +202,7 @@ export const getPayoutsMultiOutcome = (
|
|||
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
|
||||
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 }
|
||||
})
|
||||
|
||||
|
@ -182,6 +216,16 @@ export const getPayoutsMultiOutcome = (
|
|||
liquidityFee: 0,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
resolutions,
|
||||
'pool',
|
||||
poolTotal,
|
||||
'profits',
|
||||
profits,
|
||||
'creator fee',
|
||||
creatorFee
|
||||
)
|
||||
return {
|
||||
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
|
||||
creatorPayout: creatorFee,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { sum } from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { getProbability } from './calculate'
|
||||
import { getCpmmLiquidityPoolWeights } from './calculate-cpmm'
|
||||
|
@ -41,6 +43,18 @@ export const getStandardFixedPayouts = (
|
|||
|
||||
const { collectedFees } = contract
|
||||
const creatorPayout = collectedFees.creatorFee
|
||||
|
||||
console.log(
|
||||
'resolved',
|
||||
outcome,
|
||||
'pool',
|
||||
contract.pool[outcome],
|
||||
'payouts',
|
||||
sum(payouts),
|
||||
'creator fee',
|
||||
creatorPayout
|
||||
)
|
||||
|
||||
const liquidityPayouts = getLiquidityPoolPayouts(
|
||||
contract,
|
||||
outcome,
|
||||
|
@ -55,11 +69,10 @@ export const getLiquidityPoolPayouts = (
|
|||
outcome: string,
|
||||
liquidities: LiquidityProvision[]
|
||||
) => {
|
||||
const { pool, subsidyPool } = contract
|
||||
const finalPool = pool[outcome] + (subsidyPool ?? 0)
|
||||
if (finalPool < 1e-3) return []
|
||||
const { pool } = contract
|
||||
const finalPool = pool[outcome]
|
||||
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false)
|
||||
|
||||
return Object.entries(weights).map(([providerId, weight]) => ({
|
||||
userId: providerId,
|
||||
|
@ -85,6 +98,18 @@ export const getMktFixedPayouts = (
|
|||
|
||||
const { collectedFees } = contract
|
||||
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)
|
||||
|
||||
return { payouts, creatorPayout, liquidityPayouts, collectedFees }
|
||||
|
@ -95,11 +120,10 @@ export const getLiquidityPoolProbPayouts = (
|
|||
p: number,
|
||||
liquidities: LiquidityProvision[]
|
||||
) => {
|
||||
const { pool, subsidyPool } = contract
|
||||
const finalPool = p * pool.YES + (1 - p) * pool.NO + (subsidyPool ?? 0)
|
||||
if (finalPool < 1e-3) return []
|
||||
const { pool } = contract
|
||||
const finalPool = p * pool.YES + (1 - p) * pool.NO
|
||||
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false)
|
||||
|
||||
return Object.entries(weights).map(([providerId, weight]) => ({
|
||||
userId: providerId,
|
||||
|
|
|
@ -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
|
|
@ -13,10 +13,7 @@ export const getRedeemableAmount = (bets: RedeemableBet[]) => {
|
|||
const yesShares = sumBy(yesBets, (b) => b.shares)
|
||||
const noShares = sumBy(noBets, (b) => b.shares)
|
||||
const shares = Math.max(Math.min(yesShares, noShares), 0)
|
||||
const soldFrac =
|
||||
shares > 0
|
||||
? Math.min(yesShares, noShares) / Math.max(yesShares, noShares)
|
||||
: 0
|
||||
const soldFrac = shares > 0 ? Math.min(yesShares, noShares) / shares : 0
|
||||
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
|
||||
const loanPayment = loanAmount * soldFrac
|
||||
const netAmount = shares - loanPayment
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { groupBy, sumBy, mapValues, keyBy, sortBy } from 'lodash'
|
||||
import { groupBy, sumBy, mapValues, partition } from 'lodash'
|
||||
|
||||
import { Bet } from './bet'
|
||||
import { getContractBetMetrics, resolvedPayout } from './calculate'
|
||||
import { Contract } from './contract'
|
||||
import { ContractComment } from './comment'
|
||||
import { getPayouts } from './payouts'
|
||||
|
||||
export function scoreCreators(contracts: Contract[]) {
|
||||
const creatorScore = mapValues(
|
||||
|
@ -31,11 +30,46 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
|
|||
}
|
||||
|
||||
export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||
const betsByUser = groupBy(bets, (bet) => bet.userId)
|
||||
return mapValues(
|
||||
betsByUser,
|
||||
(bets) => getContractBetMetrics(contract, bets).profit
|
||||
const { resolution } = contract
|
||||
const resolutionProb =
|
||||
contract.outcomeType == 'BINARY'
|
||||
? 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(
|
||||
|
@ -47,47 +81,3 @@ export function addUserScores(
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,7 @@ import { CPMMContract, DPMContract } from './contract'
|
|||
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
|
||||
import { sumBy } from 'lodash'
|
||||
|
||||
export type CandidateBet<T extends Bet> = Omit<
|
||||
T,
|
||||
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
|
||||
>
|
||||
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
||||
|
||||
export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
@ -84,17 +81,15 @@ export const getCpmmSellBetInfo = (
|
|||
outcome: 'YES' | 'NO',
|
||||
contract: CPMMContract,
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number },
|
||||
loanPaid: number
|
||||
) => {
|
||||
const { pool, p } = contract
|
||||
|
||||
const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale(
|
||||
const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale(
|
||||
contract,
|
||||
shares,
|
||||
outcome,
|
||||
unfilledBets,
|
||||
balanceByUserId,
|
||||
unfilledBets
|
||||
)
|
||||
|
||||
const probBefore = getCpmmProbability(pool, p)
|
||||
|
@ -136,6 +131,5 @@ export const getCpmmSellBetInfo = (
|
|||
fees,
|
||||
makers,
|
||||
takers,
|
||||
ordersToCancel
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
export type Stats = {
|
||||
startDate: number
|
||||
dailyActiveUsers: number[]
|
||||
dailyActiveUsersWeeklyAvg: number[]
|
||||
weeklyActiveUsers: number[]
|
||||
monthlyActiveUsers: number[]
|
||||
d1: number[]
|
||||
d1WeeklyAvg: number[]
|
||||
nd1: number[]
|
||||
nd1WeeklyAvg: number[]
|
||||
nw1: number[]
|
||||
dailyBetCounts: number[]
|
||||
dailyContractCounts: number[]
|
||||
dailyCommentCounts: number[]
|
||||
dailySignups: number[]
|
||||
weekOnWeekRetention: number[]
|
||||
monthlyRetention: number[]
|
||||
dailyActivationRate: number[]
|
||||
dailyActivationRateWeeklyAvg: number[]
|
||||
weeklyActivationRate: number[]
|
||||
topTenthActions: {
|
||||
daily: number[]
|
||||
weekly: number[]
|
||||
monthly: number[]
|
||||
}
|
||||
manaBet: {
|
||||
daily: number[]
|
||||
weekly: number[]
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
|
||||
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
|
||||
type AnyTxnType =
|
||||
| Donation
|
||||
| Tip
|
||||
| Manalink
|
||||
| Referral
|
||||
| UniqueBettorBonus
|
||||
| BettingStreakBonus
|
||||
| CancelUniqueBettorBonus
|
||||
| CommentBountyRefund
|
||||
type AnyTxnType = Donation | Tip | Manalink | Referral | Bonus
|
||||
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
|
||||
|
||||
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||
|
@ -31,9 +23,6 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
|||
| 'REFERRAL'
|
||||
| 'UNIQUE_BETTOR_BONUS'
|
||||
| 'BETTING_STREAK_BONUS'
|
||||
| 'CANCEL_UNIQUE_BETTOR_BONUS'
|
||||
| 'COMMENT_BOUNTY'
|
||||
| 'REFUND_COMMENT_BOUNTY'
|
||||
|
||||
// Any extra data
|
||||
data?: { [key: string]: any }
|
||||
|
@ -71,70 +60,13 @@ type Referral = {
|
|||
category: 'REFERRAL'
|
||||
}
|
||||
|
||||
type UniqueBettorBonus = {
|
||||
type Bonus = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
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
|
||||
}
|
||||
category: 'UNIQUE_BETTOR_BONUS' | 'BETTING_STREAK_BONUS'
|
||||
}
|
||||
|
||||
export type DonationTxn = Txn & Donation
|
||||
export type TipTxn = Txn & Tip
|
||||
export type ManalinkTxn = Txn & Manalink
|
||||
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
|
||||
|
|
|
@ -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§ion=${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: '',
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
import { notification_preferences } from './user-notification-preferences'
|
||||
import { ENV_CONFIG } from './envs/constants'
|
||||
import { MarketCreatorBadge, ProvenCorrectBadge, StreakerBadge } from './badge'
|
||||
|
||||
export type User = {
|
||||
id: string
|
||||
createdTime: number
|
||||
|
@ -12,6 +8,7 @@ export type User = {
|
|||
|
||||
// For their user page
|
||||
bio?: string
|
||||
bannerUrl?: string
|
||||
website?: string
|
||||
twitterHandle?: string
|
||||
discordHandle?: string
|
||||
|
@ -33,13 +30,10 @@ export type User = {
|
|||
allTime: number
|
||||
}
|
||||
|
||||
fractionResolvedCorrectly: number
|
||||
|
||||
nextLoanCached: number
|
||||
followerCountCached: number
|
||||
|
||||
followedCategories?: string[]
|
||||
homeSections?: string[]
|
||||
|
||||
referredByUserId?: string
|
||||
referredByContractId?: string
|
||||
|
@ -50,19 +44,6 @@ export type User = {
|
|||
currentBettingStreak?: number
|
||||
hasSeenContractFollowModal?: boolean
|
||||
freeMarketsCreated?: number
|
||||
isBannedFromPosting?: boolean
|
||||
|
||||
achievements: {
|
||||
provenCorrect?: {
|
||||
badges: ProvenCorrectBadge[]
|
||||
}
|
||||
marketCreator?: {
|
||||
badges: MarketCreatorBadge[]
|
||||
}
|
||||
streaker?: {
|
||||
badges: StreakerBadge[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type PrivateUser = {
|
||||
|
@ -70,21 +51,25 @@ export type PrivateUser = {
|
|||
username: string // denormalized from User
|
||||
|
||||
email?: string
|
||||
unsubscribedFromResolutionEmails?: boolean
|
||||
unsubscribedFromCommentEmails?: boolean
|
||||
unsubscribedFromAnswerEmails?: boolean
|
||||
unsubscribedFromGenericEmails?: boolean
|
||||
unsubscribedFromWeeklyTrendingEmails?: boolean
|
||||
weeklyTrendingEmailSent?: boolean
|
||||
weeklyPortfolioUpdateEmailSent?: boolean
|
||||
manaBonusEmailSent?: boolean
|
||||
initialDeviceToken?: string
|
||||
initialIpAddress?: 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 = {
|
||||
investmentValue: number
|
||||
balance: number
|
||||
|
@ -93,15 +78,5 @@ export type PortfolioMetrics = {
|
|||
userId: string
|
||||
}
|
||||
|
||||
export const MANIFOLD_USER_USERNAME = 'ManifoldMarkets'
|
||||
export const MANIFOLD_USER_NAME = 'ManifoldMarkets'
|
||||
export const MANIFOLD_USERNAME = 'ManifoldMarkets'
|
||||
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'
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -8,14 +8,7 @@ const formatter = new Intl.NumberFormat('en-US', {
|
|||
})
|
||||
|
||||
export function formatMoney(amount: number) {
|
||||
const newAmount =
|
||||
// handle -0 case
|
||||
Math.round(amount) === 0
|
||||
? 0
|
||||
: // Handle 499.9999999999999 case
|
||||
(amount > 0 ? Math.floor : Math.ceil)(
|
||||
amount + 0.00000000001 * Math.sign(amount)
|
||||
)
|
||||
const newAmount = Math.round(amount) === 0 ? 0 : Math.floor(amount) // handle -0 case
|
||||
return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
|
||||
}
|
||||
|
||||
|
@ -60,16 +53,6 @@ export function formatLargeNumber(num: number, sigfigs = 2): string {
|
|||
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) {
|
||||
const camelCase = words
|
||||
.split(' ')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { union } from 'lodash'
|
||||
|
||||
export const removeUndefinedProps = <T extends object>(obj: T): T => {
|
||||
export const removeUndefinedProps = <T>(obj: T): T => {
|
||||
const newObj: any = {}
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
|
@ -37,3 +37,4 @@ export const subtractObjects = <T extends { [key: string]: number }>(
|
|||
|
||||
return newObj as T
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { generateText, JSONContent, Node } from '@tiptap/core'
|
||||
import { generateJSON } from '@tiptap/html'
|
||||
import { MAX_TAG_LENGTH } from '../contract'
|
||||
import { generateText, JSONContent } from '@tiptap/core'
|
||||
// Tiptap starter extensions
|
||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||
import { Bold } from '@tiptap/extension-bold'
|
||||
|
@ -23,14 +23,34 @@ 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 getUrl(text: string) {
|
||||
const results = find(text, 'url')
|
||||
return results.length ? results[0].href : null
|
||||
export function parseTags(text: string) {
|
||||
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
|
||||
const matches = (text.match(regex) || []).map((match) =>
|
||||
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
|
||||
|
@ -52,28 +72,8 @@ export function parseMentions(data: JSONContent): 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
|
||||
// can't just do [StarterKit, Image...] because it doesn't work with cjs imports
|
||||
export const exhibitExts = [
|
||||
Blockquote,
|
||||
Bold,
|
||||
BulletList,
|
||||
|
@ -90,26 +90,14 @@ const stringParseExts = [
|
|||
Paragraph,
|
||||
Strike,
|
||||
Text,
|
||||
// other extensions
|
||||
|
||||
Image,
|
||||
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] }),
|
||||
Mention,
|
||||
Iframe,
|
||||
TiptapTweet,
|
||||
]
|
||||
|
||||
export function richTextToString(text?: JSONContent) {
|
||||
if (!text) return ''
|
||||
return generateText(text, stringParseExts)
|
||||
}
|
||||
|
||||
export function htmlToRichText(html: string) {
|
||||
return generateJSON(html, stringParseExts)
|
||||
return !text ? '' : generateText(text, exhibitExts)
|
||||
}
|
||||
|
|
|
@ -46,10 +46,3 @@ export const shuffle = (array: unknown[], rand: () => number) => {
|
|||
;[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)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,2 @@
|
|||
export const MINUTE_MS = 60 * 1000
|
||||
export const HOUR_MS = 60 * MINUTE_MS
|
||||
export const HOUR_MS = 60 * 60 * 1000
|
||||
export const DAY_MS = 24 * HOUR_MS
|
||||
|
||||
export const sleep = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
|
|
@ -35,7 +35,7 @@ export default Node.create<IframeOptions>({
|
|||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper' + ' ' + wrapperClasses,
|
||||
// Tailwind JIT doesn't seem to pick up `pb-[20rem]`, so we hack this in:
|
||||
style: 'padding-bottom: 20rem; ',
|
||||
style: 'padding-bottom: 20rem;',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -48,9 +48,6 @@ export default Node.create<IframeOptions>({
|
|||
frameborder: {
|
||||
default: 0,
|
||||
},
|
||||
height: {
|
||||
default: 0,
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => this.options.allowFullscreen,
|
||||
|
@ -63,11 +60,6 @@ export default Node.create<IframeOptions>({
|
|||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
this.options.HTMLAttributes.style =
|
||||
this.options.HTMLAttributes.style +
|
||||
' height: ' +
|
||||
HTMLAttributes.height +
|
||||
';'
|
||||
return [
|
||||
'div',
|
||||
this.options.HTMLAttributes,
|
||||
|
|
|
@ -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
|
||||
},
|
||||
})
|
|
@ -54,33 +54,19 @@ Returns the authenticated user.
|
|||
|
||||
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]`
|
||||
### `GET /v0/groups/[slug]`
|
||||
|
||||
Gets a group by its slug.
|
||||
|
||||
Requires no authorization.
|
||||
Note: group is singular in the URL.
|
||||
|
||||
### `GET /v0/group/by-id/[id]`
|
||||
### `GET /v0/groups/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`
|
||||
|
||||
|
@ -158,16 +144,13 @@ Requires no authorization.
|
|||
// i.e. https://manifold.markets/Austin/test-market is the same as https://manifold.markets/foo/test-market
|
||||
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
|
||||
|
||||
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.
|
||||
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
|
||||
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
|
||||
volume7Days: number
|
||||
|
@ -411,7 +394,7 @@ Requires no authorization.
|
|||
type FullMarket = LiteMarket & {
|
||||
bets: Bet[]
|
||||
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
|
||||
}
|
||||
|
@ -557,7 +540,7 @@ Creates a new market on behalf of the authorized user.
|
|||
|
||||
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.
|
||||
- `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).
|
||||
|
@ -572,12 +555,6 @@ For numeric markets, you must also provide:
|
|||
|
||||
- `min`: The minimum 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:
|
||||
|
||||
|
@ -591,18 +568,6 @@ $ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: applicat
|
|||
"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`
|
||||
|
||||
Resolves a market on behalf of the authorized user.
|
||||
|
@ -614,18 +579,15 @@ For binary markets:
|
|||
- `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`.
|
||||
- `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.
|
||||
- `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:
|
||||
|
||||
- `outcome`: Required. One of `CANCEL`, or a `number` indicating the selected numeric bucket ID.
|
||||
- `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:
|
||||
|
||||
|
@ -680,17 +642,6 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \
|
|||
--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`
|
||||
|
||||
Gets a list of bets, ordered by creation date descending.
|
||||
|
@ -780,7 +731,6 @@ Requires no authorization.
|
|||
|
||||
## 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-06-08: Add paging to markets endpoint
|
||||
- 2022-06-05: Add new authorized write endpoints
|
||||
|
|
|
@ -8,13 +8,13 @@ A list of community-created projects built on, or related to, Manifold Markets.
|
|||
|
||||
## Sites using Manifold
|
||||
|
||||
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government
|
||||
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
|
||||
- [WagerWith.me](https://www.wagerwith.me/) — Bet with your friends, with full Manifold integration to bet with M$.
|
||||
- [Alignment Markets](https://alignmentmarkets.com/) - Bet on the progress of benchmarks in ML safety!
|
||||
|
||||
## API / Dev
|
||||
|
||||
- [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)
|
||||
- [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
|
||||
|
@ -24,24 +24,3 @@ A list of community-created projects built on, or related to, Manifold Markets.
|
|||
|
||||
- [@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
|
||||
|
|
|
@ -15,22 +15,6 @@ Our community is the beating heart of Manifold; your individual contributions ar
|
|||
|
||||
## 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*
|
||||
|
||||
**[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000**
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
|
||||
### 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?
|
||||
|
||||
|
|
|
@ -2,30 +2,10 @@
|
|||
"functions": {
|
||||
"predeploy": "cd functions && yarn build",
|
||||
"runtime": "nodejs16",
|
||||
"source": "functions/dist",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git",
|
||||
"firebase-debug.log",
|
||||
"firebase-debug.*.log"
|
||||
]
|
||||
"source": "functions/dist"
|
||||
},
|
||||
"firestore": {
|
||||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json"
|
||||
},
|
||||
"emulators": {
|
||||
"functions": {
|
||||
"port": 5001
|
||||
},
|
||||
"firestore": {
|
||||
"port": 8080
|
||||
},
|
||||
"pubsub": {
|
||||
"port": 8085
|
||||
},
|
||||
"ui": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,20 +22,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "bets",
|
||||
"queryScope": "COLLECTION_GROUP",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "isFilled",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"order": "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "bets",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"queryScope": "COLLECTION",
|
||||
|
|
|
@ -12,10 +12,7 @@ service cloud.firestore {
|
|||
'taowell@gmail.com',
|
||||
'abc.sinclair@gmail.com',
|
||||
'manticmarkets@gmail.com',
|
||||
'iansphilips@gmail.com',
|
||||
'd4vidchee@gmail.com',
|
||||
'federicoruizcassarino@gmail.com',
|
||||
'ingawei@gmail.com'
|
||||
'iansphilips@gmail.com'
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -23,17 +20,11 @@ service cloud.firestore {
|
|||
allow read;
|
||||
}
|
||||
|
||||
match /globalConfig/globalConfig {
|
||||
allow read;
|
||||
allow update: if isAdmin()
|
||||
allow create: if isAdmin()
|
||||
}
|
||||
|
||||
match /users/{userId} {
|
||||
allow read;
|
||||
allow update: if userId == request.auth.uid
|
||||
&& 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','shouldShowWelcome', 'hasSeenContractFollowModal']);
|
||||
// User referral rules
|
||||
allow update: if userId == request.auth.uid
|
||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||
|
@ -50,10 +41,6 @@ service cloud.firestore {
|
|||
allow read;
|
||||
}
|
||||
|
||||
match /{somePath=**}/contract-metrics/{contractId} {
|
||||
allow read;
|
||||
}
|
||||
|
||||
match /{somePath=**}/challenges/{challengeId}{
|
||||
allow read;
|
||||
}
|
||||
|
@ -75,11 +62,6 @@ service cloud.firestore {
|
|||
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} {
|
||||
allow read;
|
||||
}
|
||||
|
@ -88,7 +70,7 @@ service cloud.firestore {
|
|||
allow read: if userId == request.auth.uid || isAdmin();
|
||||
allow update: if (userId == request.auth.uid || isAdmin())
|
||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||
.hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']);
|
||||
.hasOnly(['apiKey', 'twitchInfo', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences', 'unsubscribedFromWeeklyTrendingEmails' ]);
|
||||
}
|
||||
|
||||
match /private-users/{userId}/views/{viewId} {
|
||||
|
@ -110,9 +92,9 @@ service cloud.firestore {
|
|||
match /contracts/{contractId} {
|
||||
allow read;
|
||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks', 'flaggedByUsernames']);
|
||||
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks']);
|
||||
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;
|
||||
allow update: if isAdmin();
|
||||
match /comments/{commentId} {
|
||||
|
@ -173,52 +155,25 @@ service cloud.firestore {
|
|||
.hasOnly(['isSeen', 'viewTime']);
|
||||
}
|
||||
|
||||
match /{somePath=**}/groupMembers/{memberId} {
|
||||
allow read;
|
||||
}
|
||||
match /groups/{groupId} {
|
||||
allow read;
|
||||
allow update: if request.auth.uid == resource.data.creatorId
|
||||
&& request.resource.data.diff(resource.data)
|
||||
.affectedKeys()
|
||||
.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;
|
||||
|
||||
match /{somePath=**}/groupContracts/{contractId} {
|
||||
allow read;
|
||||
}
|
||||
|
||||
match /groups/{groupId} {
|
||||
allow read;
|
||||
allow update: if (request.auth.uid == resource.data.creatorId || isAdmin())
|
||||
&& request.resource.data.diff(resource.data)
|
||||
.affectedKeys()
|
||||
.hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId', 'pinnedItems' ]);
|
||||
allow delete: if request.auth.uid == resource.data.creatorId;
|
||||
|
||||
match /groupContracts/{contractId} {
|
||||
allow write: if isGroupMember() || request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId
|
||||
}
|
||||
|
||||
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} {
|
||||
allow read;
|
||||
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isGroupMember();
|
||||
}
|
||||
}
|
||||
|
||||
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) ;
|
||||
function isMember() {
|
||||
return request.auth.uid in get(/databases/$(database)/documents/groups/$(groupId)).data.memberIds;
|
||||
}
|
||||
}
|
||||
match /comments/{commentId} {
|
||||
allow read;
|
||||
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isMember();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# This sets which EnvConfig is deployed to Firebase Cloud Functions
|
||||
|
||||
NEXT_PUBLIC_FIREBASE_ENV=DEV
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: ['lodash', 'unused-imports'],
|
||||
plugins: ['lodash'],
|
||||
extends: ['eslint:recommended'],
|
||||
ignorePatterns: ['dist', 'lib'],
|
||||
env: {
|
||||
|
@ -26,7 +26,6 @@ module.exports = {
|
|||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
1
functions/.gitignore
vendored
1
functions/.gitignore
vendored
|
@ -17,5 +17,4 @@ package-lock.json
|
|||
ui-debug.log
|
||||
firebase-debug.log
|
||||
firestore-debug.log
|
||||
pubsub-debug.log
|
||||
firestore_export/
|
||||
|
|
|
@ -20,7 +20,7 @@ Adapted from https://firebase.google.com/docs/functions/get-started
|
|||
3. `$ firebase login` to authenticate the CLI tools to Firebase
|
||||
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
|
||||
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
|
||||
|
||||
0. `$ ./dev.sh localdb` to start the local emulator and front end
|
||||
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`
|
||||
- There's surely a better way to cause/react to a db trigger update but just adding this here for now as it works
|
||||
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/)!)
|
||||
0. `$ firebase use dev` if you haven't already
|
||||
1. `$ yarn serve` to spin up the emulators 0. The Emulator UI is at http://localhost:4000; the functions are hosted on :5001.
|
||||
Note: You have to kill and restart emulators when you change code; no hot reload =(
|
||||
2. `$ yarn dev:emulate` in `/web` to connect to emulators with the frontend 0. Note: emulated database is cleared after every shutdown
|
||||
|
||||
## 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:
|
||||
|
||||
- Set a secret: `$ firebase functions:secrets:set STRIPE_APIKEY`
|
||||
- Then, enter the secret in the prompt.
|
||||
- Set a secret: `$ firebase functions:secrets:set stripe.test_secret="THE-API-KEY"`
|
||||
- Read a secret: `$ firebase functions:secrets:access STRIPE_APIKEY`
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"firestore": "dev-mantic-markets.appspot.com"
|
||||
},
|
||||
"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",
|
||||
"watch": "tsc -w",
|
||||
"shell": "yarn build && firebase functions:shell",
|
||||
|
@ -15,9 +15,9 @@
|
|||
"dev": "nodemon src/serve.ts",
|
||||
"localDbScript": "firebase emulators:start --only functions,firestore,pubsub --import=./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:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./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/",
|
||||
"verify": "(cd .. && yarn verify)",
|
||||
"verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty"
|
||||
|
@ -26,13 +26,11 @@
|
|||
"dependencies": {
|
||||
"@amplitude/node": "1.10.0",
|
||||
"@google-cloud/functions-framework": "3.1.2",
|
||||
"@tiptap/core": "2.0.0-beta.199",
|
||||
"@tiptap/extension-image": "2.0.0-beta.199",
|
||||
"@tiptap/extension-link": "2.0.0-beta.199",
|
||||
"@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",
|
||||
"@tiptap/core": "2.0.0-beta.182",
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.191",
|
||||
"cors": "2.8.5",
|
||||
"dayjs": "1.11.4",
|
||||
"express": "4.18.1",
|
||||
|
@ -40,19 +38,15 @@
|
|||
"firebase-functions": "3.21.2",
|
||||
"lodash": "4.17.21",
|
||||
"mailgun-js": "0.22.0",
|
||||
"marked": "4.1.1",
|
||||
"module-alias": "2.2.2",
|
||||
"node-fetch": "2",
|
||||
"react-masonry-css": "1.0.16",
|
||||
"stripe": "8.194.0",
|
||||
"zod": "3.17.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mailgun-js": "0.22.12",
|
||||
"@types/marked": "4.0.7",
|
||||
"@types/module-alias": "2.0.1",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"firebase-functions-test": "0.3.3",
|
||||
"puppeteer": "18.0.5"
|
||||
"firebase-functions-test": "0.3.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Contract, CPMMContract } from '../../common/contract'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import { removeUndefinedProps } from '../../common/util/object'
|
||||
import { getNewLiquidityProvision } from '../../common/add-liquidity'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
|
||||
|
@ -11,10 +12,10 @@ const bodySchema = z.object({
|
|||
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)
|
||||
|
||||
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
|
||||
return await firestore.runTransaction(async (transaction) => {
|
||||
|
@ -44,18 +45,29 @@ export const addsubsidy = newEndpoint({}, async (req, auth) => {
|
|||
.collection(`contracts/${contractId}/liquidity`)
|
||||
.doc()
|
||||
|
||||
const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } =
|
||||
const { newLiquidityProvision, newPool, newP, newTotalLiquidity } =
|
||||
getNewLiquidityProvision(
|
||||
user.id,
|
||||
user,
|
||||
amount,
|
||||
contract,
|
||||
newLiquidityProvisionDoc.id
|
||||
)
|
||||
|
||||
transaction.update(contractDoc, {
|
||||
subsidyPool: newSubsidyPool,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
} as Partial<CPMMContract>)
|
||||
if (newP !== undefined && !isFinite(newP)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Liquidity injection rejected due to overflow error.',
|
||||
}
|
||||
}
|
||||
|
||||
transaction.update(
|
||||
contractDoc,
|
||||
removeUndefinedProps({
|
||||
pool: newPool,
|
||||
p: newP,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
})
|
||||
)
|
||||
|
||||
const newBalance = user.balance - amount
|
||||
const newTotalDeposits = user.totalDeposits - amount
|
|
@ -14,7 +14,7 @@ import {
|
|||
export { APIError } from '../../common/api'
|
||||
|
||||
type Output = Record<string, unknown>
|
||||
export type AuthedUser = {
|
||||
type AuthedUser = {
|
||||
uid: string
|
||||
creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser })
|
||||
}
|
||||
|
@ -146,24 +146,3 @@ export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
|
|||
},
|
||||
} as EndpointDefinition
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import * as admin from 'firebase-admin'
|
|||
import { z } from 'zod'
|
||||
|
||||
import { getUser } from './utils'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { User } from '../../common/user'
|
||||
|
@ -38,56 +37,6 @@ export const changeUser = async (
|
|||
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) => {
|
||||
if (update.username) {
|
||||
update.username = cleanUsername(update.username)
|
||||
|
@ -109,7 +58,42 @@ export const changeUser = async (
|
|||
|
||||
const userRef = firestore.collection('users').doc(user.id)
|
||||
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)
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -5,9 +5,9 @@ import { Contract } from '../../common/contract'
|
|||
import { User } from '../../common/user'
|
||||
import { getNewMultiBetInfo } from '../../common/new-bet'
|
||||
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 { addUserToContractFollowers } from './follow-market'
|
||||
|
||||
const bodySchema = z.object({
|
||||
contractId: z.string().max(MAX_ANSWER_LENGTH),
|
||||
|
@ -97,7 +97,9 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
|
|||
return answer
|
||||
})
|
||||
|
||||
await addUserToContractFollowers(contractId, auth.uid)
|
||||
const contract = await getContract(contractId)
|
||||
|
||||
if (answer && contract) await sendNewAnswerEmail(answer, contract)
|
||||
|
||||
return answer
|
||||
})
|
||||
|
|
|
@ -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 }
|
||||
})
|
|
@ -10,7 +10,7 @@ import {
|
|||
MAX_GROUP_NAME_LENGTH,
|
||||
MAX_ID_LENGTH,
|
||||
} from '../../common/group'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { APIError, newEndpoint, validate } from '../../functions/src/api'
|
||||
import { z } from 'zod'
|
||||
|
||||
const bodySchema = z.object({
|
||||
|
@ -58,25 +58,13 @@ export const creategroup = newEndpoint({}, async (req, auth) => {
|
|||
createdTime: Date.now(),
|
||||
mostRecentActivityTime: Date.now(),
|
||||
// TODO: allow users to add contract ids on group creation
|
||||
contractIds: [],
|
||||
anyoneCanJoin,
|
||||
totalContracts: 0,
|
||||
totalMembers: memberIds.length,
|
||||
postIds: [],
|
||||
pinnedItems: [],
|
||||
memberIds,
|
||||
}
|
||||
|
||||
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 }
|
||||
})
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ 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 { APIError, newEndpoint, validate, zTimestamp } from './api'
|
||||
|
||||
import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy'
|
||||
import {
|
||||
|
@ -92,11 +92,7 @@ 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) {
|
||||
export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||
const {
|
||||
question,
|
||||
description,
|
||||
|
@ -105,13 +101,16 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
|
|||
outcomeType,
|
||||
groupId,
|
||||
visibility = 'public',
|
||||
} = validate(bodySchema, body)
|
||||
} = validate(bodySchema, req.body)
|
||||
|
||||
let min, max, initialProb, isLogScale, answers
|
||||
|
||||
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
||||
let initialValue
|
||||
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
|
||||
;({ min, max, initialValue, isLogScale } = validate(
|
||||
numericSchema,
|
||||
req.body
|
||||
))
|
||||
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
|
||||
throw new APIError(400, 'Invalid range.')
|
||||
|
||||
|
@ -127,11 +126,11 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
|
|||
}
|
||||
|
||||
if (outcomeType === 'BINARY') {
|
||||
;({ initialProb } = validate(binarySchema, body))
|
||||
;({ initialProb } = validate(binarySchema, req.body))
|
||||
}
|
||||
|
||||
if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||
;({ answers } = validate(multipleChoiceSchema, body))
|
||||
;({ answers } = validate(multipleChoiceSchema, req.body))
|
||||
}
|
||||
|
||||
const userDoc = await firestore.collection('users').doc(auth.uid).get()
|
||||
|
@ -156,14 +155,8 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
|
|||
}
|
||||
|
||||
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.memberIds.includes(user.id) &&
|
||||
!group.anyoneCanJoin &&
|
||||
group.creatorId !== user.id
|
||||
) {
|
||||
|
@ -187,17 +180,17 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
|
|||
|
||||
// convert string descriptions into JSONContent
|
||||
const newDescription =
|
||||
!description || typeof description === 'string'
|
||||
typeof description === 'string'
|
||||
? {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: description || ' ' }],
|
||||
content: [{ type: 'text', text: description }],
|
||||
},
|
||||
],
|
||||
}
|
||||
: description
|
||||
: description ?? {}
|
||||
|
||||
const contract = getNewContract(
|
||||
contractRef.id,
|
||||
|
@ -234,20 +227,11 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
|
|||
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)) {
|
||||
if (!group.contractIds.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(),
|
||||
const groupDocRef = firestore.collection('groups').doc(group.id)
|
||||
groupDocRef.update({
|
||||
contractIds: uniq([...group.contractIds, contractRef.id]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -324,7 +308,7 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
|
|||
}
|
||||
|
||||
return contract
|
||||
}
|
||||
})
|
||||
|
||||
const getSlug = async (question: string) => {
|
||||
const proposedSlug = slugify(question)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
import { uniq } from 'lodash'
|
||||
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
import { getUser, getUserByUsername, getValues } from './utils'
|
||||
|
@ -16,9 +17,8 @@ import {
|
|||
|
||||
import { track } from './analytics'
|
||||
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 { getDefaultNotificationPreferences } from '../../common/user-notification-preferences'
|
||||
|
||||
const bodySchema = z.object({
|
||||
deviceToken: z.string().optional(),
|
||||
|
@ -69,8 +69,6 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
|||
followerCountCached: 0,
|
||||
followedCategories: DEFAULT_CATEGORIES,
|
||||
shouldShowWelcome: true,
|
||||
fractionResolvedCorrectly: 1,
|
||||
achievements: {},
|
||||
}
|
||||
|
||||
await firestore.collection('users').doc(auth.uid).create(user)
|
||||
|
@ -82,7 +80,6 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
|||
email,
|
||||
initialIpAddress: req.ip,
|
||||
initialDeviceToken: deviceToken,
|
||||
notificationPreferences: getDefaultNotificationPreferences(auth.uid),
|
||||
}
|
||||
|
||||
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
||||
|
@ -120,8 +117,23 @@ const addUserToDefaultGroups = async (user: User) => {
|
|||
firestore.collection('groups').where('slug', '==', slug)
|
||||
)
|
||||
await firestore
|
||||
.collection(`groups/${groups[0].id}/groupMembers`)
|
||||
.doc(user.id)
|
||||
.set({ userId: user.id, createdTime: Date.now() })
|
||||
.collection('groups')
|
||||
.doc(groups[0].id)
|
||||
.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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
318
functions/src/email-templates/500-mana.html
Normal file
318
functions/src/email-templates/500-mana.html
Normal file
|
@ -0,0 +1,318 @@
|
|||
<!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 Markets 7th Day Anniversary Gift!</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]-->
|
||||
<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="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/home" target="_blank"><img
|
||||
alt="" height="auto" src="https://i.imgur.com/8EP8Y8q.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>
|
||||
<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: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;">Thanks for
|
||||
using Manifold Markets. Running low
|
||||
on mana (M$)? Click the link below to receive a one time gift of M$500!</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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;">Receiving
|
||||
tips on comments</span></li>
|
||||
<li style="line-height:23px;"><span
|
||||
style="font-family:Arial, sans-serif;font-size:18px;">Unique
|
||||
trader bonus for each user who bets 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;"> </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 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>
|
||||
</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"></a></li>
|
||||
</ul>
|
||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"> </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 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>
|
||||
</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]><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;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
</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: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;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;">
|
||||
<p style="margin: 10px 0;">This e-mail has been sent to {{name}}, <a
|
||||
href="{{unsubscribeLink}}” style=" color:inherit;text-decoration:none;"
|
||||
target="_blank">click here to unsubscribe</a>.</p>
|
||||
</div>
|
||||
</td>
|
||||
</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>
|
||||
</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>
|
|
@ -186,9 +186,8 @@
|
|||
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
|
||||
">Did you know you create your own prediction market on <a class="link-build-content"
|
||||
style="color: #55575d" target="_blank" href="https://manifold.markets">Manifold</a> for
|
||||
any question you care about?</span>
|
||||
</p>
|
||||
|
||||
|
@ -491,10 +490,10 @@
|
|||
">
|
||||
<p style="margin: 10px 0">
|
||||
This e-mail has been sent to {{name}},
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
<a href="{{unsubscribeLink}}" style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||
" target="_blank">click here to unsubscribe</a>.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -440,10 +440,11 @@
|
|||
<p style="margin: 10px 0">
|
||||
This e-mail has been sent to
|
||||
{{name}},
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
<a href="{{unsubscribeLink}}"
|
||||
style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||
" target="_blank">click here to unsubscribe</a> from future recommended markets.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -526,10 +526,19 @@
|
|||
"
|
||||
>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>.
|
||||
<a
|
||||
href="{{unsubscribeUrl}}"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</a
|
||||
>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -1,91 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="
|
||||
<html
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
"
|
||||
>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Market answer</title>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Market answer</title>
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
<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;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
@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>
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -96,29 +89,43 @@
|
|||
line-height: 1.6em;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<table class="body-wrap" style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
bgcolor="#f6f6f6"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<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="
|
||||
"
|
||||
valign="top"
|
||||
></td>
|
||||
<td
|
||||
class="container"
|
||||
width="600"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -127,8 +134,12 @@
|
|||
max-width: 600px !important;
|
||||
clear: both !important;
|
||||
margin: 0 auto;
|
||||
" valign="top">
|
||||
<div class="content" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -136,8 +147,14 @@
|
|||
display: block;
|
||||
margin: 0 auto;
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -145,14 +162,20 @@
|
|||
background-color: #fff;
|
||||
margin: 0;
|
||||
border: 1px solid #e9e9e9;
|
||||
" bgcolor="#fff">
|
||||
<tr style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-wrap aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -160,23 +183,35 @@
|
|||
text-align: center;
|
||||
margin: 0;
|
||||
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,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -185,21 +220,29 @@
|
|||
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 style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<img
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
width="300"
|
||||
style="height: auto"
|
||||
alt="Manifold Markets"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block aligncenter" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -208,8 +251,13 @@
|
|||
text-align: center;
|
||||
margin: 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,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -218,15 +266,19 @@
|
|||
width: 80%;
|
||||
margin: 40px auto;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -234,26 +286,37 @@
|
|||
margin: 0;
|
||||
padding: 5px 0;
|
||||
font-weight: bold;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{avatarUrl}}" width="30" height="30" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src="{{avatarUrl}}"
|
||||
width="30"
|
||||
height="30"
|
||||
style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
{{name}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
alt=""
|
||||
/>
|
||||
{{name}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -261,29 +324,40 @@
|
|||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
" valign="top">
|
||||
<div style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<span style="white-space: pre-line">{{answer}}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<span style="white-space: pre-line"
|
||||
>{{answer}}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="padding: 20px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a href="{{marketUrl}}" target="_blank" style="
|
||||
"
|
||||
>
|
||||
<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;
|
||||
|
@ -301,29 +375,38 @@
|
|||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
mso-border-alt: none;
|
||||
">
|
||||
<span style="
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
line-height: 120%;
|
||||
"><span style="
|
||||
"
|
||||
><span
|
||||
style="
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 18.8px;
|
||||
">View answer</span></span>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer" style="
|
||||
"
|
||||
>View answer</span
|
||||
></span
|
||||
>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div
|
||||
class="footer"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -332,20 +415,28 @@
|
|||
color: #999;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
">
|
||||
<table width="100%" style="
|
||||
"
|
||||
>
|
||||
<table
|
||||
width="100%"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="aligncenter content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -355,9 +446,14 @@
|
|||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" align="center" valign="top">
|
||||
Questions? Come ask in
|
||||
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||
"
|
||||
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;
|
||||
|
@ -365,26 +461,39 @@
|
|||
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>
|
||||
"
|
||||
>our Discord</a
|
||||
>! Or,
|
||||
<a
|
||||
href="{{unsubscribeUrl}}"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</a
|
||||
>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="
|
||||
</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>
|
||||
"
|
||||
valign="top"
|
||||
></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,91 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="
|
||||
<html
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
"
|
||||
>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Market closed</title>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Market closed</title>
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
<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;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
@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>
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -96,29 +89,43 @@
|
|||
line-height: 1.6em;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<table class="body-wrap" style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
bgcolor="#f6f6f6"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<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="
|
||||
"
|
||||
valign="top"
|
||||
></td>
|
||||
<td
|
||||
class="container"
|
||||
width="600"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -127,8 +134,12 @@
|
|||
max-width: 600px !important;
|
||||
clear: both !important;
|
||||
margin: 0 auto;
|
||||
" valign="top">
|
||||
<div class="content" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -136,8 +147,14 @@
|
|||
display: block;
|
||||
margin: 0 auto;
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -145,14 +162,20 @@
|
|||
background-color: #fff;
|
||||
margin: 0;
|
||||
border: 1px solid #e9e9e9;
|
||||
" bgcolor="#fff">
|
||||
<tr style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-wrap aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -160,23 +183,35 @@
|
|||
text-align: center;
|
||||
margin: 0;
|
||||
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,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -185,22 +220,30 @@
|
|||
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="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<img
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
width="300"
|
||||
style="height: auto"
|
||||
alt="Manifold Markets"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -209,18 +252,24 @@
|
|||
margin: 0;
|
||||
padding: 0 0 6px 0;
|
||||
text-align: left;
|
||||
" valign="top">
|
||||
You asked
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
You asked
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -228,8 +277,12 @@
|
|||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" valign="top">
|
||||
<a href="{{url}}" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<a
|
||||
href="{{url}}"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -242,18 +295,24 @@
|
|||
color: #4337c9;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
">
|
||||
{{question}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
{{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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -261,8 +320,12 @@
|
|||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 0px;
|
||||
" valign="top">
|
||||
<h2 class="aligncenter" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<h2
|
||||
class="aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -272,19 +335,25 @@
|
|||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin: 10px 0 0;
|
||||
" align="center">
|
||||
Market closed
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
Market closed
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block aligncenter" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -293,8 +362,13 @@
|
|||
text-align: center;
|
||||
margin: 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,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -302,15 +376,19 @@
|
|||
text-align: left;
|
||||
width: 80%;
|
||||
margin: 40px auto;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -318,90 +396,116 @@
|
|||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
" valign="top">
|
||||
Hi {{name}},
|
||||
<br style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
Hi {{name}},
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
A market you created has closed. It's attracted
|
||||
<span style="font-weight: bold">{{volume}}</span>
|
||||
in bets — congrats!
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
A market you created has closed. It's attracted
|
||||
<span style="font-weight: bold">{{volume}}</span>
|
||||
in bets — congrats!
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
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,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
Thanks,
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
Thanks,
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
Manifold Team
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
Manifold Team
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
/>
|
||||
</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="
|
||||
"
|
||||
>
|
||||
<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;
|
||||
|
@ -419,29 +523,38 @@
|
|||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
mso-border-alt: none;
|
||||
">
|
||||
<span style="
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
line-height: 120%;
|
||||
"><span style="
|
||||
"
|
||||
><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="
|
||||
"
|
||||
>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;
|
||||
|
@ -450,20 +563,28 @@
|
|||
color: #999;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
">
|
||||
<table width="100%" style="
|
||||
"
|
||||
>
|
||||
<table
|
||||
width="100%"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="aligncenter content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -473,9 +594,14 @@
|
|||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" align="center" valign="top">
|
||||
Questions? Come ask in
|
||||
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||
"
|
||||
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;
|
||||
|
@ -483,22 +609,39 @@
|
|||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
">our Discord</a>!
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
"
|
||||
>our Discord</a
|
||||
>! Or,
|
||||
<a
|
||||
href="{{unsubscribeUrl}}"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</a
|
||||
>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="
|
||||
</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>
|
||||
"
|
||||
valign="top"
|
||||
></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,91 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="
|
||||
<html
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
"
|
||||
>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Market comment</title>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Market comment</title>
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
<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;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
@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>
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -96,29 +89,43 @@
|
|||
line-height: 1.6em;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<table class="body-wrap" style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
bgcolor="#f6f6f6"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<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="
|
||||
"
|
||||
valign="top"
|
||||
></td>
|
||||
<td
|
||||
class="container"
|
||||
width="600"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -127,8 +134,12 @@
|
|||
max-width: 600px !important;
|
||||
clear: both !important;
|
||||
margin: 0 auto;
|
||||
" valign="top">
|
||||
<div class="content" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -136,8 +147,14 @@
|
|||
display: block;
|
||||
margin: 0 auto;
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -145,14 +162,20 @@
|
|||
background-color: #fff;
|
||||
margin: 0;
|
||||
border: 1px solid #e9e9e9;
|
||||
" bgcolor="#fff">
|
||||
<tr style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-wrap aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -160,23 +183,35 @@
|
|||
text-align: center;
|
||||
margin: 0;
|
||||
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,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -185,21 +220,29 @@
|
|||
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 style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<img
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
width="300"
|
||||
style="height: auto"
|
||||
alt="Manifold Markets"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block aligncenter" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -208,8 +251,13 @@
|
|||
text-align: center;
|
||||
margin: 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,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -218,42 +266,59 @@
|
|||
width: 80%;
|
||||
margin: 40px auto;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{commentorAvatarUrl}}" width="30" height="30" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src="{{commentorAvatarUrl}}"
|
||||
width="30"
|
||||
height="30"
|
||||
style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{commentorName}}</span>
|
||||
{{betDescription}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
alt=""
|
||||
/>
|
||||
<span style="font-weight: bold"
|
||||
>{{commentorName}}</span
|
||||
>
|
||||
{{betDescription}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -261,29 +326,40 @@
|
|||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
" valign="top">
|
||||
<div style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<span style="white-space: pre-line">{{comment}}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<span style="white-space: pre-line"
|
||||
>{{comment}}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="padding: 20px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a href="{{marketUrl}}" target="_blank" style="
|
||||
"
|
||||
>
|
||||
<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;
|
||||
|
@ -301,29 +377,38 @@
|
|||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
mso-border-alt: none;
|
||||
">
|
||||
<span style="
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
line-height: 120%;
|
||||
"><span style="
|
||||
"
|
||||
><span
|
||||
style="
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 18.8px;
|
||||
">View comment</span></span>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer" style="
|
||||
"
|
||||
>View comment</span
|
||||
></span
|
||||
>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div
|
||||
class="footer"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -332,20 +417,28 @@
|
|||
color: #999;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
">
|
||||
<table width="100%" style="
|
||||
"
|
||||
>
|
||||
<table
|
||||
width="100%"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="aligncenter content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -355,9 +448,14 @@
|
|||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" align="center" valign="top">
|
||||
Questions? Come ask in
|
||||
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||
"
|
||||
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;
|
||||
|
@ -365,26 +463,39 @@
|
|||
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>
|
||||
"
|
||||
>our Discord</a
|
||||
>! Or,
|
||||
<a
|
||||
href="{{unsubscribeUrl}}"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</a
|
||||
>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="
|
||||
</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>
|
||||
"
|
||||
valign="top"
|
||||
></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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>
|
|
@ -1,91 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="
|
||||
<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>
|
||||
|
||||
<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%;
|
||||
}
|
||||
|
||||
<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;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
@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>
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -96,29 +89,43 @@
|
|||
line-height: 1.6em;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<table class="body-wrap" style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
bgcolor="#f6f6f6"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<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="
|
||||
"
|
||||
valign="top"
|
||||
></td>
|
||||
<td
|
||||
class="container"
|
||||
width="600"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -127,8 +134,12 @@
|
|||
max-width: 600px !important;
|
||||
clear: both !important;
|
||||
margin: 0 auto;
|
||||
" valign="top">
|
||||
<div class="content" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -136,8 +147,14 @@
|
|||
display: block;
|
||||
margin: 0 auto;
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -145,14 +162,20 @@
|
|||
background-color: #fff;
|
||||
margin: 0;
|
||||
border: 1px solid #e9e9e9;
|
||||
" bgcolor="#fff">
|
||||
<tr style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-wrap aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -160,23 +183,35 @@
|
|||
text-align: center;
|
||||
margin: 0;
|
||||
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,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -185,22 +220,30 @@
|
|||
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="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<img
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
width="300"
|
||||
style="height: auto"
|
||||
alt="Manifold Markets"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -209,18 +252,24 @@
|
|||
margin: 0;
|
||||
padding: 0 0 6px 0;
|
||||
text-align: left;
|
||||
" valign="top">
|
||||
{{creatorName}} asked
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -228,8 +277,12 @@
|
|||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" valign="top">
|
||||
<a href="{{url}}" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<a
|
||||
href="{{url}}"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -242,18 +295,24 @@
|
|||
color: #4337c9;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
">
|
||||
{{question}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
{{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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -261,8 +320,12 @@
|
|||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 0px;
|
||||
" valign="top">
|
||||
<h2 class="aligncenter" style="
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<h2
|
||||
class="aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -272,19 +335,25 @@
|
|||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin: 10px 0 0;
|
||||
" align="center">
|
||||
Resolved {{outcome}}
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block aligncenter"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -293,8 +362,13 @@
|
|||
text-align: center;
|
||||
margin: 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,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -302,15 +376,19 @@
|
|||
text-align: left;
|
||||
width: 80%;
|
||||
margin: 40px auto;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -318,105 +396,138 @@
|
|||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
" valign="top">
|
||||
Dear {{name}},
|
||||
<br style="
|
||||
"
|
||||
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="
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
A market you bet in has been resolved!
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
A market you bet in has been resolved!
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
Your investment was
|
||||
<span style="font-weight: bold">{{investment}}</span>.
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
Your investment was
|
||||
<span style="font-weight: bold"
|
||||
>M$ {{investment}}</span
|
||||
>.
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
Your payout is
|
||||
<span style="font-weight: bold">{{payout}}</span>.
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
Your payout is
|
||||
<span style="font-weight: bold"
|
||||
>M$ {{payout}}</span
|
||||
>.
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
Thanks,
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
Thanks,
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
Manifold Team
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
Manifold Team
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
"
|
||||
/>
|
||||
</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="
|
||||
"
|
||||
>
|
||||
<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;
|
||||
|
@ -434,29 +545,38 @@
|
|||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
mso-border-alt: none;
|
||||
">
|
||||
<span style="
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
line-height: 120%;
|
||||
"><span style="
|
||||
"
|
||||
><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="
|
||||
"
|
||||
>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;
|
||||
|
@ -465,20 +585,28 @@
|
|||
color: #999;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
">
|
||||
<table width="100%" style="
|
||||
"
|
||||
>
|
||||
<table
|
||||
width="100%"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<tr style="
|
||||
"
|
||||
>
|
||||
<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="
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="aligncenter content-block"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -488,9 +616,14 @@
|
|||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" align="center" valign="top">
|
||||
Questions? Come ask in
|
||||
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||
"
|
||||
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;
|
||||
|
@ -498,26 +631,39 @@
|
|||
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>
|
||||
"
|
||||
>our Discord</a
|
||||
>! Or,
|
||||
<a
|
||||
href="{{unsubscribeUrl}}"
|
||||
style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
"
|
||||
>unsubscribe</a
|
||||
>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="
|
||||
</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>
|
||||
"
|
||||
valign="top"
|
||||
></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,316 +1,519 @@
|
|||
<!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 Markets 7th Day Anniversary Gift!</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">
|
||||
<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>7th Day Anniversary Gift!</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;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
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;
|
||||
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;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
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]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml> </noscript
|
||||
>z
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix {
|
||||
width: 100% !important;
|
||||
}
|
||||
</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)">
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
</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;
|
||||
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;
|
||||
}
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing:normal;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]-->
|
||||
<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;">
|
||||
<body style="word-spacing: normal; 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]-->
|
||||
<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 style="width:550px;"><a href="https://manifold.markets/home" target="_blank"><img
|
||||
alt="" height="auto" src="https://i.imgur.com/8EP8Y8q.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>
|
||||
<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: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;">Thanks for
|
||||
using Manifold Markets. Running low
|
||||
on mana (M$)? Click the link below to receive a one time gift of M$500!</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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;"> </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 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>
|
||||
</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]><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;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
</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: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%">
|
||||
<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 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;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>
|
||||
<tr>
|
||||
<td style="width: 550px">
|
||||
<a
|
||||
href="https://manifold.markets/home"
|
||||
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>
|
||||
</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>
|
||||
<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="
|
||||
text-align: center;
|
||||
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;
|
||||
"
|
||||
>Hopefully you haven't gambled all your M$
|
||||
away already... but if you have I bring good
|
||||
news! Click the link below to recieve a one time
|
||||
gift of M$ 500 to your account!</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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'd be willing to make a market betting
|
||||
you'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>
|
||||
</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: 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;
|
||||
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;
|
||||
"
|
||||
>
|
||||
<p style="margin: 10px 0">
|
||||
This e-mail has been sent to {{name}},
|
||||
<a
|
||||
href="{{unsubscribeLink}}"
|
||||
style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
"
|
||||
target="_blank"
|
||||
>click here to unsubscribe</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
|
|
|
@ -214,12 +214,10 @@
|
|||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;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>
|
||||
to {{name}}, <a href="{{unsubscribeLink}}"
|
||||
style="color:inherit;text-decoration:none;"
|
||||
target="_blank">click here to
|
||||
unsubscribe</a>.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,411 +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>Weekly Portfolio Update 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%;
|
||||
font-family:"Readex Pro", Helvetica, sans-serif;
|
||||
}
|
||||
table { margin: 0 auto; }
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0;
|
||||
mso-table-rspace: 0;
|
||||
}
|
||||
th {color:#000000; font-size:17px;}
|
||||
th, td {padding: 10px; }
|
||||
td{ font-size: 17px}
|
||||
th, td { vertical-align: center; text-align: left }
|
||||
a { vertical-align: center; text-align: left}
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
p.change{
|
||||
margin: 0; vertical-align: middle;font-size:16px;display: inline; padding: 2px; border-radius: 5px; width: 20px; text-align: right;
|
||||
}
|
||||
p.prob{
|
||||
font-size: 22px;display: inline; vertical-align: middle; font-weight: bold; width: 50px;
|
||||
}
|
||||
a.question{
|
||||
font-size: 18px;display: inline; vertical-align: middle;
|
||||
}
|
||||
td.question{
|
||||
vertical-align: middle; padding-bottom: 15px; text-align: left;
|
||||
}
|
||||
td.probs{
|
||||
text-align: right; padding-left: 10px; min-width: 115px
|
||||
}
|
||||
</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; margin-bottom: 30px" 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: 0px;"
|
||||
data-testid="4XoHRGw1Y">
|
||||
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
We ran the numbers and here's how you did this past week!
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!--/ show 5 columns with headers titled: Investment value, 7-day change, current balance, tips received, and markets made/-->
|
||||
<tr>
|
||||
<tr>
|
||||
<th style='font-size: 22px; text-align: center'>
|
||||
Profit
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style='padding-bottom: 30px; text-align: center'>
|
||||
<p class='change' style='font-size: 24px; padding:4px; {{profit_style}}'>
|
||||
{{profit}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:10px 20px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px; ">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style='width: 170px'>
|
||||
🔥 Prediction streak
|
||||
</th>
|
||||
<td>
|
||||
{{prediction_streak}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
💸 Tips received
|
||||
</th>
|
||||
<td>
|
||||
{{tips_received}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
📈 Markets traded
|
||||
</th>
|
||||
<td>
|
||||
{{markets_traded}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
❓ Markets created
|
||||
</th>
|
||||
|
||||
<td>
|
||||
{{markets_created}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='width: 55px'>
|
||||
🥳 Traders attracted
|
||||
</th>
|
||||
<td>
|
||||
{{unique_bettors}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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>
|
|
@ -1,510 +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>Weekly Portfolio Update 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%;
|
||||
font-family:"Readex Pro", Helvetica, sans-serif;
|
||||
}
|
||||
table { margin: 0 auto; }
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0;
|
||||
mso-table-rspace: 0;
|
||||
}
|
||||
th {color:#000000; font-size:17px;}
|
||||
th, td {padding: 10px; }
|
||||
td{ font-size: 17px}
|
||||
th, td { vertical-align: center; text-align: left }
|
||||
a { vertical-align: center; text-align: left}
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
p.change{
|
||||
margin: 0; vertical-align: middle;font-size:16px;display: inline; padding: 2px; border-radius: 5px; width: 20px; text-align: right;
|
||||
}
|
||||
p.prob{
|
||||
font-size: 22px;display: inline; vertical-align: middle; font-weight: bold; width: 50px;
|
||||
}
|
||||
a.question{
|
||||
font-size: 18px;display: inline; vertical-align: middle;
|
||||
}
|
||||
td.question{
|
||||
vertical-align: middle; padding-bottom: 15px; text-align: left;
|
||||
}
|
||||
td.probs{
|
||||
text-align: right; padding-left: 10px; min-width: 115px
|
||||
}
|
||||
</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: 0px;"
|
||||
data-testid="4XoHRGw1Y">
|
||||
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
We ran the numbers and here's how you did this past week!
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!--/ show 5 columns with headers titled: Investment value, 7-day change, current balance, tips received, and markets made/-->
|
||||
<tr>
|
||||
<tr>
|
||||
<th style='font-size: 22px; text-align: center'>
|
||||
Profit
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style='padding-bottom: 30px; text-align: center'>
|
||||
<p class='change' style='font-size: 24px; padding:4px; {{profit_style}}'>
|
||||
{{profit}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:10px 20px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px; ">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style='width: 170px'>
|
||||
🔥 Prediction streak
|
||||
</th>
|
||||
<td>
|
||||
{{prediction_streak}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
💸 Tips received
|
||||
</th>
|
||||
<td>
|
||||
{{tips_received}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
📈 Markets traded
|
||||
</th>
|
||||
<td>
|
||||
{{markets_traded}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
❓ Markets created
|
||||
</th>
|
||||
|
||||
<td>
|
||||
{{markets_created}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='width: 55px'>
|
||||
🥳 Traders attracted
|
||||
</th>
|
||||
<td>
|
||||
{{unique_bettors}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:20px;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: 20px; margin-bottom: 20px;"
|
||||
data-testid="4XoHRGw1Y">
|
||||
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
And here's some recent changes in your investments:
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0; padding-left:10px;padding-top:10px;padding-bottom:0;word-break:break-word;">
|
||||
<table role="presentation">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class='question'>
|
||||
<a class='question' href='{{question1Url}}'>
|
||||
{{question1Title}}
|
||||
<!-- Will the US economy recover from the pandemic?-->
|
||||
</a>
|
||||
</td>
|
||||
<td class='probs'>
|
||||
<p class='prob'>
|
||||
{{question1Prob}}
|
||||
<!-- 9.9%-->
|
||||
<p class='change' style='{{question1ChangeStyle}}'>
|
||||
{{question1Change}}
|
||||
<!-- +17%-->
|
||||
</p>
|
||||
</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class='question'>
|
||||
<a class='question' href='{{question2Url}}'>
|
||||
{{question2Title}}
|
||||
<!-- Will the US economy recover from the pandemic? blah blah blah-->
|
||||
</a>
|
||||
</td>
|
||||
<td class='probs'>
|
||||
<p class='prob'>
|
||||
{{question2Prob}}
|
||||
<!-- 99.9%-->
|
||||
<p class='change' style='{{question2ChangeStyle}}'>
|
||||
{{question2Change}}
|
||||
<!-- +7%-->
|
||||
</p>
|
||||
</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<!-- <td style="{{investment_value_style}}">-->
|
||||
<td class='question'>
|
||||
<a class='question' href='{{question3Url}}'>
|
||||
{{question3Title}}
|
||||
<!-- Will the US economy recover from the pandemic?-->
|
||||
</a>
|
||||
</td>
|
||||
<td class='probs'>
|
||||
<p class='prob'>
|
||||
{{question3Prob}}
|
||||
<!-- 99.9%-->
|
||||
<p class='change' style='{{question3ChangeStyle}}'>
|
||||
{{question3Change}}
|
||||
<!-- +17%-->
|
||||
</p>
|
||||
</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<!-- <td style="{{investment_value_style}}">-->
|
||||
<td class='question'>
|
||||
<a class='question' href='{{question4Url}}'>
|
||||
{{question4Title}}
|
||||
<!-- Will the US economy recover from the pandemic?-->
|
||||
</a>
|
||||
</td>
|
||||
<td class='probs'>
|
||||
<p class='prob'>
|
||||
{{question4Prob}}
|
||||
<!-- 99.9%-->
|
||||
<p class='change' style='{{question4ChangeStyle}}'>
|
||||
{{question4Change}}
|
||||
<!-- +17%-->
|
||||
</p>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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>
|
|
@ -137,7 +137,7 @@
|
|||
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;">
|
||||
Welcome! Manifold Markets is a play-money prediction market platform where you can predict
|
||||
Welcome! Manifold Markets is a play-money prediction market platform where you can bet on
|
||||
anything, from elections to Elon Musk to scientific papers to the NBA. </span></p>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -210,7 +210,7 @@
|
|||
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>Refer
|
||||
your friends</u></span></a> and earn M$250 for each signup!</span></li>
|
||||
your friends</u></span></a> and earn M$500 for each signup!</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;"
|
||||
|
@ -286,12 +286,9 @@
|
|||
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;">
|
||||
<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>
|
||||
<p style="margin: 10px 0;">This e-mail has been sent to {{name}}, <a
|
||||
href="{{unsubscribeLink}}” style=" color:inherit;text-decoration:none;"
|
||||
target="_blank">click here to unsubscribe</a>.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { DOMAIN } from '../../common/envs/constants'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { DPM_CREATOR_FEE } from '../../common/fees'
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
import {
|
||||
formatLargeNumber,
|
||||
|
@ -12,19 +15,15 @@ import { getValueFromBucket } from '../../common/calculate-dpm'
|
|||
import { formatNumericProbability } from '../../common/pseudo-numeric'
|
||||
|
||||
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
||||
import { contractUrl, getUser, log } from './utils'
|
||||
import { getPrivateUser, getUser } from './utils'
|
||||
import { getFunctionUrl } from '../../common/api'
|
||||
import { richTextToString } from '../../common/util/parse'
|
||||
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
|
||||
import { notification_reason_types } from '../../common/notification'
|
||||
import { Dictionary } from 'lodash'
|
||||
import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences'
|
||||
import {
|
||||
PerContractInvestmentsData,
|
||||
OverallPerformanceData,
|
||||
} from './weekly-portfolio-emails'
|
||||
|
||||
const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe')
|
||||
|
||||
export const sendMarketResolutionEmail = async (
|
||||
reason: notification_reason_types,
|
||||
privateUser: PrivateUser,
|
||||
userId: string,
|
||||
investment: number,
|
||||
payout: number,
|
||||
creator: User,
|
||||
|
@ -34,13 +33,15 @@ export const sendMarketResolutionEmail = async (
|
|||
resolutionProbability?: number,
|
||||
resolutions?: { [outcome: string]: number }
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
const privateUser = await getPrivateUser(userId)
|
||||
if (
|
||||
!privateUser ||
|
||||
privateUser.unsubscribedFromResolutionEmails ||
|
||||
!privateUser.email
|
||||
)
|
||||
if (!privateUser || !privateUser.email || !sendToEmail) return
|
||||
return
|
||||
|
||||
const user = await getUser(privateUser.id)
|
||||
const user = await getUser(userId)
|
||||
if (!user) return
|
||||
|
||||
const outcome = toDisplayResolution(
|
||||
|
@ -52,16 +53,13 @@ export const sendMarketResolutionEmail = async (
|
|||
|
||||
const subject = `Resolved ${outcome}: ${contract.question}`
|
||||
|
||||
const creatorPayoutText =
|
||||
creatorPayout >= 1 && privateUser.id === creator.id
|
||||
? ` (plus ${formatMoney(creatorPayout)} in commissions)`
|
||||
: ''
|
||||
// const creatorPayoutText =
|
||||
// userId === creator.id
|
||||
// ? ` (plus ${formatMoney(creatorPayout)} in commissions)`
|
||||
// : ''
|
||||
|
||||
const correctedInvestment =
|
||||
Number.isNaN(investment) || investment < 0 ? 0 : investment
|
||||
const displayedInvestment = formatMoney(correctedInvestment)
|
||||
|
||||
const displayedPayout = formatMoney(payout)
|
||||
const emailType = 'market-resolved'
|
||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
const templateData: market_resolved_template = {
|
||||
userId: user.id,
|
||||
|
@ -69,8 +67,8 @@ export const sendMarketResolutionEmail = async (
|
|||
creatorName: creator.name,
|
||||
question: contract.question,
|
||||
outcome,
|
||||
investment: displayedInvestment,
|
||||
payout: displayedPayout + creatorPayoutText,
|
||||
investment: `${Math.floor(investment)}`,
|
||||
payout: `${Math.floor(payout)}`,
|
||||
url: `https://${DOMAIN}/${creator.username}/${contract.slug}`,
|
||||
unsubscribeUrl,
|
||||
}
|
||||
|
@ -81,7 +79,7 @@ export const sendMarketResolutionEmail = async (
|
|||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
correctedInvestment === 0 ? 'market-resolved-no-bets' : 'market-resolved',
|
||||
'market-resolved',
|
||||
templateData
|
||||
)
|
||||
}
|
||||
|
@ -150,13 +148,11 @@ export const sendWelcomeEmail = async (
|
|||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name } = user
|
||||
const { name, id: userId } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'onboarding_flow'
|
||||
)
|
||||
const emailType = 'generic'
|
||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
|
@ -164,7 +160,7 @@ export const sendWelcomeEmail = async (
|
|||
'welcome',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeUrl,
|
||||
unsubscribeLink,
|
||||
},
|
||||
{
|
||||
from: 'David from Manifold <david@manifold.markets>',
|
||||
|
@ -184,7 +180,7 @@ export const sendPersonalFollowupEmail = async (
|
|||
|
||||
const emailBody = `Hi ${firstName},
|
||||
|
||||
Thanks for signing up! I'm one of the cofounders of Manifold Markets, and was wondering how you've found your experience on the platform so far?
|
||||
Thanks for signing up! I'm one of the cofounders of Manifold Markets, and was wondering how you've found your exprience on the platform so far?
|
||||
|
||||
If you haven't already, I encourage you to try creating your own prediction market (https://manifold.markets/create) and joining our Discord chat (https://discord.com/invite/eHQBNBqXuh).
|
||||
|
||||
|
@ -212,16 +208,18 @@ export const sendOneWeekBonusEmail = async (
|
|||
user: User,
|
||||
privateUser: PrivateUser
|
||||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromGenericEmails
|
||||
)
|
||||
return
|
||||
|
||||
const { name } = user
|
||||
const { name, id: userId } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'onboarding_flow'
|
||||
)
|
||||
if (!sendToEmail) return
|
||||
const emailType = 'generic'
|
||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
|
@ -229,7 +227,7 @@ export const sendOneWeekBonusEmail = async (
|
|||
'one-week',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeUrl,
|
||||
unsubscribeLink,
|
||||
manalink: 'https://manifold.markets/link/lj4JbBvE',
|
||||
},
|
||||
{
|
||||
|
@ -243,22 +241,26 @@ export const sendCreatorGuideEmail = async (
|
|||
privateUser: PrivateUser,
|
||||
sendTime: string
|
||||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'onboarding_flow'
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromGenericEmails
|
||||
)
|
||||
if (!sendToEmail) return
|
||||
return
|
||||
|
||||
const { name, id: userId } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const emailType = 'generic'
|
||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Create your own prediction market',
|
||||
'creating-market',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeUrl,
|
||||
unsubscribeLink,
|
||||
},
|
||||
{
|
||||
from: 'David from Manifold <david@manifold.markets>',
|
||||
|
@ -271,23 +273,26 @@ export const sendThankYouEmail = async (
|
|||
user: User,
|
||||
privateUser: PrivateUser
|
||||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'thank_you_for_purchases'
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromGenericEmails
|
||||
)
|
||||
return
|
||||
|
||||
const { name, id: userId } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const emailType = 'generic'
|
||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
if (!sendToEmail) return
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Thanks for your Manifold purchase',
|
||||
'thank-you',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeUrl,
|
||||
unsubscribeLink,
|
||||
},
|
||||
{
|
||||
from: 'David from Manifold <david@manifold.markets>',
|
||||
|
@ -296,21 +301,26 @@ export const sendThankYouEmail = async (
|
|||
}
|
||||
|
||||
export const sendMarketCloseEmail = async (
|
||||
reason: notification_reason_types,
|
||||
user: User,
|
||||
privateUser: PrivateUser,
|
||||
contract: Contract
|
||||
) => {
|
||||
if (!privateUser.email) return
|
||||
if (
|
||||
!privateUser ||
|
||||
privateUser.unsubscribedFromResolutionEmails ||
|
||||
!privateUser.email
|
||||
)
|
||||
return
|
||||
|
||||
const { username, name, id: userId } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const { question, slug, volume } = contract
|
||||
const { question, slug, volume, mechanism, collectedFees } = contract
|
||||
|
||||
const url = `https://${DOMAIN}/${username}/${slug}`
|
||||
const emailType = 'market-resolve'
|
||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
// We ignore if they were able to unsubscribe from market close emails, this is a necessary email
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Your market has closed',
|
||||
|
@ -318,35 +328,43 @@ export const sendMarketCloseEmail = async (
|
|||
{
|
||||
question,
|
||||
url,
|
||||
unsubscribeUrl: '',
|
||||
unsubscribeUrl,
|
||||
userId,
|
||||
name: firstName,
|
||||
volume: formatMoney(volume),
|
||||
creatorFee:
|
||||
mechanism === 'dpm-2'
|
||||
? `${DPM_CREATOR_FEE * 100}% of the profits`
|
||||
: formatMoney(collectedFees.creatorFee),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const sendNewCommentEmail = async (
|
||||
reason: notification_reason_types,
|
||||
privateUser: PrivateUser,
|
||||
userId: string,
|
||||
commentCreator: User,
|
||||
contract: Contract,
|
||||
commentText: string,
|
||||
commentId: string,
|
||||
comment: Comment,
|
||||
bet?: Bet,
|
||||
answerText?: string,
|
||||
answerId?: string
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
const privateUser = await getPrivateUser(userId)
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromCommentEmails
|
||||
)
|
||||
if (!privateUser || !privateUser.email || !sendToEmail) return
|
||||
return
|
||||
|
||||
const { question } = contract
|
||||
const marketUrl = `https://${DOMAIN}/${contract.creatorUsername}/${contract.slug}#${commentId}`
|
||||
const { question, creatorUsername, slug } = contract
|
||||
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}#${comment.id}`
|
||||
const emailType = 'market-comment'
|
||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
|
||||
const { content } = comment
|
||||
const text = richTextToString(content)
|
||||
|
||||
let betDescription = ''
|
||||
if (bet) {
|
||||
|
@ -360,7 +378,7 @@ export const sendNewCommentEmail = async (
|
|||
const from = `${commentorName} on Manifold <no-reply@manifold.markets>`
|
||||
|
||||
if (contract.outcomeType === 'FREE_RESPONSE' && answerId && answerText) {
|
||||
const answerNumber = answerId ? `#${answerId}` : ''
|
||||
const answerNumber = `#${answerId}`
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
|
@ -371,7 +389,7 @@ export const sendNewCommentEmail = async (
|
|||
answerNumber,
|
||||
commentorName,
|
||||
commentorAvatarUrl: commentorAvatarUrl ?? '',
|
||||
comment: commentText,
|
||||
comment: text,
|
||||
marketUrl,
|
||||
unsubscribeUrl,
|
||||
betDescription,
|
||||
|
@ -392,7 +410,7 @@ export const sendNewCommentEmail = async (
|
|||
{
|
||||
commentorName,
|
||||
commentorAvatarUrl: commentorAvatarUrl ?? '',
|
||||
comment: commentText,
|
||||
comment: text,
|
||||
marketUrl,
|
||||
unsubscribeUrl,
|
||||
betDescription,
|
||||
|
@ -403,26 +421,29 @@ export const sendNewCommentEmail = async (
|
|||
}
|
||||
|
||||
export const sendNewAnswerEmail = async (
|
||||
reason: notification_reason_types,
|
||||
privateUser: PrivateUser,
|
||||
name: string,
|
||||
text: string,
|
||||
contract: Contract,
|
||||
avatarUrl?: string
|
||||
answer: Answer,
|
||||
contract: Contract
|
||||
) => {
|
||||
const { creatorId } = contract
|
||||
// Don't send the creator's own answers.
|
||||
if (privateUser.id === creatorId) return
|
||||
// Send to just the creator for now.
|
||||
const { creatorId: userId } = contract
|
||||
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
// Don't send the creator's own answers.
|
||||
if (answer.userId === userId) return
|
||||
|
||||
const privateUser = await getPrivateUser(userId)
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromAnswerEmails
|
||||
)
|
||||
if (!privateUser.email || !sendToEmail) return
|
||||
return
|
||||
|
||||
const { question, creatorUsername, slug } = contract
|
||||
const { name, avatarUrl, text } = answer
|
||||
|
||||
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}`
|
||||
const emailType = 'market-answer'
|
||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
const subject = `New answer on ${question}`
|
||||
const from = `${name} <info@manifold.markets>`
|
||||
|
@ -448,13 +469,15 @@ export const sendInterestingMarketsEmail = async (
|
|||
contractsToSend: Contract[],
|
||||
deliveryTime?: string
|
||||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'trending_markets'
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser?.unsubscribedFromWeeklyTrendingEmails
|
||||
)
|
||||
if (!sendToEmail) return
|
||||
return
|
||||
|
||||
const emailType = 'weekly-trending'
|
||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${privateUser.id}&type=${emailType}`
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
@ -465,7 +488,7 @@ export const sendInterestingMarketsEmail = async (
|
|||
'interesting-markets',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeUrl,
|
||||
unsubscribeLink: unsubscribeUrl,
|
||||
|
||||
question1Title: contractsToSend[0].question,
|
||||
question1Link: contractUrl(contractsToSend[0]),
|
||||
|
@ -490,146 +513,10 @@ export const sendInterestingMarketsEmail = async (
|
|||
)
|
||||
}
|
||||
|
||||
function contractUrl(contract: Contract) {
|
||||
return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}`
|
||||
}
|
||||
|
||||
function imageSourceUrl(contract: Contract) {
|
||||
return buildCardUrl(getOpenGraphProps(contract))
|
||||
}
|
||||
|
||||
export const sendNewFollowedMarketEmail = async (
|
||||
reason: notification_reason_types,
|
||||
userId: string,
|
||||
privateUser: PrivateUser,
|
||||
contract: Contract
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
)
|
||||
if (!privateUser.email || !sendToEmail) return
|
||||
const user = await getUser(privateUser.id)
|
||||
if (!user) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const creatorName = contract.creatorName
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
`${creatorName} asked ${contract.question}`,
|
||||
'new-market-from-followed-user',
|
||||
{
|
||||
name: firstName,
|
||||
creatorName,
|
||||
unsubscribeUrl,
|
||||
questionTitle: contract.question,
|
||||
questionUrl: contractUrl(contract),
|
||||
questionImgSrc: imageSourceUrl(contract),
|
||||
},
|
||||
{
|
||||
from: `${creatorName} on Manifold <no-reply@manifold.markets>`,
|
||||
}
|
||||
)
|
||||
}
|
||||
export const sendNewUniqueBettorsEmail = async (
|
||||
reason: notification_reason_types,
|
||||
userId: string,
|
||||
privateUser: PrivateUser,
|
||||
contract: Contract,
|
||||
totalPredictors: number,
|
||||
newPredictors: User[],
|
||||
userBets: Dictionary<[Bet, ...Bet[]]>,
|
||||
bonusAmount: number
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
)
|
||||
if (!privateUser.email || !sendToEmail) return
|
||||
const user = await getUser(privateUser.id)
|
||||
if (!user) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const creatorName = contract.creatorName
|
||||
// make the emails stack for the same contract
|
||||
const subject = `You made a popular market! ${
|
||||
contract.question.length > 50
|
||||
? contract.question.slice(0, 50) + '...'
|
||||
: contract.question
|
||||
} just got ${
|
||||
newPredictors.length
|
||||
} new predictions. Check out who's predicting on it inside.`
|
||||
const templateData: Record<string, string> = {
|
||||
name: firstName,
|
||||
creatorName,
|
||||
totalPredictors: totalPredictors.toString(),
|
||||
bonusString: formatMoney(bonusAmount),
|
||||
marketTitle: contract.question,
|
||||
marketUrl: contractUrl(contract),
|
||||
unsubscribeUrl,
|
||||
newPredictors: newPredictors.length.toString(),
|
||||
}
|
||||
|
||||
newPredictors.forEach((p, i) => {
|
||||
templateData[`bettor${i + 1}Name`] = p.name
|
||||
if (p.avatarUrl) templateData[`bettor${i + 1}AvatarUrl`] = p.avatarUrl
|
||||
const bet = userBets[p.id][0]
|
||||
if (bet) {
|
||||
const { amount, sale } = bet
|
||||
templateData[`bet${i + 1}Description`] = `${
|
||||
sale || amount < 0 ? 'sold' : 'bought'
|
||||
} ${formatMoney(Math.abs(amount))}`
|
||||
}
|
||||
})
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
newPredictors.length === 1 ? 'new-unique-bettor' : 'new-unique-bettors',
|
||||
templateData,
|
||||
{
|
||||
from: `Manifold Markets <no-reply@manifold.markets>`,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const sendWeeklyPortfolioUpdateEmail = async (
|
||||
user: User,
|
||||
privateUser: PrivateUser,
|
||||
investments: PerContractInvestmentsData[],
|
||||
overallPerformance: OverallPerformanceData
|
||||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'profit_loss_updates'
|
||||
)
|
||||
|
||||
if (!sendToEmail) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const templateData: Record<string, string> = {
|
||||
name: firstName,
|
||||
unsubscribeUrl,
|
||||
...overallPerformance,
|
||||
}
|
||||
investments.forEach((investment, i) => {
|
||||
templateData[`question${i + 1}Title`] = investment.questionTitle
|
||||
templateData[`question${i + 1}Url`] = investment.questionUrl
|
||||
templateData[`question${i + 1}Prob`] = investment.questionProb
|
||||
templateData[`question${i + 1}Change`] = formatMoney(investment.profit)
|
||||
templateData[`question${i + 1}ChangeStyle`] = investment.profitStyle
|
||||
})
|
||||
|
||||
await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
// 'iansphilips@gmail.com',
|
||||
`Here's your weekly portfolio update!`,
|
||||
investments.length === 0
|
||||
? 'portfolio-update-no-movers'
|
||||
: 'portfolio-update',
|
||||
templateData
|
||||
)
|
||||
log('Sent portfolio update email to', privateUser.email)
|
||||
}
|
||||
|
|
33
functions/src/get-custom-token.ts
Normal file
33
functions/src/get-custom-token.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import {
|
||||
APIError,
|
||||
EndpointDefinition,
|
||||
lookupUser,
|
||||
parseCredentials,
|
||||
writeResponseError,
|
||||
} from './api'
|
||||
|
||||
const opts = {
|
||||
method: 'GET',
|
||||
minInstances: 1,
|
||||
concurrency: 100,
|
||||
memory: '2GiB',
|
||||
cpu: 1,
|
||||
} as const
|
||||
|
||||
export const getcustomtoken: EndpointDefinition = {
|
||||
opts,
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
const credentials = await parseCredentials(req)
|
||||
if (credentials.kind != 'jwt') {
|
||||
throw new APIError(403, 'API keys cannot mint custom tokens.')
|
||||
}
|
||||
const user = await lookupUser(credentials)
|
||||
const token = await admin.auth().createCustomToken(user.uid)
|
||||
res.status(200).json({ token: token })
|
||||
} catch (e) {
|
||||
writeResponseError(e, res)
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { CPMMContract } from '../../../common/contract'
|
||||
import { isProd } from '../utils'
|
||||
import {
|
||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
} from '../../../common/antes'
|
||||
import { getNewLiquidityProvision } from '../../../common/add-liquidity'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const addHouseSubsidy = (contractId: string, amount: number) => {
|
||||
return firestore.runTransaction(async (transaction) => {
|
||||
const newLiquidityProvisionDoc = firestore
|
||||
.collection(`contracts/${contractId}/liquidity`)
|
||||
.doc()
|
||||
|
||||
const providerId = isProd()
|
||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
const snap = await contractDoc.get()
|
||||
const contract = snap.data() as CPMMContract
|
||||
|
||||
const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } =
|
||||
getNewLiquidityProvision(
|
||||
providerId,
|
||||
amount,
|
||||
contract,
|
||||
newLiquidityProvisionDoc.id
|
||||
)
|
||||
|
||||
transaction.update(contractDoc, {
|
||||
subsidyPool: newSubsidyPool,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
} as Partial<CPMMContract>)
|
||||
|
||||
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
|
||||
})
|
||||
}
|
|
@ -9,7 +9,7 @@ export * from './on-create-user'
|
|||
export * from './on-create-bet'
|
||||
export * from './on-create-comment-on-contract'
|
||||
export * from './on-view'
|
||||
export { scheduleUpdateMetrics } from './update-metrics'
|
||||
export * from './update-metrics'
|
||||
export * from './update-stats'
|
||||
export * from './update-loans'
|
||||
export * from './backup-db'
|
||||
|
@ -21,17 +21,16 @@ export * from './on-follow-user'
|
|||
export * from './on-unfollow-user'
|
||||
export * from './on-create-liquidity-provision'
|
||||
export * from './on-update-group'
|
||||
export * from './on-create-group'
|
||||
export * from './on-update-user'
|
||||
export * from './on-create-comment-on-group'
|
||||
export * from './on-create-txn'
|
||||
export * from './on-delete-group'
|
||||
export * from './score-contracts'
|
||||
export * from './weekly-markets-emails'
|
||||
export * from './reset-betting-streaks'
|
||||
export * from './reset-weekly-emails-flags'
|
||||
export * from './reset-weekly-emails-flag'
|
||||
export * from './on-update-contract-follow'
|
||||
export * from './on-update-like'
|
||||
export * from './weekly-portfolio-emails'
|
||||
export * from './drizzle-liquidity'
|
||||
|
||||
// v2
|
||||
export * from './health'
|
||||
|
@ -45,14 +44,13 @@ export * from './sell-bet'
|
|||
export * from './sell-shares'
|
||||
export * from './claim-manalink'
|
||||
export * from './create-market'
|
||||
export * from './add-liquidity'
|
||||
export * from './withdraw-liquidity'
|
||||
export * from './create-group'
|
||||
export * from './resolve-market'
|
||||
export * from './unsubscribe'
|
||||
export * from './stripe'
|
||||
export * from './mana-bonus-email'
|
||||
export * from './close-market'
|
||||
export * from './update-comment-bounty'
|
||||
export * from './add-subsidy'
|
||||
|
||||
import { health } from './health'
|
||||
import { transact } from './transact'
|
||||
|
@ -65,19 +63,15 @@ import { sellbet } from './sell-bet'
|
|||
import { sellshares } from './sell-shares'
|
||||
import { claimmanalink } from './claim-manalink'
|
||||
import { createmarket } from './create-market'
|
||||
import { createcomment } from './create-comment'
|
||||
import { addcommentbounty, awardcommentbounty } from './update-comment-bounty'
|
||||
import { addliquidity } from './add-liquidity'
|
||||
import { withdrawliquidity } from './withdraw-liquidity'
|
||||
import { creategroup } from './create-group'
|
||||
import { resolvemarket } from './resolve-market'
|
||||
import { closemarket } from './close-market'
|
||||
import { unsubscribe } from './unsubscribe'
|
||||
import { stripewebhook, createcheckoutsession } from './stripe'
|
||||
import { getcurrentuser } from './get-current-user'
|
||||
import { acceptchallenge } from './accept-challenge'
|
||||
import { createpost } from './create-post'
|
||||
import { savetwitchcredentials } from './save-twitch-credentials'
|
||||
import { updatemetrics } from './update-metrics'
|
||||
import { addsubsidy } from './add-subsidy'
|
||||
import { getcustomtoken } from './get-custom-token'
|
||||
|
||||
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
||||
return onRequest(opts, handler as any)
|
||||
|
@ -93,21 +87,16 @@ const sellBetFunction = toCloudFunction(sellbet)
|
|||
const sellSharesFunction = toCloudFunction(sellshares)
|
||||
const claimManalinkFunction = toCloudFunction(claimmanalink)
|
||||
const createMarketFunction = toCloudFunction(createmarket)
|
||||
const addSubsidyFunction = toCloudFunction(addsubsidy)
|
||||
const addCommentBounty = toCloudFunction(addcommentbounty)
|
||||
const createCommentFunction = toCloudFunction(createcomment)
|
||||
const awardCommentBounty = toCloudFunction(awardcommentbounty)
|
||||
const addLiquidityFunction = toCloudFunction(addliquidity)
|
||||
const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity)
|
||||
const createGroupFunction = toCloudFunction(creategroup)
|
||||
const resolveMarketFunction = toCloudFunction(resolvemarket)
|
||||
const closeMarketFunction = toCloudFunction(closemarket)
|
||||
const unsubscribeFunction = toCloudFunction(unsubscribe)
|
||||
const stripeWebhookFunction = toCloudFunction(stripewebhook)
|
||||
const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
|
||||
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
||||
const acceptChallenge = toCloudFunction(acceptchallenge)
|
||||
const createPostFunction = toCloudFunction(createpost)
|
||||
const saveTwitchCredentials = toCloudFunction(savetwitchcredentials)
|
||||
const updateMetricsFunction = toCloudFunction(updatemetrics)
|
||||
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
||||
|
||||
export {
|
||||
healthFunction as health,
|
||||
|
@ -121,19 +110,14 @@ export {
|
|||
sellSharesFunction as sellshares,
|
||||
claimManalinkFunction as claimmanalink,
|
||||
createMarketFunction as createmarket,
|
||||
addSubsidyFunction as addsubsidy,
|
||||
addLiquidityFunction as addliquidity,
|
||||
withdrawLiquidityFunction as withdrawliquidity,
|
||||
createGroupFunction as creategroup,
|
||||
resolveMarketFunction as resolvemarket,
|
||||
closeMarketFunction as closemarket,
|
||||
unsubscribeFunction as unsubscribe,
|
||||
stripeWebhookFunction as stripewebhook,
|
||||
createCheckoutSessionFunction as createcheckoutsession,
|
||||
getCurrentUserFunction as getcurrentuser,
|
||||
acceptChallenge as acceptchallenge,
|
||||
createPostFunction as createpost,
|
||||
saveTwitchCredentials as savetwitchcredentials,
|
||||
createCommentFunction as createcomment,
|
||||
addCommentBounty as addcommentbounty,
|
||||
awardCommentBounty as awardcommentbounty,
|
||||
updateMetricsFunction as updatemetrics,
|
||||
getCustomTokenFunction as getcustomtoken,
|
||||
}
|
||||
|
|
|
@ -3,10 +3,9 @@ import * as admin from 'firebase-admin'
|
|||
|
||||
import { Contract } from '../../common/contract'
|
||||
import { getPrivateUser, getUserByUsername } from './utils'
|
||||
import { createMarketClosedNotification } from './create-notification'
|
||||
import { DAY_MS } from '../../common/util/time'
|
||||
import { sendMarketCloseEmail } from './emails'
|
||||
import { createNotification } from './create-notification'
|
||||
|
||||
const SEND_NOTIFICATIONS_EVERY_DAYS = 5
|
||||
export const marketCloseNotifications = functions
|
||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||
.pubsub.schedule('every 1 hours')
|
||||
|
@ -16,31 +15,31 @@ export const marketCloseNotifications = functions
|
|||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export async function sendMarketCloseEmails() {
|
||||
async function sendMarketCloseEmails() {
|
||||
const contracts = await firestore.runTransaction(async (transaction) => {
|
||||
const snap = await transaction.get(
|
||||
firestore.collection('contracts').where('isResolved', '!=', true)
|
||||
)
|
||||
const contracts = snap.docs.map((doc) => doc.data() as Contract)
|
||||
const now = Date.now()
|
||||
const closeContracts = contracts.filter(
|
||||
(contract) =>
|
||||
contract.closeTime &&
|
||||
contract.closeTime < now &&
|
||||
shouldSendFirstOrFollowUpCloseNotification(contract)
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
closeContracts.map(async (contract) => {
|
||||
await transaction.update(
|
||||
firestore.collection('contracts').doc(contract.id),
|
||||
{
|
||||
closeEmailsSent: admin.firestore.FieldValue.increment(1),
|
||||
}
|
||||
return snap.docs
|
||||
.map((doc) => {
|
||||
const contract = doc.data() as Contract
|
||||
|
||||
if (
|
||||
contract.resolution ||
|
||||
(contract.closeEmailsSent ?? 0) >= 1 ||
|
||||
contract.closeTime === undefined ||
|
||||
(contract.closeTime ?? 0) > Date.now()
|
||||
)
|
||||
return undefined
|
||||
|
||||
transaction.update(doc.ref, {
|
||||
closeEmailsSent: (contract.closeEmailsSent ?? 0) + 1,
|
||||
})
|
||||
|
||||
return contract
|
||||
})
|
||||
)
|
||||
return closeContracts
|
||||
.filter((x) => !!x) as Contract[]
|
||||
})
|
||||
|
||||
for (const contract of contracts) {
|
||||
|
@ -57,40 +56,15 @@ export async function sendMarketCloseEmails() {
|
|||
const privateUser = await getPrivateUser(user.id)
|
||||
if (!privateUser) continue
|
||||
|
||||
await createMarketClosedNotification(
|
||||
contract,
|
||||
await sendMarketCloseEmail(user, privateUser, contract)
|
||||
await createNotification(
|
||||
contract.id,
|
||||
'contract',
|
||||
'closed',
|
||||
user,
|
||||
privateUser,
|
||||
contract.id + '-closed-at-' + contract.closeTime
|
||||
'closed' + contract.id.slice(6, contract.id.length),
|
||||
contract.closeTime?.toString() ?? new Date().toString(),
|
||||
{ contract }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// The downside of this approach is if this function goes down for the entire
|
||||
// day of a multiple of the time period after the market has closed, it won't
|
||||
// keep sending them notifications bc when it comes back online the time period will have passed
|
||||
function shouldSendFirstOrFollowUpCloseNotification(contract: Contract) {
|
||||
if (!contract.closeEmailsSent || contract.closeEmailsSent === 0) return true
|
||||
const { closedMultipleOfNDaysAgo, fullTimePeriodsSinceClose } =
|
||||
marketClosedMultipleOfNDaysAgo(contract)
|
||||
return (
|
||||
contract.closeEmailsSent > 0 &&
|
||||
closedMultipleOfNDaysAgo &&
|
||||
contract.closeEmailsSent === fullTimePeriodsSinceClose
|
||||
)
|
||||
}
|
||||
|
||||
function marketClosedMultipleOfNDaysAgo(contract: Contract) {
|
||||
const now = Date.now()
|
||||
const closeTime = contract.closeTime
|
||||
if (!closeTime)
|
||||
return { closedMultipleOfNDaysAgo: false, fullTimePeriodsSinceClose: 0 }
|
||||
const daysSinceClose = Math.floor((now - closeTime) / DAY_MS)
|
||||
return {
|
||||
closedMultipleOfNDaysAgo:
|
||||
daysSinceClose % SEND_NOTIFICATIONS_EVERY_DAYS == 0,
|
||||
fullTimePeriodsSinceClose: Math.floor(
|
||||
daysSinceClose / SEND_NOTIFICATIONS_EVERY_DAYS
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,19 +3,11 @@ import * as admin from 'firebase-admin'
|
|||
import { keyBy, uniq } from 'lodash'
|
||||
|
||||
import { Bet, LimitBet } from '../../common/bet'
|
||||
import { getUser, getValues, isProd, log } from './utils'
|
||||
import {
|
||||
getContractPath,
|
||||
getUser,
|
||||
getValues,
|
||||
isProd,
|
||||
log,
|
||||
revalidateStaticProps,
|
||||
} from './utils'
|
||||
import {
|
||||
createBadgeAwardedNotification,
|
||||
createBetFillNotification,
|
||||
createBettingStreakBonusNotification,
|
||||
createUniqueBettorBonusNotification,
|
||||
createNotification,
|
||||
} from './create-notification'
|
||||
import { filterDefined } from '../../common/util/array'
|
||||
import { Contract } from '../../common/contract'
|
||||
|
@ -25,7 +17,6 @@ import {
|
|||
BETTING_STREAK_BONUS_MAX,
|
||||
BETTING_STREAK_RESET_HOUR,
|
||||
UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||
UNIQUE_BETTOR_LIQUIDITY,
|
||||
} from '../../common/economy'
|
||||
import {
|
||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
|
@ -33,20 +24,12 @@ import {
|
|||
} from '../../common/antes'
|
||||
import { APIError } from '../../common/api'
|
||||
import { User } from '../../common/user'
|
||||
import { DAY_MS } from '../../common/util/time'
|
||||
import { BettingStreakBonusTxn, UniqueBettorBonusTxn } from '../../common/txn'
|
||||
import { addHouseSubsidy } from './helpers/add-house-subsidy'
|
||||
import {
|
||||
StreakerBadge,
|
||||
streakerBadgeRarityThresholds,
|
||||
} from '../../common/badge'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime()
|
||||
|
||||
export const onCreateBet = functions
|
||||
.runWith({ secrets: ['MAILGUN_KEY', 'API_SECRET'] })
|
||||
.firestore.document('contracts/{contractId}/bets/{betId}')
|
||||
export const onCreateBet = functions.firestore
|
||||
.document('contracts/{contractId}/bets/{betId}')
|
||||
.onCreate(async (change, context) => {
|
||||
const { contractId } = context.params as {
|
||||
contractId: string
|
||||
|
@ -71,21 +54,15 @@ export const onCreateBet = functions
|
|||
log(`Could not find contract ${contractId}`)
|
||||
return
|
||||
}
|
||||
await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bet.userId)
|
||||
|
||||
const bettor = await getUser(bet.userId)
|
||||
if (!bettor) return
|
||||
|
||||
await change.ref.update({
|
||||
userAvatarUrl: bettor.avatarUrl,
|
||||
userName: bettor.name,
|
||||
userUsername: bettor.username,
|
||||
})
|
||||
|
||||
await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bettor)
|
||||
await notifyFills(bet, contract, eventId, bettor)
|
||||
await updateBettingStreak(bettor, bet, contract, eventId)
|
||||
|
||||
await revalidateStaticProps(getContractPath(contract))
|
||||
await firestore.collection('users').doc(bettor.id).update({ lastBetTime })
|
||||
})
|
||||
|
||||
const updateBettingStreak = async (
|
||||
|
@ -94,42 +71,31 @@ const updateBettingStreak = async (
|
|||
contract: Contract,
|
||||
eventId: string
|
||||
) => {
|
||||
const { newBettingStreak } = await firestore.runTransaction(async (trans) => {
|
||||
const userDoc = firestore.collection('users').doc(user.id)
|
||||
const bettor = (await trans.get(userDoc)).data() as User
|
||||
const now = Date.now()
|
||||
const currentDateResetTime = currentDateBettingStreakResetTime()
|
||||
// if now is before reset time, use yesterday's reset time
|
||||
const lastDateResetTime = currentDateResetTime - DAY_MS
|
||||
const betStreakResetTime =
|
||||
now < currentDateResetTime ? lastDateResetTime : currentDateResetTime
|
||||
const lastBetTime = bettor?.lastBetTime ?? 0
|
||||
const betStreakResetTime = getTodaysBettingStreakResetTime()
|
||||
const lastBetTime = user?.lastBetTime ?? 0
|
||||
|
||||
// If they've already bet after the reset time
|
||||
if (lastBetTime > betStreakResetTime) return { newBettingStreak: undefined }
|
||||
// If they've already bet after the reset time, or if we haven't hit the reset time yet
|
||||
if (lastBetTime > betStreakResetTime || bet.createdTime < betStreakResetTime)
|
||||
return
|
||||
|
||||
const newBettingStreak = (bettor?.currentBettingStreak ?? 0) + 1
|
||||
// Otherwise, add 1 to their betting streak
|
||||
trans.update(userDoc, {
|
||||
currentBettingStreak: newBettingStreak,
|
||||
lastBetTime: bet.createdTime,
|
||||
})
|
||||
return { newBettingStreak }
|
||||
const newBettingStreak = (user?.currentBettingStreak ?? 0) + 1
|
||||
// Otherwise, add 1 to their betting streak
|
||||
await firestore.collection('users').doc(user.id).update({
|
||||
currentBettingStreak: newBettingStreak,
|
||||
})
|
||||
if (!newBettingStreak) return
|
||||
const result = await firestore.runTransaction(async (trans) => {
|
||||
// Send them the bonus times their streak
|
||||
const bonusAmount = Math.min(
|
||||
BETTING_STREAK_BONUS_AMOUNT * newBettingStreak,
|
||||
BETTING_STREAK_BONUS_MAX
|
||||
)
|
||||
const fromUserId = isProd()
|
||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
const bonusTxnDetails = {
|
||||
currentBettingStreak: newBettingStreak,
|
||||
}
|
||||
|
||||
// Send them the bonus times their streak
|
||||
const bonusAmount = Math.min(
|
||||
BETTING_STREAK_BONUS_AMOUNT * newBettingStreak,
|
||||
BETTING_STREAK_BONUS_MAX
|
||||
)
|
||||
const fromUserId = isProd()
|
||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
const bonusTxnDetails = {
|
||||
currentBettingStreak: newBettingStreak,
|
||||
}
|
||||
const result = await firestore.runTransaction(async (trans) => {
|
||||
const bonusTxn: TxnData = {
|
||||
fromId: fromUserId,
|
||||
fromType: 'BANK',
|
||||
|
@ -139,133 +105,105 @@ const updateBettingStreak = async (
|
|||
token: 'M$',
|
||||
category: 'BETTING_STREAK_BONUS',
|
||||
description: JSON.stringify(bonusTxnDetails),
|
||||
data: bonusTxnDetails,
|
||||
} as Omit<BettingStreakBonusTxn, 'id' | 'createdTime'>
|
||||
const { message, txn, status } = await runTxn(trans, bonusTxn)
|
||||
return { message, txn, status, bonusAmount }
|
||||
}
|
||||
return await runTxn(trans, bonusTxn)
|
||||
})
|
||||
if (result.status != 'success') {
|
||||
if (!result.txn) {
|
||||
log("betting streak bonus txn couldn't be made")
|
||||
log('status:', result.status)
|
||||
log('message:', result.message)
|
||||
return
|
||||
}
|
||||
if (result.txn) {
|
||||
await createBettingStreakBonusNotification(
|
||||
user,
|
||||
result.txn.id,
|
||||
bet,
|
||||
contract,
|
||||
result.bonusAmount,
|
||||
newBettingStreak,
|
||||
eventId
|
||||
)
|
||||
await handleBettingStreakBadgeAward(user, newBettingStreak)
|
||||
}
|
||||
|
||||
await createBettingStreakBonusNotification(
|
||||
user,
|
||||
result.txn.id,
|
||||
bet,
|
||||
contract,
|
||||
bonusAmount,
|
||||
eventId
|
||||
)
|
||||
}
|
||||
|
||||
const updateUniqueBettorsAndGiveCreatorBonus = async (
|
||||
oldContract: Contract,
|
||||
contract: Contract,
|
||||
eventId: string,
|
||||
bettor: User
|
||||
bettorId: string
|
||||
) => {
|
||||
const { newUniqueBettorIds } = await firestore.runTransaction(
|
||||
async (trans) => {
|
||||
const contractDoc = firestore.collection(`contracts`).doc(oldContract.id)
|
||||
const contract = (await trans.get(contractDoc)).data() as Contract
|
||||
let previousUniqueBettorIds = contract.uniqueBettorIds
|
||||
let previousUniqueBettorIds = contract.uniqueBettorIds
|
||||
|
||||
const betsSnap = await trans.get(
|
||||
firestore.collection(`contracts/${contract.id}/bets`)
|
||||
)
|
||||
if (!previousUniqueBettorIds) {
|
||||
const contractBets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
||||
if (!previousUniqueBettorIds) {
|
||||
const contractBets = (
|
||||
await firestore.collection(`contracts/${contract.id}/bets`).get()
|
||||
).docs.map((doc) => doc.data() as Bet)
|
||||
|
||||
if (contractBets.length === 0) {
|
||||
return { newUniqueBettorIds: undefined }
|
||||
}
|
||||
|
||||
previousUniqueBettorIds = uniq(
|
||||
contractBets
|
||||
.filter((bet) => bet.createdTime < BONUS_START_DATE)
|
||||
.map((bet) => bet.userId)
|
||||
)
|
||||
}
|
||||
|
||||
const isNewUniqueBettor = !previousUniqueBettorIds.includes(bettor.id)
|
||||
const newUniqueBettorIds = uniq([...previousUniqueBettorIds, bettor.id])
|
||||
|
||||
// Update contract unique bettors
|
||||
if (!contract.uniqueBettorIds || isNewUniqueBettor) {
|
||||
log(`Got ${previousUniqueBettorIds} unique bettors`)
|
||||
isNewUniqueBettor && log(`And a new unique bettor ${bettor.id}`)
|
||||
|
||||
trans.update(contractDoc, {
|
||||
uniqueBettorIds: newUniqueBettorIds,
|
||||
uniqueBettorCount: newUniqueBettorIds.length,
|
||||
})
|
||||
}
|
||||
|
||||
// No need to give a bonus for the creator's bet
|
||||
if (!isNewUniqueBettor || bettor.id == contract.creatorId)
|
||||
return { newUniqueBettorIds: undefined }
|
||||
|
||||
return { newUniqueBettorIds }
|
||||
if (contractBets.length === 0) {
|
||||
log(`No bets for contract ${contract.id}`)
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
if (!newUniqueBettorIds) return
|
||||
|
||||
if (oldContract.mechanism === 'cpmm-1') {
|
||||
await addHouseSubsidy(oldContract.id, UNIQUE_BETTOR_LIQUIDITY)
|
||||
previousUniqueBettorIds = uniq(
|
||||
contractBets
|
||||
.filter((bet) => bet.createdTime < BONUS_START_DATE)
|
||||
.map((bet) => bet.userId)
|
||||
)
|
||||
}
|
||||
|
||||
const isNewUniqueBettor = !previousUniqueBettorIds.includes(bettorId)
|
||||
|
||||
const newUniqueBettorIds = uniq([...previousUniqueBettorIds, bettorId])
|
||||
// Update contract unique bettors
|
||||
if (!contract.uniqueBettorIds || isNewUniqueBettor) {
|
||||
log(`Got ${previousUniqueBettorIds} unique bettors`)
|
||||
isNewUniqueBettor && log(`And a new unique bettor ${bettorId}`)
|
||||
await firestore.collection(`contracts`).doc(contract.id).update({
|
||||
uniqueBettorIds: newUniqueBettorIds,
|
||||
uniqueBettorCount: newUniqueBettorIds.length,
|
||||
})
|
||||
}
|
||||
|
||||
// No need to give a bonus for the creator's bet
|
||||
if (!isNewUniqueBettor || bettorId == contract.creatorId) return
|
||||
|
||||
// Create combined txn for all new unique bettors
|
||||
const bonusTxnDetails = {
|
||||
contractId: oldContract.id,
|
||||
uniqueNewBettorId: bettor.id,
|
||||
contractId: contract.id,
|
||||
uniqueBettorIds: newUniqueBettorIds,
|
||||
}
|
||||
const fromUserId = isProd()
|
||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
|
||||
if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
|
||||
|
||||
const fromUser = fromSnap.data() as User
|
||||
|
||||
const result = await firestore.runTransaction(async (trans) => {
|
||||
const bonusTxn: TxnData = {
|
||||
fromId: fromUser.id,
|
||||
fromType: 'BANK',
|
||||
toId: oldContract.creatorId,
|
||||
toId: contract.creatorId,
|
||||
toType: 'USER',
|
||||
amount: UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||
token: 'M$',
|
||||
category: 'UNIQUE_BETTOR_BONUS',
|
||||
description: JSON.stringify(bonusTxnDetails),
|
||||
data: bonusTxnDetails,
|
||||
} as Omit<UniqueBettorBonusTxn, 'id' | 'createdTime'>
|
||||
|
||||
const { status, message, txn } = await runTxn(trans, bonusTxn)
|
||||
|
||||
return { status, newUniqueBettorIds, message, txn }
|
||||
}
|
||||
return await runTxn(trans, bonusTxn)
|
||||
})
|
||||
|
||||
if (result.status != 'success' || !result.txn) {
|
||||
log(`No bonus for user: ${oldContract.creatorId} - status:`, result.status)
|
||||
log('message:', result.message)
|
||||
log(`No bonus for user: ${contract.creatorId} - reason:`, result.status)
|
||||
} else {
|
||||
log(
|
||||
`Bonus txn for user: ${oldContract.creatorId} completed:`,
|
||||
result.txn?.id
|
||||
)
|
||||
await createUniqueBettorBonusNotification(
|
||||
oldContract.creatorId,
|
||||
bettor,
|
||||
log(`Bonus txn for user: ${contract.creatorId} completed:`, result.txn?.id)
|
||||
await createNotification(
|
||||
result.txn.id,
|
||||
oldContract,
|
||||
result.txn.amount,
|
||||
result.newUniqueBettorIds,
|
||||
eventId + '-unique-bettor-bonus'
|
||||
'bonus',
|
||||
'created',
|
||||
fromUser,
|
||||
eventId + '-bonus',
|
||||
result.txn.amount + '',
|
||||
{
|
||||
contract,
|
||||
slug: contract.slug,
|
||||
title: contract.question,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -311,42 +249,6 @@ const notifyFills = async (
|
|||
)
|
||||
}
|
||||
|
||||
const currentDateBettingStreakResetTime = () => {
|
||||
const getTodaysBettingStreakResetTime = () => {
|
||||
return new Date().setUTCHours(BETTING_STREAK_RESET_HOUR, 0, 0, 0)
|
||||
}
|
||||
|
||||
async function handleBettingStreakBadgeAward(
|
||||
user: User,
|
||||
newBettingStreak: number
|
||||
) {
|
||||
const alreadyHasBadgeForFirstStreak =
|
||||
user.achievements?.streaker?.badges.some(
|
||||
(badge) => badge.data.totalBettingStreak === 1
|
||||
)
|
||||
// TODO: check if already awarded 50th streak as well
|
||||
if (newBettingStreak === 1 && alreadyHasBadgeForFirstStreak) return
|
||||
|
||||
if (streakerBadgeRarityThresholds.includes(newBettingStreak)) {
|
||||
const badge = {
|
||||
type: 'STREAKER',
|
||||
name: 'Streaker',
|
||||
data: {
|
||||
totalBettingStreak: newBettingStreak,
|
||||
},
|
||||
createdTime: Date.now(),
|
||||
} as StreakerBadge
|
||||
// update user
|
||||
await firestore
|
||||
.collection('users')
|
||||
.doc(user.id)
|
||||
.update({
|
||||
achievements: {
|
||||
...user.achievements,
|
||||
streaker: {
|
||||
badges: [...(user.achievements?.streaker?.badges ?? []), badge],
|
||||
},
|
||||
},
|
||||
})
|
||||
await createBadgeAwardedNotification(user, badge)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,81 +1,17 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { compact } from 'lodash'
|
||||
import {
|
||||
getContract,
|
||||
getContractPath,
|
||||
getUser,
|
||||
getValues,
|
||||
revalidateStaticProps,
|
||||
} from './utils'
|
||||
import { compact, uniq } from 'lodash'
|
||||
import { getContract, getUser, getValues } from './utils'
|
||||
import { ContractComment } from '../../common/comment'
|
||||
import { sendNewCommentEmail } from './emails'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { getLargestPosition } from '../../common/calculate'
|
||||
import { maxBy } from 'lodash'
|
||||
import {
|
||||
createCommentOrAnswerOrUpdatedContractNotification,
|
||||
replied_users_info,
|
||||
} from './create-notification'
|
||||
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
|
||||
import { parseMentions, richTextToString } from '../../common/util/parse'
|
||||
import { addUserToContractFollowers } from './follow-market'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
function getMostRecentCommentableBet(
|
||||
before: number,
|
||||
betsByCurrentUser: Bet[],
|
||||
commentsByCurrentUser: ContractComment[],
|
||||
answerOutcome?: string
|
||||
) {
|
||||
let sortedBetsByCurrentUser = betsByCurrentUser.sort(
|
||||
(a, b) => b.createdTime - a.createdTime
|
||||
)
|
||||
if (answerOutcome) {
|
||||
sortedBetsByCurrentUser = sortedBetsByCurrentUser.slice(0, 1)
|
||||
}
|
||||
return sortedBetsByCurrentUser
|
||||
.filter((bet) => {
|
||||
const { createdTime, isRedemption } = bet
|
||||
// You can comment on bets posted in the last hour
|
||||
const commentable = !isRedemption && before - createdTime < 60 * 60 * 1000
|
||||
const alreadyCommented = commentsByCurrentUser.some(
|
||||
(comment) => comment.createdTime > bet.createdTime
|
||||
)
|
||||
if (commentable && !alreadyCommented) {
|
||||
if (!answerOutcome) return true
|
||||
return answerOutcome === bet.outcome
|
||||
}
|
||||
return false
|
||||
})
|
||||
.pop()
|
||||
}
|
||||
|
||||
async function getPriorUserComments(
|
||||
contractId: string,
|
||||
userId: string,
|
||||
before: number
|
||||
) {
|
||||
const priorCommentsQuery = await firestore
|
||||
.collection('contracts')
|
||||
.doc(contractId)
|
||||
.collection('comments')
|
||||
.where('createdTime', '<', before)
|
||||
.where('userId', '==', userId)
|
||||
.get()
|
||||
return priorCommentsQuery.docs.map((d) => d.data() as ContractComment)
|
||||
}
|
||||
|
||||
async function getPriorContractBets(contractId: string, before: number) {
|
||||
const priorBetsQuery = await firestore
|
||||
.collection('contracts')
|
||||
.doc(contractId)
|
||||
.collection('bets')
|
||||
.where('createdTime', '<', before)
|
||||
.get()
|
||||
return priorBetsQuery.docs.map((d) => d.data() as Bet)
|
||||
}
|
||||
|
||||
export const onCreateCommentOnContract = functions
|
||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||
.firestore.document('contracts/{contractId}/comments/{commentId}')
|
||||
|
@ -94,8 +30,6 @@ export const onCreateCommentOnContract = functions
|
|||
contractQuestion: contract.question,
|
||||
})
|
||||
|
||||
await revalidateStaticProps(getContractPath(contract))
|
||||
|
||||
const comment = change.data() as ContractComment
|
||||
const lastCommentTime = comment.createdTime
|
||||
|
||||
|
@ -109,48 +43,7 @@ export const onCreateCommentOnContract = functions
|
|||
.doc(contract.id)
|
||||
.update({ lastCommentTime, lastUpdatedTime: Date.now() })
|
||||
|
||||
const priorBets = await getPriorContractBets(
|
||||
contractId,
|
||||
comment.createdTime
|
||||
)
|
||||
const priorUserBets = priorBets.filter(
|
||||
(b) => b.userId === comment.userId && !b.isAnte
|
||||
)
|
||||
const priorUserComments = await getPriorUserComments(
|
||||
contractId,
|
||||
comment.userId,
|
||||
comment.createdTime
|
||||
)
|
||||
const bet = getMostRecentCommentableBet(
|
||||
comment.createdTime,
|
||||
priorUserBets,
|
||||
priorUserComments,
|
||||
comment.answerOutcome
|
||||
)
|
||||
if (bet) {
|
||||
await change.ref.update({
|
||||
betId: bet.id,
|
||||
betOutcome: bet.outcome,
|
||||
betAmount: bet.amount,
|
||||
})
|
||||
}
|
||||
|
||||
const position = getLargestPosition(contract, priorUserBets)
|
||||
if (position) {
|
||||
const fields: { [k: string]: unknown } = {
|
||||
commenterPositionShares: position.shares,
|
||||
commenterPositionOutcome: position.outcome,
|
||||
}
|
||||
const previousProb =
|
||||
contract.outcomeType === 'BINARY'
|
||||
? maxBy(priorBets, (bet) => bet.createdTime)?.probAfter
|
||||
: undefined
|
||||
if (previousProb != null) {
|
||||
fields.commenterPositionProb = previousProb
|
||||
}
|
||||
await change.ref.update(fields)
|
||||
}
|
||||
|
||||
let bet: Bet | undefined
|
||||
let answer: Answer | undefined
|
||||
if (comment.answerOutcome) {
|
||||
answer =
|
||||
|
@ -159,51 +52,34 @@ export const onCreateCommentOnContract = functions
|
|||
(answer) => answer.id === comment.answerOutcome
|
||||
)
|
||||
: undefined
|
||||
} else if (comment.betId) {
|
||||
const betSnapshot = await firestore
|
||||
.collection('contracts')
|
||||
.doc(contractId)
|
||||
.collection('bets')
|
||||
.doc(comment.betId)
|
||||
.get()
|
||||
bet = betSnapshot.data() as Bet
|
||||
|
||||
answer =
|
||||
contract.outcomeType === 'FREE_RESPONSE' && contract.answers
|
||||
? contract.answers.find((answer) => answer.id === bet?.outcome)
|
||||
: undefined
|
||||
}
|
||||
|
||||
const comments = await getValues<ContractComment>(
|
||||
firestore.collection('contracts').doc(contractId).collection('comments')
|
||||
)
|
||||
const repliedToType = answer
|
||||
? 'answer'
|
||||
: comment.replyToCommentId
|
||||
const relatedSourceType = comment.replyToCommentId
|
||||
? 'comment'
|
||||
: comment.answerOutcome
|
||||
? 'answer'
|
||||
: undefined
|
||||
|
||||
const repliedUserId = comment.replyToCommentId
|
||||
? comments.find((c) => c.id === comment.replyToCommentId)?.userId
|
||||
: answer?.userId
|
||||
|
||||
const mentionedUsers = compact(parseMentions(comment.content))
|
||||
const repliedUsers: replied_users_info = {}
|
||||
|
||||
// The parent of the reply chain could be a comment or an answer
|
||||
if (repliedUserId && repliedToType)
|
||||
repliedUsers[repliedUserId] = {
|
||||
repliedToType,
|
||||
repliedToAnswerText: answer ? answer.text : undefined,
|
||||
repliedToId: comment.replyToCommentId || answer?.id,
|
||||
bet: bet,
|
||||
}
|
||||
|
||||
const commentsInSameReplyChain = comments.filter((c) =>
|
||||
repliedToType === 'answer'
|
||||
? c.answerOutcome === answer?.id
|
||||
: repliedToType === 'comment'
|
||||
? c.replyToCommentId === comment.replyToCommentId
|
||||
: false
|
||||
)
|
||||
// The rest of the children in the chain are always comments
|
||||
commentsInSameReplyChain.forEach((c) => {
|
||||
if (c.userId !== comment.userId && c.userId !== repliedUserId) {
|
||||
repliedUsers[c.userId] = {
|
||||
repliedToType: 'comment',
|
||||
repliedToAnswerText: undefined,
|
||||
repliedToId: c.id,
|
||||
bet: undefined,
|
||||
}
|
||||
}
|
||||
})
|
||||
await createCommentOrAnswerOrUpdatedContractNotification(
|
||||
comment.id,
|
||||
'comment',
|
||||
|
@ -213,8 +89,28 @@ export const onCreateCommentOnContract = functions
|
|||
richTextToString(comment.content),
|
||||
contract,
|
||||
{
|
||||
repliedUsersInfo: repliedUsers,
|
||||
taggedUserIds: mentionedUsers,
|
||||
relatedSourceType,
|
||||
repliedUserId,
|
||||
taggedUserIds: compact(parseMentions(comment.content)),
|
||||
}
|
||||
)
|
||||
|
||||
const recipientUserIds = uniq([
|
||||
contract.creatorId,
|
||||
...comments.map((comment) => comment.userId),
|
||||
]).filter((id) => id !== comment.userId)
|
||||
|
||||
await Promise.all(
|
||||
recipientUserIds.map((userId) =>
|
||||
sendNewCommentEmail(
|
||||
userId,
|
||||
commentCreator,
|
||||
contract,
|
||||
comment,
|
||||
bet,
|
||||
answer?.text,
|
||||
answer?.id
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
|
46
functions/src/on-create-comment-on-group.ts
Normal file
46
functions/src/on-create-comment-on-group.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import { GroupComment } from '../../common/comment'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { Group } from '../../common/group'
|
||||
import { User } from '../../common/user'
|
||||
import { createGroupCommentNotification } from './create-notification'
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const onCreateCommentOnGroup = functions.firestore
|
||||
.document('groups/{groupId}/comments/{commentId}')
|
||||
.onCreate(async (change, context) => {
|
||||
const { eventId } = context
|
||||
const { groupId } = context.params as {
|
||||
groupId: string
|
||||
}
|
||||
|
||||
const comment = change.data() as GroupComment
|
||||
const creatorSnapshot = await firestore
|
||||
.collection('users')
|
||||
.doc(comment.userId)
|
||||
.get()
|
||||
if (!creatorSnapshot.exists) throw new Error('Could not find user')
|
||||
|
||||
const groupSnapshot = await firestore
|
||||
.collection('groups')
|
||||
.doc(groupId)
|
||||
.get()
|
||||
if (!groupSnapshot.exists) throw new Error('Could not find group')
|
||||
|
||||
const group = groupSnapshot.data() as Group
|
||||
await firestore.collection('groups').doc(groupId).update({
|
||||
mostRecentChatActivityTime: comment.createdTime,
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
group.memberIds.map(async (memberId) => {
|
||||
return await createGroupCommentNotification(
|
||||
creatorSnapshot.data() as User,
|
||||
memberId,
|
||||
comment,
|
||||
group,
|
||||
eventId
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
|
@ -1,20 +1,11 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
|
||||
import { getUser, getValues } from './utils'
|
||||
import {
|
||||
createBadgeAwardedNotification,
|
||||
createNewContractNotification,
|
||||
} from './create-notification'
|
||||
import { getUser } from './utils'
|
||||
import { createNotification } from './create-notification'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { parseMentions, richTextToString } from '../../common/util/parse'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
import { addUserToContractFollowers } from './follow-market'
|
||||
import { User } from '../../common/user'
|
||||
import * as admin from 'firebase-admin'
|
||||
import {
|
||||
MarketCreatorBadge,
|
||||
marketCreatorBadgeRarityThresholds,
|
||||
} from '../../common/badge'
|
||||
|
||||
export const onCreateContract = functions
|
||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||
|
@ -30,50 +21,13 @@ export const onCreateContract = functions
|
|||
const mentioned = parseMentions(desc)
|
||||
await addUserToContractFollowers(contract.id, contractCreator.id)
|
||||
|
||||
await createNewContractNotification(
|
||||
await createNotification(
|
||||
contract.id,
|
||||
'contract',
|
||||
'created',
|
||||
contractCreator,
|
||||
contract,
|
||||
eventId,
|
||||
richTextToString(desc),
|
||||
mentioned
|
||||
{ contract, recipients: mentioned }
|
||||
)
|
||||
await handleMarketCreatorBadgeAward(contractCreator)
|
||||
})
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function handleMarketCreatorBadgeAward(contractCreator: User) {
|
||||
// get all contracts by user and calculate size of array
|
||||
const contracts = await getValues<Contract>(
|
||||
firestore
|
||||
.collection(`contracts`)
|
||||
.where('creatorId', '==', contractCreator.id)
|
||||
.where('resolution', '!=', 'CANCEL')
|
||||
)
|
||||
if (marketCreatorBadgeRarityThresholds.includes(contracts.length)) {
|
||||
const badge = {
|
||||
type: 'MARKET_CREATOR',
|
||||
name: 'Market Creator',
|
||||
data: {
|
||||
totalContractsCreated: contracts.length,
|
||||
},
|
||||
createdTime: Date.now(),
|
||||
} as MarketCreatorBadge
|
||||
// update user
|
||||
await firestore
|
||||
.collection('users')
|
||||
.doc(contractCreator.id)
|
||||
.update({
|
||||
achievements: {
|
||||
...contractCreator.achievements,
|
||||
marketCreator: {
|
||||
badges: [
|
||||
...(contractCreator.achievements?.marketCreator?.badges ?? []),
|
||||
badge,
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
await createBadgeAwardedNotification(contractCreator, badge)
|
||||
}
|
||||
}
|
||||
|
|
28
functions/src/on-create-group.ts
Normal file
28
functions/src/on-create-group.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import { getUser } from './utils'
|
||||
import { createNotification } from './create-notification'
|
||||
import { Group } from '../../common/group'
|
||||
|
||||
export const onCreateGroup = functions.firestore
|
||||
.document('groups/{groupId}')
|
||||
.onCreate(async (change, context) => {
|
||||
const group = change.data() as Group
|
||||
const { eventId } = context
|
||||
|
||||
const groupCreator = await getUser(group.creatorId)
|
||||
if (!groupCreator) throw new Error('Could not find group creator')
|
||||
// create notifications for all members of the group
|
||||
await createNotification(
|
||||
group.id,
|
||||
'group',
|
||||
'created',
|
||||
groupCreator,
|
||||
eventId,
|
||||
group.about,
|
||||
{
|
||||
recipients: group.memberIds,
|
||||
slug: group.slug,
|
||||
title: group.name,
|
||||
}
|
||||
)
|
||||
})
|
|
@ -1,13 +1,12 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import { getContract, getUser, log } from './utils'
|
||||
import { createFollowOrMarketSubsidizedNotification } from './create-notification'
|
||||
import { createNotification } from './create-notification'
|
||||
import { LiquidityProvision } from '../../common/liquidity-provision'
|
||||
import { addUserToContractFollowers } from './follow-market'
|
||||
import { FIXED_ANTE } from '../../common/economy'
|
||||
import {
|
||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
UNIQUE_BETTOR_LIQUIDITY_AMOUNT,
|
||||
} from '../../common/antes'
|
||||
|
||||
export const onCreateLiquidityProvision = functions.firestore
|
||||
|
@ -18,11 +17,9 @@ export const onCreateLiquidityProvision = functions.firestore
|
|||
|
||||
// Ignore Manifold Markets liquidity for now - users see a notification for free market liquidity provision
|
||||
if (
|
||||
liquidity.isAnte ||
|
||||
((liquidity.userId === HOUSE_LIQUIDITY_PROVIDER_ID ||
|
||||
(liquidity.userId === HOUSE_LIQUIDITY_PROVIDER_ID ||
|
||||
liquidity.userId === DEV_HOUSE_LIQUIDITY_PROVIDER_ID) &&
|
||||
(liquidity.amount === FIXED_ANTE ||
|
||||
liquidity.amount === UNIQUE_BETTOR_LIQUIDITY_AMOUNT))
|
||||
liquidity.amount === FIXED_ANTE
|
||||
)
|
||||
return
|
||||
|
||||
|
@ -36,7 +33,7 @@ export const onCreateLiquidityProvision = functions.firestore
|
|||
if (!liquidityProvider) throw new Error('Could not find liquidity provider')
|
||||
await addUserToContractFollowers(contract.id, liquidityProvider.id)
|
||||
|
||||
await createFollowOrMarketSubsidizedNotification(
|
||||
await createNotification(
|
||||
contract.id,
|
||||
'liquidity',
|
||||
'created',
|
||||
|
|
|
@ -23,12 +23,12 @@ export const onCreateUser = functions
|
|||
|
||||
await sendWelcomeEmail(user, privateUser)
|
||||
|
||||
const guideSendTime = dayjs().add(28, 'hours').toString()
|
||||
await sendCreatorGuideEmail(user, privateUser, guideSendTime)
|
||||
|
||||
const followupSendTime = dayjs().add(48, 'hours').toString()
|
||||
await sendPersonalFollowupEmail(user, privateUser, followupSendTime)
|
||||
|
||||
const guideSendTime = dayjs().add(96, 'hours').toString()
|
||||
await sendCreatorGuideEmail(user, privateUser, guideSendTime)
|
||||
|
||||
// skip email if weekly email is about to go out
|
||||
const day = dayjs().utc().day()
|
||||
if (day === 0 || (day === 1 && dayjs().utc().hour() <= 19)) return
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user