From 2439317408ac2f17473c02ccac0931502a39cf79 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Sat, 20 Aug 2022 13:32:12 -0700 Subject: [PATCH 1/9] Convert tags to groups on demand (#784) * Fix stuff to not prematurely initialize Firebase when imported * Add script to convert a tag to a group with the same contracts --- functions/src/api.ts | 11 ++-- functions/src/create-group.ts | 6 +- functions/src/scripts/convert-tag-to-group.ts | 66 +++++++++++++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 functions/src/scripts/convert-tag-to-group.ts diff --git a/functions/src/api.ts b/functions/src/api.ts index e9a488c2..7440f16a 100644 --- a/functions/src/api.ts +++ b/functions/src/api.ts @@ -23,13 +23,8 @@ type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken } type KeyCredentials = { kind: 'key'; data: string } type Credentials = JwtCredentials | KeyCredentials -const auth = admin.auth() -const firestore = admin.firestore() -const privateUsers = firestore.collection( - 'private-users' -) as admin.firestore.CollectionReference - export const parseCredentials = async (req: Request): Promise => { + const auth = admin.auth() const authHeader = req.get('Authorization') if (!authHeader) { throw new APIError(403, 'Missing Authorization header.') @@ -57,6 +52,8 @@ export const parseCredentials = async (req: Request): Promise => { } export const lookupUser = async (creds: Credentials): Promise => { + const firestore = admin.firestore() + const privateUsers = firestore.collection('private-users') switch (creds.kind) { case 'jwt': { if (typeof creds.data.user_id !== 'string') { @@ -70,7 +67,7 @@ export const lookupUser = async (creds: Credentials): Promise => { if (privateUserQ.empty) { throw new APIError(403, `No private user exists with API key ${key}.`) } - const privateUser = privateUserQ.docs[0].data() + const privateUser = privateUserQ.docs[0].data() as PrivateUser return { uid: privateUser.id, creds: { privateUser, ...creds } } } default: diff --git a/functions/src/create-group.ts b/functions/src/create-group.ts index a9626916..71c6bd64 100644 --- a/functions/src/create-group.ts +++ b/functions/src/create-group.ts @@ -21,6 +21,7 @@ const bodySchema = z.object({ }) export const creategroup = newEndpoint({}, async (req, auth) => { + const firestore = admin.firestore() const { name, about, memberIds, anyoneCanJoin } = validate( bodySchema, req.body @@ -67,7 +68,7 @@ export const creategroup = newEndpoint({}, async (req, auth) => { return { status: 'success', group: group } }) -const getSlug = async (name: string) => { +export const getSlug = async (name: string) => { const proposedSlug = slugify(name) const preexistingGroup = await getGroupFromSlug(proposedSlug) @@ -75,9 +76,8 @@ const getSlug = async (name: string) => { return preexistingGroup ? proposedSlug + '-' + randomString() : proposedSlug } -const firestore = admin.firestore() - export async function getGroupFromSlug(slug: string) { + const firestore = admin.firestore() const snap = await firestore .collection('groups') .where('slug', '==', slug) diff --git a/functions/src/scripts/convert-tag-to-group.ts b/functions/src/scripts/convert-tag-to-group.ts new file mode 100644 index 00000000..48f14e27 --- /dev/null +++ b/functions/src/scripts/convert-tag-to-group.ts @@ -0,0 +1,66 @@ +// Takes a tag and makes a new group with all the contracts in it. + +import * as admin from 'firebase-admin' +import { initAdmin } from './script-init' +import { isProd, log } from '../utils' +import { getSlug } from '../create-group' +import { Group } from '../../../common/group' + +const getTaggedContractIds = async (tag: string) => { + const firestore = admin.firestore() + const results = await firestore + .collection('contracts') + .where('lowercaseTags', 'array-contains', tag.toLowerCase()) + .get() + return results.docs.map((d) => d.id) +} + +const createGroup = async ( + name: string, + about: string, + contractIds: string[] +) => { + const firestore = admin.firestore() + const creatorId = isProd() + ? 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' + : '94YYTk1AFWfbWMpfYcvnnwI1veP2' + + const slug = await getSlug(name) + const groupRef = firestore.collection('groups').doc() + const now = Date.now() + const group: Group = { + id: groupRef.id, + creatorId, + slug, + name, + about, + createdTime: now, + mostRecentActivityTime: now, + contractIds: contractIds, + anyoneCanJoin: true, + memberIds: [], + } + return await groupRef.create(group) +} + +const convertTagToGroup = async (tag: string, groupName: string) => { + log(`Looking up contract IDs with tag ${tag}...`) + const contractIds = await getTaggedContractIds(tag) + log(`${contractIds.length} contracts found.`) + if (contractIds.length > 0) { + log(`Creating group ${groupName}...`) + const about = `Contracts that used to be tagged ${tag}.` + const result = await createGroup(groupName, about, contractIds) + log(`Done. Group: `, result) + } +} + +if (require.main === module) { + initAdmin() + const args = process.argv.slice(2) + if (args.length != 2) { + console.log('Usage: convert-tag-to-group [tag] [group-name]') + } else { + convertTagToGroup(args[0], args[1]).catch((e) => console.error(e)) + } +} From 43bbc9ec24d025eb8782c01acf0bafa2222c19f9 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 20 Aug 2022 14:04:55 -0500 Subject: [PATCH 2/9] send followup email on D2 --- functions/src/on-create-user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/on-create-user.ts b/functions/src/on-create-user.ts index fd951ab4..dfb6edaa 100644 --- a/functions/src/on-create-user.ts +++ b/functions/src/on-create-user.ts @@ -22,7 +22,7 @@ export const onCreateUser = functions await sendWelcomeEmail(user, privateUser) - const followupSendTime = dayjs().add(4, 'hours').toString() + const followupSendTime = dayjs().add(48, 'hours').toString() await sendPersonalFollowupEmail(user, privateUser, followupSendTime) // skip email if weekly email is about to go out From ef127ea33518838fd1910d6296b108efa9b8c0eb Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 20 Aug 2022 14:20:42 -0500 Subject: [PATCH 3/9] update welcome email --- functions/src/email-templates/welcome.html | 35 ++++++---------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/functions/src/email-templates/welcome.html b/functions/src/email-templates/welcome.html index 0ffafbd5..366709e3 100644 --- a/functions/src/email-templates/welcome.html +++ b/functions/src/email-templates/welcome.html @@ -107,19 +107,12 @@ width="100%"> - - - - - - - -
+ + + banner logo + @@ -175,9 +168,9 @@ - @@ -225,22 +218,12 @@ style="color:#55575d;font-family:Arial;font-size:18px;">Join our Discord chat - -

 

-

Cheers, -

-

David - from Manifold

-

 

+ + style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;"> Explore markets
+ style="font-size:0px;padding:15px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">

Date: Sat, 20 Aug 2022 15:34:22 -0500 Subject: [PATCH 4/9] send creator guide on D1 --- .../src/email-templates/creating-market.html | 452 +++++++++--------- functions/src/emails.ts | 8 +- functions/src/on-create-contract.ts | 24 +- functions/src/on-create-user.ts | 4 + 4 files changed, 230 insertions(+), 258 deletions(-) diff --git a/functions/src/email-templates/creating-market.html b/functions/src/email-templates/creating-market.html index 674a30ed..a61e8d65 100644 --- a/functions/src/email-templates/creating-market.html +++ b/functions/src/email-templates/creating-market.html @@ -103,94 +103,28 @@ -

- -
+
- - - - - - -
- -
- - - - - - -
- - - - - - -
- -
-
-
- -
-
- -
- - - - + + + + + + + +
+
+ + banner logo + +
- -
+
- - - - - - - + + + + + + + +
-
-

- Hi {{name}},

-
-
+
+
+

+ Hi {{name}},

+
+
-
-

- Congrats on creating your first market on Manifold! -

+ ">Did you know you create your own prediction market on Manifold for + any question you care about? +

-

- + The following is a short guide to creating markets. -

-

-   -

-

- Whether it's current events like Musk buying + Twitter or 2024 + elections or personal matters + like book + recommendations or losing + weight, + Manifold can help you find the answer. +

+

+ The following is a + short guide to creating markets. +

+ + + + + +
+ + Create a market + +
+ +

+   +

+

+ What makes a good market? -

-
    -
  • - Interesting - topic. Manifold gives - creators M$10 for - each unique trader that bets on your - market, so it pays to ask a question people are interested in! -
  • +

    +
      +
    • + Interesting + topic. Manifold gives + creators M$10 for + each unique trader that bets on your + market, so it pays to ask a question people are interested in! +
    • -
    • - + Clear resolution criteria. Any ambiguities or edge cases in your description - will drive traders away from your markets. -
    • + will drive traders away from your markets. + -
    • - + Detailed description. Include images/videos/tweets and any context or - background - information that could be useful to people who - are interested in learning more that are - uneducated on the subject. -
    • -
    • - + Add it to a group. Groups are the - primary way users filter for relevant markets. - Also, consider making your own groups and - inviting friends/interested communities to - them from other sites! -
    • -
    • - Part of a group. Groups are the + primary way users filter for relevant markets. + Also, consider making your own groups and + inviting friends/interested communities to + them from other sites! +
    • +
    • + Share it on social media. You'll earn the Sharing it on social media. You'll earn the M$500 - referral bonus if you get new users to sign up! -
    • -
    -

    -   -

    -

    - Examples of markets you should - emulate!  -

    - -

    -   -

    -

    - +   +

    + +

    + Why not - - - - create another marketcreate a market - while it is still fresh on your mind? -

    -

    - +   +

    +

    + Thanks for reading! -

    -

    - David from Manifold -

    -
+

+ +
+
+ +
+
+ +
+ +
+ + + +
+ +
+ + + + diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 6768e8ea..f90366fa 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,4 +1,3 @@ - import { DOMAIN } from '../../common/envs/constants' import { Answer } from '../../common/answer' import { Bet } from '../../common/bet' @@ -192,7 +191,6 @@ Cofounder of Manifold Markets https://manifold.markets ` - await sendTextEmail( privateUser.email, 'How are you finding Manifold?', @@ -238,7 +236,8 @@ export const sendOneWeekBonusEmail = async ( export const sendCreatorGuideEmail = async ( user: User, - privateUser: PrivateUser + privateUser: PrivateUser, + sendTime: string ) => { if ( !privateUser || @@ -255,7 +254,7 @@ export const sendCreatorGuideEmail = async ( return await sendTemplateEmail( privateUser.email, - 'Market creation guide', + 'Create your own prediction market', 'creating-market', { name: firstName, @@ -263,6 +262,7 @@ export const sendCreatorGuideEmail = async ( }, { from: 'David from Manifold ', + 'o:deliverytime': sendTime, } ) } diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index 73076b7f..3785ecc9 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -1,13 +1,10 @@ import * as functions from 'firebase-functions' -import * as admin from 'firebase-admin' -import { getPrivateUser, getUser } from './utils' +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 { User } from 'common/user' -import { sendCreatorGuideEmail } from './emails' export const onCreateContract = functions .runWith({ secrets: ['MAILGUN_KEY'] }) @@ -31,23 +28,4 @@ export const onCreateContract = functions richTextToString(desc), { contract, recipients: mentioned } ) - - await sendGuideEmail(contractCreator) }) - -const firestore = admin.firestore() - -const sendGuideEmail = async (contractCreator: User) => { - const query = await firestore - .collection(`contracts`) - .where('creatorId', '==', contractCreator.id) - .limit(2) - .get() - - if (query.size >= 2) return - - const privateUser = await getPrivateUser(contractCreator.id) - if (!privateUser) return - - await sendCreatorGuideEmail(contractCreator, privateUser) -} diff --git a/functions/src/on-create-user.ts b/functions/src/on-create-user.ts index dfb6edaa..844f75fc 100644 --- a/functions/src/on-create-user.ts +++ b/functions/src/on-create-user.ts @@ -6,6 +6,7 @@ dayjs.extend(utc) import { getPrivateUser } from './utils' import { User } from 'common/user' import { + sendCreatorGuideEmail, sendInterestingMarketsEmail, sendPersonalFollowupEmail, sendWelcomeEmail, @@ -22,6 +23,9 @@ 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) From 97b38c156f7b4125f297b4402fb3709d2ffdbb48 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Sat, 20 Aug 2022 15:34:52 -0500 Subject: [PATCH 5/9] Revert "create contract: ante no longer user liquidity provision" This reverts commit 56e9b5fa2f6850b83a39915b81bb5f68b950f65c. --- functions/src/create-market.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/functions/src/create-market.ts b/functions/src/create-market.ts index eb3a19eb..5b0d1daf 100644 --- a/functions/src/create-market.ts +++ b/functions/src/create-market.ts @@ -15,17 +15,15 @@ import { import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random' -import { chargeUser, getContract, isProd } from './utils' +import { chargeUser, getContract } from './utils' import { APIError, newEndpoint, validate, zTimestamp } from './api' import { - DEV_HOUSE_LIQUIDITY_PROVIDER_ID, FIXED_ANTE, getCpmmInitialLiquidity, getFreeAnswerAnte, getMultipleChoiceAntes, getNumericAnte, - HOUSE_LIQUIDITY_PROVIDER_ID, } from '../../common/antes' import { Answer, getNoneAnswer } from '../../common/answer' import { getNewContract } from '../../common/new-contract' @@ -223,9 +221,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => { } } - const providerId = isProd() - ? HOUSE_LIQUIDITY_PROVIDER_ID - : DEV_HOUSE_LIQUIDITY_PROVIDER_ID + const providerId = user.id if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { const liquidityDoc = firestore From 645cfc65f4e425864f987ff9778e96c57b54f76b Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 21 Aug 2022 12:57:00 -0500 Subject: [PATCH 6/9] Update sort order of limit orders (older bets first b/c they are filled first) --- web/components/limit-bets.tsx | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 8c9f4e6b..a3cd7973 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -22,20 +22,20 @@ export function LimitBets(props: { className?: string }) { const { contract, bets, className } = props - const sortedBets = sortBy( - bets, - (bet) => -1 * bet.limitProb, - (bet) => -1 * bet.createdTime - ) const user = useUser() - const yourBets = sortedBets.filter((bet) => bet.userId === user?.id) + + const yourBets = sortBy( + bets.filter((bet) => bet.userId === user?.id), + (bet) => -1 * bet.limitProb, + (bet) => bet.createdTime + ) return ( {yourBets.length === 0 && ( )} @@ -49,7 +49,7 @@ export function LimitBets(props: { @@ -163,8 +163,16 @@ export function OrderBookButton(props: { const { limitBets, contract, className } = props const [open, setOpen] = useState(false) - const yesBets = limitBets.filter((bet) => bet.outcome === 'YES') - const noBets = limitBets.filter((bet) => bet.outcome === 'NO').reverse() + const yesBets = sortBy( + limitBets.filter((bet) => bet.outcome === 'YES'), + (bet) => -1 * bet.limitProb, + (bet) => bet.createdTime + ) + const noBets = sortBy( + limitBets.filter((bet) => bet.outcome === 'NO'), + (bet) => bet.limitProb, + (bet) => bet.createdTime + ) return ( <> From d18dd5b8fbbb4cbefd508141557e59650dfb72b1 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 21 Aug 2022 15:58:49 -0500 Subject: [PATCH 7/9] Fix a case of limit order sorting --- web/components/limit-bets.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index a3cd7973..466b7a9b 100644 --- a/web/components/limit-bets.tsx +++ b/web/components/limit-bets.tsx @@ -163,13 +163,15 @@ export function OrderBookButton(props: { const { limitBets, contract, className } = props const [open, setOpen] = useState(false) - const yesBets = sortBy( - limitBets.filter((bet) => bet.outcome === 'YES'), + const sortedBets = sortBy( + limitBets, (bet) => -1 * bet.limitProb, (bet) => bet.createdTime ) + + const yesBets = sortedBets.filter((bet) => bet.outcome === 'YES') const noBets = sortBy( - limitBets.filter((bet) => bet.outcome === 'NO'), + sortedBets.filter((bet) => bet.outcome === 'NO'), (bet) => bet.limitProb, (bet) => bet.createdTime ) @@ -202,7 +204,7 @@ export function OrderBookButton(props: { From aa3647e0f316aada5ccd74f2e39884e8d70922ca Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Sun, 21 Aug 2022 17:10:58 -0700 Subject: [PATCH 8/9] Handle case when no charity txns --- web/pages/charity/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/pages/charity/index.tsx b/web/pages/charity/index.tsx index e9014bfb..0bc6f0f8 100644 --- a/web/pages/charity/index.tsx +++ b/web/pages/charity/index.tsx @@ -39,8 +39,8 @@ export async function getStaticProps() { ]) const matches = quadraticMatches(txns, totalRaised) const numDonors = uniqBy(txns, (txn) => txn.fromId).length - const mostRecentDonor = await getUser(txns[0].fromId) - const mostRecentCharity = txns[0].toId + const mostRecentDonor = txns[0] ? await getUser(txns[0].fromId) : null + const mostRecentCharity = txns[0]?.toId ?? '' return { props: { @@ -94,8 +94,8 @@ export default function Charity(props: { matches: { [charityId: string]: number } txns: Txn[] numDonors: number - mostRecentDonor: User - mostRecentCharity: string + mostRecentDonor?: User | null + mostRecentCharity?: string }) { const { totalRaised, @@ -159,8 +159,8 @@ export default function Charity(props: { }, { name: 'Most recent donor', - stat: mostRecentDonor.name ?? 'Nobody', - url: `/${mostRecentDonor.username}`, + stat: mostRecentDonor?.name ?? 'Nobody', + url: `/${mostRecentDonor?.username}`, }, { name: 'Most recent donation', From 258b2a318fa626df536ca31ce178ec6ea6af4919 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Sun, 21 Aug 2022 21:02:56 -0700 Subject: [PATCH 9/9] Default to showing weekly bet graph --- web/components/portfolio/portfolio-value-section.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index 13880bd4..604873e9 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -14,7 +14,7 @@ export const PortfolioValueSection = memo( }) { const { disableSelector, userId } = props - const [portfolioPeriod, setPortfolioPeriod] = useState('allTime') + const [portfolioPeriod, setPortfolioPeriod] = useState('weekly') const [portfolioHistory, setUsersPortfolioHistory] = useState< PortfolioMetrics[] >([]) @@ -53,13 +53,15 @@ export const PortfolioValueSection = memo( {!disableSelector && ( )}
+ + + + + + +
+ +