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/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 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/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%"> - @@ -175,9 +168,9 @@
+ + + + + + +
+ +
- - - - - - -
+
+ + banner logo +
- @@ -225,22 +218,12 @@ style="color:#55575d;font-family:Arial;font-size:18px;">Join our Discord chat - -

 

-

Cheers, -

-

David - from Manifold

-

 

{yourBets.length === 0 && ( )} @@ -49,7 +49,7 @@ export function LimitBets(props: { @@ -163,8 +163,18 @@ 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 sortedBets = sortBy( + limitBets, + (bet) => -1 * bet.limitProb, + (bet) => bet.createdTime + ) + + const yesBets = sortedBets.filter((bet) => bet.outcome === 'YES') + const noBets = sortBy( + sortedBets.filter((bet) => bet.outcome === 'NO'), + (bet) => bet.limitProb, + (bet) => bet.createdTime + ) return ( <> @@ -194,7 +204,7 @@ export function OrderBookButton(props: { 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 && ( )} 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',
+ + 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;">

{ 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 fd951ab4..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,7 +23,10 @@ export const onCreateUser = functions await sendWelcomeEmail(user, privateUser) - const followupSendTime = dayjs().add(4, 'hours').toString() + const guideSendTime = dayjs().add(28, 'hours').toString() + await sendCreatorGuideEmail(user, privateUser, guideSendTime) + + const followupSendTime = dayjs().add(48, 'hours').toString() await sendPersonalFollowupEmail(user, privateUser, followupSendTime) // skip email if weekly email is about to go out 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)) + } +} diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx index 8c9f4e6b..466b7a9b 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 (