Compare commits

...

1573 Commits
atlas2 ... main

Author SHA1 Message Date
Phil
c762869b83
Migrate prod bot (#1052)
* Updated dev config to point to new Twitch dev bot hosting location.

* Updated prod config to point to new Twitch bot hosting location.
2022-10-14 16:41:06 +01:00
FRC
aaa09f49c0
New component to add arbitrary (but static) react embeds to posts programatically + midterms component (#1051) 2022-10-14 16:21:54 +01:00
FRC
4d214c01b4
Tipping in posts (#1045)
* Tipping in posts

* Rm itemType field
2022-10-14 13:05:07 +01:00
ingawei
0a70652667
fixed tip button bug (#1049)
* fixed tip button bug
2022-10-14 02:20:32 -05:00
mqp
b4162a0896 Auto-prettification 2022-10-14 06:49:23 +00:00
ingawei
2ecece02c3 Auto-remove unused imports 2022-10-14 06:48:40 +00:00
ingawei
4359ad0530
added cancelling tipping and lil coin (#1047)
* added cancelling tipping and lil coin
* forced timeout to fix weird toast bug
2022-10-14 01:47:51 -05:00
ingawei
3f8988bf27
Inga/colorful answer comments (#1040)
* changing answers in comments to match colors, no longer indenting those responses
2022-10-14 00:07:54 -05:00
Austin Chen
41a46aad9b Update stability-client to fix /dream 2022-10-13 21:44:22 -07:00
James Grugett
4097082c75
Dynamically choose outcome in Position tooltip (#1048) 2022-10-13 23:29:57 -05:00
mantikoros
58cd0e57bd track tournaments 2022-10-13 21:22:32 -05:00
Austin Chen
3a63503161 Fix @/% suggestion regression
See https://github.com/ueberdosis/tiptap/pull/3239
2022-10-13 19:06:03 -07:00
mantikoros
abd06f272b contract card: show traders instead of volume 2022-10-13 21:05:21 -05:00
Austin Chen
ab1c3020da Warn users about Dream breakage 2022-10-13 18:45:09 -07:00
Sinclair Chen
c044460a91
Don't break build on unused import (#1046)
* Fix lint warnings

* Let vercel build when unused import
2022-10-13 18:16:22 -07:00
mantikoros
7a6725ee77 contract card: less busy design 2022-10-13 19:56:16 -05:00
Austin Chen
823d1ddd4c Update stability-client 2022-10-13 17:01:05 -07:00
Austin Chen
7d490e0de1 Expand image on hover 2022-10-13 16:23:25 -07:00
Sinclair Chen
96d2255cb1 fix find and replace mistake 2022-10-13 15:15:45 -07:00
Sinclair Chen
4a139c5cc2 Fix limit prob styling 2022-10-13 14:55:48 -07:00
James Grugett
47eb8abed0 Don't include loans in email payout message 2022-10-13 16:05:40 -05:00
Sinclair Chen
903fcc83b3 Fix form-control styling 2022-10-13 13:20:04 -07:00
Austin Chen
0615bb2d4b
List ManifoldDream as a bot 2022-10-13 13:12:22 -07:00
mantikoros
3508c94634 tip button: fix tip color 2022-10-13 14:33:41 -05:00
mantikoros
29c0dfe3fe tip button: remove fill color 2022-10-13 14:27:47 -05:00
mantikoros
18a3b66164 dream label 2022-10-13 14:16:30 -05:00
James Grugett
9e4f41253f Filter out resolved markets from daily movers 2022-10-13 13:49:48 -05:00
Sinclair Chen
546b0231e7
Die Daisy (#1042)
* un-daisy labels

* un-daisy inputs

* un-daisy input groups

* fixup input

* un-daisy selects

* un-daisy slider

* Uninstall daisy. Migrate colors

* un-daisy tables

* fix input error styling

* lint
2022-10-13 11:23:42 -07:00
Phil
8bb44593f3
Updated dev config to point to new Twitch dev bot hosting location. (#1044) 2022-10-13 17:19:50 +01:00
Pico2x
2c2bc61788 Fix bug with parsing in abritrary react components 2022-10-13 16:56:35 +01:00
Pico2x
34c9dbb3e7 Revert "Revert "New implementation of market card embeddings (#1025)""
This reverts commit d6525bae9f.
2022-10-13 16:25:15 +01:00
Ian Philips
d6525bae9f Revert "New implementation of market card embeddings (#1025)"
This reverts commit 3fc53112b9.
2022-10-13 09:19:28 -06:00
Ian Philips
da32a756a8 Need not follow contract for tag notification 2022-10-13 08:41:33 -06:00
Ian Philips
fa476c78dd Handle numeric outcomes in movers 2022-10-13 08:18:16 -06:00
Ian Philips
e7ba7e715f default cursor on open answer 2022-10-13 08:04:01 -06:00
Ian Philips
9bf82c6082 Match colors on portfolio 2022-10-13 08:00:04 -06:00
Ian Philips
3e1876f0dc Add flaggedByUsernames to firestore rules 2022-10-13 07:58:14 -06:00
ingawei
5ba4a9dce7
Inga/tip button (#1043)
* added tip jar
* made market actions/comments and manalink buttons IconButtons
2022-10-13 01:53:26 -05:00
James Grugett
4e5b78f4ee Use in-memory store for home featured section data 2022-10-12 21:15:40 -05:00
James Grugett
bc6fab399e Make text grayer 2022-10-12 20:51:48 -05:00
James Grugett
c2d112e516 Position => shares 2022-10-12 20:51:20 -05:00
ingawei
3cbe8ad8bb
Inga/comment bounty fix (#1041)
* fixed bounty button in comments
2022-10-12 20:34:07 -05:00
ingawei
6226291e02 made comments smaller 2022-10-12 17:53:37 -07:00
Austin Chen
fa4dba4da3 Document the new /comment endpoint 2022-10-12 17:26:00 -07:00
Austin Chen
9eff69be75
Add /createcomment API endpoint (#946)
* /dream api: Upload StableDiffusion image to Firestore

* Minor tweaks

* Set content type on uploaded image

This makes it so the image doesn't auto-download when opened in a new tab

* Allow users to dream directly from within Manifold

* Remove unused import

* Implement a /comment endpoint which supports html and markdown

* Upgrade @tiptap/core to latest

* Update all tiptap deps to beta.199

* Add @tiptap/suggestion

* Import @tiptap/html in the right place

* ... add deps everywhere

So I have no idea how common deps work apparently

* Add tiptap/suggestion too

* Clean up dream

* More cleanups

* Rework /comment endpoint

* Move API to /comment

* Change imports in case that matters

* Add a couple todos

* Dynamically import micromark

* Parallellize gsutil with -m option

* Adding comments via api working, editor.tsx erroring out

* Unused import

* Remove disabled state from useTextEditor

Co-authored-by: Ian Philips <iansphilips@gmail.com>
2022-10-12 17:25:17 -07:00
James Grugett
789bec2a4f Expand replies by default (quick fix for comment links not working) 2022-10-12 17:49:04 -05:00
James Grugett
18042cd4d1 Return listener 2022-10-12 17:39:13 -05:00
mantikoros
04a126707b fix welcome dialog page freezing bug 2022-10-12 17:33:18 -05:00
mantikoros
7a412fdb0d add key props 2022-10-12 17:33:18 -05:00
James Grugett
e2dc4c6b8f Resolve markets again script 2022-10-12 17:30:21 -05:00
mantikoros
204d302d87 portfolio copy: "Open" => "Active" 2022-10-12 16:57:21 -05:00
James Grugett
ae39c1175b
Better resolve market payouts (#1038)
* Check payout preconditions first. Try to pay out market in 1 transaction.

* Format

* toBatch => lodash's chunk
2022-10-12 16:21:37 -05:00
Marshall Polaris
c44f223064
Fix some hydration issues (#1033)
* Extract `useIsClient` hook

* Fix a bunch of hydration bugs relevant to the contract page
2022-10-12 14:07:07 -07:00
mantikoros
aa717a767d backfill subsidyPool 2022-10-12 16:05:15 -05:00
Sinclair Chen
d9f57b7daa
Remove all daisy buttons (#1036)
* Tweak limit order UI and fix button

* Style all follow/unfollow buttons blue

also get rid of highlight-blue button

* remove all other uses of 'btn'

* Style group follow button like user follow

* lint and format
2022-10-12 12:27:17 -07:00
mantikoros
93ceaa52c4 payouts: fix subsidyPool undefined bug 2022-10-12 14:24:54 -05:00
James Grugett
de76557326 Show shares as contract card position. Fix bug on outcome 2022-10-12 13:55:58 -05:00
James Grugett
da1fcb646f Fix infinite re-render on home page 2022-10-12 13:42:54 -05:00
ingawei
1d618ba337
Inga/fr remove double comments (#1019)
incorporating answer comments into general comments section
2022-10-12 13:05:58 -05:00
Pico2x
2cda3a4d4f Don't show spinner if pinned items is undefined or zero 2022-10-12 18:12:32 +01:00
Sinclair Chen
e44fc8ae13
Simplify rich text to string parsing logic (#1022)
* Simplify rich text to string parsing logic

* lint
2022-10-12 10:09:59 -07:00
James Grugett
e6a90e18e4 Add more padding and improve layout of post card 2022-10-12 11:59:29 -05:00
Austin Chen
cee8caa3e8
Generate images from StableDiffusion (#1035)
* Generate images from StableDiffusion

* Update yarn.lock

* Log an error, remove extra comment

* Code cleanup

* Note about the API
2022-10-12 09:53:04 -07:00
Pico2x
b49264ddfa Show featured to all users 2022-10-12 17:44:43 +01:00
Pico2x
12ed569ff6 Update post card subtitle color 2022-10-12 17:33:02 +01:00
Pico2x
00acc262a0 Update post-cards to show profile pic correctly 2022-10-12 17:31:00 +01:00
Pico2x
fd7d4eb5e2 Make post-cards consistent with contract cards 2022-10-12 17:22:58 +01:00
Pico2x
8ae1166c49 Posts now have denormalized creator username and name 2022-10-12 16:42:28 +01:00
FRC
84e2b63c49
Entire homepage now loads simultaneously including featured (#1034) 2022-10-12 16:02:57 +01:00
FRCassarino
f19ef83ac2 Auto-remove unused imports 2022-10-12 14:38:59 +00:00
Pico2x
0c11f3b450 Fix is admin in featured 2022-10-12 15:38:06 +01:00
Pico2x
de9ffa2b52 Fix bug in which Featured section shows for everyone 2022-10-12 15:29:32 +01:00
Ian Philips
decb3213f6 Ignore cancel resolutions for proven correct 2022-10-12 08:05:55 -06:00
FRC
ff6278b147
Featured items to homepage (#1024)
* Featured items to homepage

* Fix nits
2022-10-12 15:04:39 +01:00
FRC
3fc53112b9
New implementation of market card embeddings (#1025)
* Grids of cards now implemented by rendering component instead of iframe

* Sinclair's nit
2022-10-12 13:24:22 +01:00
Marshall Polaris
59cdc9f776
Update FR colors, consolidate non-top answers into "Other" (#1031)
* Update FR colors, consolidate non-top answers into "Other"

* Fix answer panel coloration to not be weird and work on Firefox
2022-10-11 23:59:11 -07:00
ingawei
f587e0256d
standardizing red and green colors (#1032) 2022-10-12 01:58:20 -05:00
ingawei
1c209f68f6
de daisy sell button (#1030)
* de daisy sell button
2022-10-12 01:31:32 -05:00
ingawei
b4e7d88ed8
de daisied cancel limit bet button (#1029) 2022-10-12 01:13:43 -05:00
ingawei
b2cd6bbe03
Inga/de daisy follow button (#1028)
* de daisy follow button
2022-10-12 01:00:52 -05:00
ingawei
a6d5d5ad15
made create a post button not daisy (#1027)
yay no daisy
2022-10-12 00:24:59 -05:00
ingawei
beeca57d4e
getting rid of daisy for limit order button (#1026)
* getting rid of daisy for limit order button, got rid of betChoice in limit order panel
2022-10-12 00:09:45 -05:00
Ian Philips
fb8bd1acfb Handle slugs with and without leading / 2022-10-11 17:22:58 -06:00
Ian Philips
4215821f35 Fix firebase query for market creators 2022-10-11 16:59:47 -06:00
Ian Philips
a71c3d6a4a Fix notification link 2022-10-11 16:59:20 -06:00
James Grugett
cdcce421a8 Include today's bets in daily profit 2022-10-11 17:32:35 -05:00
Ian Philips
8beff6eb1f Format money 2022-10-11 15:36:35 -06:00
Ian Philips
f714918b88 Update functions readme with local dev details 2022-10-11 15:32:53 -06:00
James Grugett
946d74489f Gray scale position and profit 2022-10-11 16:29:15 -05:00
mantikoros
220d0841bd move liquidity to info dialog 2022-10-11 16:22:35 -05:00
James Grugett
9d44190b9a Fix group url nav to correct tabs 2022-10-11 16:04:40 -05:00
Sinclair Chen
3cdd790ae9
Autosave post, market, comment rich text (#1015)
* Fix freezing when typing big docs

* Make rich text fields autosave to localstorage

* Add autosave for comments

* delete vestigial text editor from challenges

* Clear autosave on submit post/market/comment

* lint
2022-10-11 12:52:27 -07:00
mantikoros
6c1ac89cbe typo 2022-10-11 14:28:10 -05:00
mantikoros
0d8a84ef06 re-order tournaments 2022-10-11 14:26:52 -05:00
James Grugett
d528566ffa People's=>Party #2 2022-10-11 13:46:58 -05:00
James Grugett
b0f8369d9c People's=>Party 2022-10-11 13:41:30 -05:00
James Grugett
721c18cf6c Add tournament for CCP 20th Congress 2022-10-11 13:37:40 -05:00
mantikoros
43b06ae6fa tip button: show number of tips 2022-10-11 13:30:30 -05:00
Ian Philips
bfdb5ae595 Cleanup comments 2022-10-11 10:26:37 -06:00
Ian Philips
274f7fa849 Send market closed notifications every 5 days 2022-10-11 10:22:38 -06:00
Ian Philips
d507c4092e Code for removing erroneous badges 2022-10-11 08:51:51 -06:00
Ian Philips
e970a908c6 Switch logic to includes 2022-10-11 08:35:59 -06:00
Pico2x
4fd0e5caad Fix bug in which new users are flagged as unreliable 2022-10-11 13:39:40 +01:00
James Grugett
70b2b14f80
Daily profit 💰 (#1023)
* Daily profit client side

* Filter out those where profit rounds to 0

* Tabs to spaces
2022-10-11 00:32:55 -05:00
mantikoros
0ec15ff2f8
Make liquidity great again (#1020)
* add subsidy

* drizzle liquidity

* update liquidity panel

* remove addliquidity

* update cloud functions index

* remove json endpoints

* imports

* drizzle liquidity: add velocity; dev script; run every minute

* adjust speed

* logging

* liquidity button, dialog

* modal size

* modal

* info table

* pay back excess liquidity

* remove client withdrawal

* house liquidity subsidy

* disable liquidity button if market resolved or closed

* format tip amount
2022-10-10 21:56:16 -05:00
Sinclair Chen
8bb9885aee
Fix @mention 500 error and can't close market bug (#1021)
* Fix @mention 500 error. Refactor text concat exts

* lint
2022-10-10 18:47:02 -07:00
Ian Philips
c46c384d1d Add more bot tags, better creator name scaling 2022-10-10 15:38:27 -06:00
Ian Philips
4f5c93be96 Badge notifications ui changes 2022-10-10 15:01:18 -06:00
James Grugett
f03e5d7af0
Refactor portfolio query (#1018)
* Fetch less data for portfolio query

* Rename var
2022-10-10 15:51:51 -05:00
Sinclair Chen
fb0a09664e delete bannerUrl from user type 2022-10-10 13:51:27 -07:00
Ian Philips
17d0fb7da6 Change badge award notif setting group 2022-10-10 14:41:24 -06:00
Ian Philips
867cdf2496 Only backfill unfilled users' badges 2022-10-10 14:34:06 -06:00
Ian Philips
f26ba1c4a2
Award badges for market creation, betting streaks, proven correct (#891)
* Award badges for market creation, betting streaks, proven correct

* Styling

* Add minimum unique bettors for proven correct

* Add name, refactor

* Add notifications for badge awards

* Correct styling

* Need at least 3 unique bettors for market maker badge

* Lint

* Switch to badges_awarded

* Don't include n/a resolutions in market creator badge

* Add badges by rarities to profile

* Show badges on profile, soon on market page

* Add achievements to new user

* Backfill all users badges
2022-10-10 14:32:29 -06:00
James Grugett
cdc64c6475 Correctly configure env var for firebase functions 2022-10-10 14:55:33 -05:00
James Grugett
5d561acdf8 Fix NaN 2022-10-10 14:23:16 -05:00
James Grugett
84f79ffe7c Remove undefined props 2022-10-10 13:34:02 -05:00
James Grugett
f6fd703005
Store each user's contract bet metrics (#1017)
* Implement most of caching metrics per user per contract

* Small group updates refactor

* Write contract-metrics subcollection

* Fix type error
2022-10-10 13:05:17 -05:00
mantikoros
b8ef272784 withdrawal warning 2022-10-10 13:01:57 -05:00
Ian Philips
a4699b79ed If unlisted during creation, fill in creator id 2022-10-10 10:48:01 -06:00
Ian Philips
66071e16fa Try without timeout seconds on pubsub 2022-10-10 09:53:42 -06:00
Ian Philips
b3136ebcac Update update-metrics timeout sends 2022-10-10 09:48:46 -06:00
Ian Philips
a143a96919 Fix unable to close contract 2022-10-10 09:24:37 -06:00
Ian Philips
dea65a4ba0 Better error handling for notification destinations 2022-10-10 07:41:41 -06:00
Ian Philips
a310963952 update prefs safely 2022-10-10 07:01:44 -06:00
Sinclair Chen
8d06e4b4d2
refactor text input into one component (#1016)
* Add responsive text input component

* Add styled expanding textarea component
2022-10-09 19:37:24 -07:00
James Grugett
dc51e2cf46 Rename updateMetrics to scheduleUpdateMetrics 2022-10-09 19:11:44 -05:00
sipec
4831c25ce0 Auto-prettification 2022-10-09 23:10:02 +00:00
Sinclair Chen
60f2552139 copy: Referrals -> Refer a friend 2022-10-09 16:09:21 -07:00
mantikoros
4b8d381da5 hide comment bounty when market closed or resolved 2022-10-09 17:14:20 -05:00
mantikoros
565177b76f track midterms, date docs 2022-10-09 17:02:34 -05:00
Sinclair Chen
8bd21c6693 hotfix %mention, add load-fail state 2022-10-08 22:52:36 -07:00
James Grugett
310a41d63e Make loan and bet streak links hoverable in notifications 2022-10-08 12:51:58 -05:00
mantikoros
e1636d0f13 update metrics: fix divide by zero, elasticity NaN bug 2022-10-08 12:16:45 -05:00
James Grugett
d00ea65279 Add MattP to winners for AMM liquidity exploit 2022-10-07 17:45:42 -05:00
James Grugett
60bb5379cb Update bounties doc 2022-10-07 17:30:20 -05:00
James Grugett
f3dedfb27a
Call updatemetrics v2 cloud function from scheduled function (#1014)
* Call updatemetrics v2 cloud function from scheduled function

* Set limits on bets and contracts loaded for portfolio page. Show warning if limit is hit

* mqp review: Use console.error if !response.ok
2022-10-07 17:10:12 -05:00
mantikoros
efa2e44937 comment bounty dialog, styling 2022-10-07 16:26:30 -05:00
mantikoros
84bc490ed3 comment sort: move below input, newest as default 2022-10-07 16:26:30 -05:00
James Grugett
443397b7dc
Action to merge main into main2 automatically 2022-10-07 15:13:57 -05:00
James Grugett
b57ff68654 Fix highlight & scroll to comment 2022-10-07 14:40:38 -05:00
Sinclair Chen
f0b35993c9 fix hydration error (button inside button) 2022-10-07 10:56:27 -07:00
James Grugett
8f56ccad22 Set limits on bets and contracts loaded for portfolio page. Show warning if limit is hit 2022-10-07 11:55:47 -05:00
mantikoros
9e289146af flat trade fee of M$0.1 aka bot tax 2022-10-06 23:04:48 -05:00
James Grugett
4285198f09 Merge branch 'main' into main2 2022-10-06 22:19:39 -05:00
James Grugett
f533d9bfcb
Verify balance of limit order "makers" (#1007)
* Fetch balance of users with open limit orders & cancel orders with insufficient balance

* Fix imports

* Fix bugs

* Fix a bug

* Remove redundant cast

* buttons overlaying content fix (#1005)

* buttons overlaying content fix

* stats: round DAU number

* made set width for portfolio/profit fields (#1006)

* tournaments: included resolved markets

* made delete red, moved button for regular posts (#1008)

* Fix localstorage saved user being overwritten on every page load

* Market page: Show no right panel while user loading

* Don't flash sign in button if user is loading

* election map coloring

* market group modal scroll fix (#1009)

* midterms: posititoning, make mobile friendly

* Un-daisy share buttons (#1010)

* Make embed and challenge buttons non-daisyui

* Allow link Buttons. Change tweet, dupe buttons.

* lint

* don't insert extra lines when upload photos

* Map fixes (#1011)

* usa map: fix sizing

* useSetIframeBackbroundColor

* preload contracts

* seo

* remove hook

* turn off sprig on dev

* Render timestamp only on client to prevent error of server not matching client

* Make sized container have default height so graph doesn't jump

* midterms: use null in static props

* Create common card component (#1012)

* Create common card component

* lint

* add key prop to pills

* redirect to /home after login

* create market: use transaction

* card: reduce border size

* Update groupContracts in db trigger

* Default sort to best

* Save comment sort per user rather than per contract

* Refactor Pinned Items into a reusable component

* Revert "create market: use transaction"

This reverts commit e1f24f24a9.

* Mark @v with a (Bot) label

* fix padding on daily movers

* fix type errors

* Wrap sprig init in check for window

* unindex date-docs from search engines

* Auto-prettification

* compute elasticity

* change dpm elasticity

* Fix google lighthouse issues (#1013)

* don't hide free response panel on open resolve

* liquidity sort

* Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'.

* Date doc: Toggle to disable creating a prediction market

* Listen for date doc changes

* Fix merge error

* Don't cancel all a users limit orders if they go negative

Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
Co-authored-by: mantikoros <95266179+mantikoros@users.noreply.github.com>
Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: Pico2x <pico2x@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: sipec <sipec@users.noreply.github.com>
2022-10-06 22:16:49 -05:00
Austin Chen
71b0c71729 Tag ArbitrageBot with bot badge 2022-10-06 21:52:55 -05:00
mantikoros
25333317b0 Show elasticity; volume tooltip 2022-10-06 21:52:55 -05:00
Austin Chen
42a7d04b4d Tag ArbitrageBot with bot badge 2022-10-06 20:17:34 -04:00
James Grugett
b1d386ca5a Listen for date doc changes 2022-10-06 18:54:22 -05:00
James Grugett
0dc8753a92 Listen for date doc changes 2022-10-06 18:50:53 -05:00
mantikoros
454f2d1417 Merge branch 'main' into main2 2022-10-06 18:48:56 -05:00
James Grugett
d846b9fb30 Date doc: Toggle to disable creating a prediction market 2022-10-06 18:42:56 -05:00
James Grugett
77e0631ea4 Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'. 2022-10-06 18:42:56 -05:00
James Grugett
badd67c278 Date doc: Toggle to disable creating a prediction market 2022-10-06 18:36:27 -05:00
mantikoros
80622dc7ee liquidity sort 2022-10-06 18:23:27 -05:00
James Grugett
9d12fa3af0 Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'. 2022-10-06 18:03:44 -05:00
Sinclair Chen
d9c8925ea0 don't hide free response panel on open resolve 2022-10-06 15:20:46 -07:00
Sinclair Chen
adb809f973
Fix google lighthouse issues (#1013) 2022-10-06 15:19:37 -07:00
mantikoros
a63405ca7c change dpm elasticity 2022-10-06 16:47:52 -05:00
mantikoros
7ca0fb72fc compute elasticity 2022-10-06 16:36:32 -05:00
sipec
ac37f94cf7 Auto-prettification 2022-10-06 20:50:29 +00:00
Sinclair Chen
bc5af50b0c unindex date-docs from search engines 2022-10-06 13:49:39 -07:00
James Grugett
4162cca3ff Wrap sprig init in check for window 2022-10-06 15:23:51 -05:00
mantikoros
91da39370f fix type errors 2022-10-06 14:54:22 -05:00
Sinclair Chen
2f2c586d5d fix padding on daily movers 2022-10-06 12:01:00 -07:00
Austin Chen
853e3e4896 Mark @v with a (Bot) label 2022-10-06 14:20:35 -04:00
Pico2x
edbd0feb37 Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-06 17:04:32 +01:00
Pico2x
59de979949 Refactor Pinned Items into a reusable component 2022-10-06 17:04:00 +01:00
mantikoros
b8d65acc3f Revert "create market: use transaction"
This reverts commit e1f24f24a9.
2022-10-06 10:54:42 -05:00
Ian Philips
26f04fb04a Save comment sort per user rather than per contract 2022-10-06 10:16:29 -04:00
Ian Philips
e127f9646a Default sort to best 2022-10-06 09:53:55 -04:00
Ian Philips
25ef17498a Update groupContracts in db trigger 2022-10-06 09:26:35 -04:00
mantikoros
68075db3da card: reduce border size 2022-10-05 22:20:51 -05:00
mantikoros
e1f24f24a9 create market: use transaction 2022-10-05 22:18:19 -05:00
mantikoros
cd8245fbee redirect to /home after login 2022-10-05 21:38:13 -05:00
mantikoros
f1e400765a add key prop to pills 2022-10-05 21:31:34 -05:00
Sinclair Chen
94624c5387
Create common card component (#1012)
* Create common card component

* lint
2022-10-05 18:02:24 -07:00
mantikoros
7ce09ae39d midterms: use null in static props 2022-10-05 19:24:03 -05:00
James Grugett
935bdd12a7 Make sized container have default height so graph doesn't jump 2022-10-05 18:44:30 -05:00
James Grugett
5d7721e041 Render timestamp only on client to prevent error of server not matching client 2022-10-05 18:44:30 -05:00
mantikoros
a149777c0e turn off sprig on dev 2022-10-05 18:36:32 -05:00
mantikoros
81fb2456bd remove hook 2022-10-05 18:29:08 -05:00
mantikoros
f8ec306ee9
Map fixes (#1011)
* usa map: fix sizing

* useSetIframeBackbroundColor

* preload contracts

* seo
2022-10-05 18:20:40 -05:00
Sinclair Chen
a53fb49ec3 don't insert extra lines when upload photos 2022-10-05 16:08:01 -07:00
Sinclair Chen
7863a4232d
Un-daisy share buttons (#1010)
* Make embed and challenge buttons non-daisyui

* Allow link Buttons. Change tweet, dupe buttons.

* lint
2022-10-05 15:51:10 -07:00
mantikoros
a3b841423f midterms: posititoning, make mobile friendly 2022-10-05 17:12:50 -05:00
ingawei
b8911cafe8
market group modal scroll fix (#1009) 2022-10-05 17:07:41 -05:00
mantikoros
60aa294131 election map coloring 2022-10-05 16:58:47 -05:00
James Grugett
0818a94307 Don't flash sign in button if user is loading 2022-10-05 16:56:47 -05:00
James Grugett
a3acd3fa3c Market page: Show no right panel while user loading 2022-10-05 16:52:16 -05:00
James Grugett
1ef1af8234 Fix localstorage saved user being overwritten on every page load 2022-10-05 16:49:28 -05:00
ingawei
189da4a0cf
made delete red, moved button for regular posts (#1008) 2022-10-05 16:21:03 -05:00
mantikoros
10f0bbc63d tournaments: included resolved markets 2022-10-05 15:46:20 -05:00
ingawei
2d56525d65
made set width for portfolio/profit fields (#1006) 2022-10-05 15:40:26 -05:00
mantikoros
f1f8082600 stats: round DAU number 2022-10-05 15:27:20 -05:00
ingawei
ec006f25c4
buttons overlaying content fix (#1005)
* buttons overlaying content fix
2022-10-05 15:19:10 -05:00
James Grugett
b40a114168 Print error from usePagination 2022-10-05 15:13:40 -05:00
Ian Philips
4bbadeb27c Parse images and iframes from tiptap to string descriptions 2022-10-05 14:08:51 -06:00
mantikoros
2596d54831 update tournaments page 2022-10-05 14:46:15 -05:00
Austin Chen
0df5497ffb Fix regression of unable to purchase mana 2022-10-05 15:35:02 -04:00
Ian Philips
27dabc193c Comment out logs 2022-10-05 13:09:40 -06:00
Ian Philips
6ec1b38a21 Add more stringent duplication req and popularity score 2022-10-05 13:07:23 -06:00
James Grugett
f35eb42d7b Make search query params work on page load 2022-10-05 13:27:46 -05:00
James Grugett
18f8ad433d Hide sort and query options when searching 2022-10-05 13:12:30 -05:00
Sinclair Chen
37e8f2ff5a
Make %mentions actually look like mentions (#993)
* Make contract mentions inline text

* Add tooltip for author, close time, volume

* Make pill wider
2022-10-05 11:11:05 -07:00
Ian Philips
328aa1457d Send interesting markets based on groups, follows, similar bettors 2022-10-05 10:41:57 -06:00
Austin Chen
b9ba3e75fa Show top 5k markets in sitemap 2022-10-05 11:49:47 -04:00
Austin Chen
70bfec2742 Improve sitemaps 2022-10-05 11:41:32 -04:00
Pico2x
26281556f7 Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-05 15:54:13 +01:00
Austin Chen
730abf584a Revert "Warn whenever rich text editor has unsaved changes"
This reverts commit 419219c703.
2022-10-05 10:54:04 -04:00
Pico2x
34d09316e0 Yscale now updates when zooming in on chart 2022-10-05 15:46:18 +01:00
Austin Chen
6f41ab8efd Keep unlisted state on duplicate 2022-10-05 10:38:23 -04:00
Pico2x
f1207e87ec Update share-modal.tsx 2022-10-05 15:18:53 +01:00
Pico2x
4e22b8e332 Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-05 15:01:44 +01:00
Pico2x
07de8cc86a Slightly less horrible color palette 2022-10-05 15:01:41 +01:00
akrolsmir
f07a022d63 Auto-remove unused imports 2022-10-05 13:58:54 +00:00
Austin Chen
d42ec42b0e Standardize on CopyLinkButton 2022-10-05 09:55:46 -04:00
Austin Chen
6fa4e17a58 Remove "Add my comment" button for signed out users 2022-10-05 09:35:21 -04:00
Austin Chen
af3a3a3934 Clean up style on Get M$ 2022-10-05 09:30:51 -04:00
Pico2x
9e3477970d Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-05 14:19:38 +01:00
Pico2x
3390c34d0a Mobile tooltip isn't occluded by finger anymore 2022-10-05 14:19:26 +01:00
Austin Chen
419219c703 Warn whenever rich text editor has unsaved changes 2022-10-05 09:16:05 -04:00
Pico2x
8aaca848b2 Updating group name works again. 2022-10-05 14:02:17 +01:00
Pico2x
e4d7d0a232 Mobile graphs no longer show the zoom function. 2022-10-05 13:57:45 +01:00
Pico2x
e9050973e1 Update post-comments.tsx 2022-10-05 11:53:59 +01:00
FRC
83d9a1f3e2
Posts changes (#988)
* Add post subtitle

* Add "Post" badge to post card

* Move post tab to overview tab, refactor components

* Fix styling nits.
2022-10-05 11:37:23 +01:00
Marshall Polaris
49e97ddac1
Small add-on React 18 fixes (#1004)
* Bump React types for main code to 18

* Replace react-beautiful-dnd with maintained fork

* Add a type annotation
2022-10-05 00:23:23 -07:00
Marshall Polaris
a9d5dd7fc8
Memoize FeedBet component (#999) 2022-10-04 23:17:09 -07:00
Marshall Polaris
ddb186dd98
Change Tipper interface, memoize FeedComment (#1000)
* Change `Tipper` interface, memoize `FeedComment`

* Fixup per James feedback

* More fixup per James feedback
2022-10-04 23:16:56 -07:00
Marshall Polaris
d2273087cf
React 17.0.2 -> 18.2.0 (#1003)
* React 17.0.2 -> 18.2.0

* Adjust title tag to only have one text node (no internal spaces)

* react-expanding-textarea 2.3.5 -> 2.3.6
2022-10-04 23:16:39 -07:00
James Grugett
6a0b577aeb Small home refactor 2022-10-05 00:54:38 -05:00
James Grugett
ca6197c7bb Show total loan amount on portfolio 2022-10-05 00:33:05 -05:00
Sinclair Chen
ed6ea011c2 copy: "Dating docs" -> "Dating" 2022-10-04 18:33:26 -07:00
James Grugett
83d33792aa Loan repaid => Loan payment 2022-10-04 20:10:26 -05:00
sipec
583c5b225e Auto-remove unused imports 2022-10-05 00:49:36 +00:00
Sinclair Chen
9f256aa7a8 refactor: require label on buy/confirm buttons 2022-10-04 17:48:24 -07:00
mantikoros
7a271fce29 fix button 2022-10-04 19:35:53 -05:00
James Grugett
d8ef363f06 Add profit amount to sell dialog. 2022-10-04 19:35:29 -05:00
James Grugett
8043fa515a Show loan repaid in sell dialog 2022-10-04 19:10:45 -05:00
mantikoros
f551e6c469 market close styling 2022-10-04 19:07:06 -05:00
Sinclair Chen
3f0b665753 Add Mriya to charities list 2022-10-04 16:57:05 -07:00
Sinclair Chen
40b07329bd Make follow & unfollow buttons same size 2022-10-04 16:42:17 -07:00
Ian Philips
7b9aeea0bd Ignore similar bettor's followed user's markets 2022-10-04 17:12:07 -06:00
Ian Philips
935ff7b97a
Personalized interesting markets emails [WIP] (#1001)
* Test new personalized emails in prod - logs only

* fix import
2022-10-04 16:47:06 -06:00
ingawei
c115b5cca7
Inga/multiple colors (#994)
* making FR bars smoller
2022-10-04 17:36:03 -05:00
Marshall Polaris
d6bb27f97c
Fix graph area to invert at 0 (#998) 2022-10-04 14:13:53 -07:00
Marshall Polaris
bbce3e873e
Tooltip follows marker on charts with marker (#997)
* Renaming of some tooltip stuff

* Tooltip follows marker on single value history charts
2022-10-04 14:02:44 -07:00
mantikoros
26f5e506b7 sell button warning 2022-10-04 15:53:00 -05:00
ingawei
5adaa7253f
made slice skinnier (#996) 2022-10-04 15:41:48 -05:00
Marshall Polaris
a55d85d4b6
Implement basic graph tooltip slice marker thingy (#995) 2022-10-04 12:55:51 -07:00
Ian Philips
f085df96e3 Allow wider group pills on mobile 2022-10-04 09:23:07 -06:00
Ian Philips
1d2af2900b Remove spaces in hashtags & line clamp metadata 2022-10-04 09:14:27 -06:00
Ian Philips
a48cec63fc Use line-clamp in sharing card 2022-10-04 09:06:23 -06:00
Ian Philips
e6374c4994 Fix title, send out the remaining emails today 2022-10-04 08:55:44 -06:00
Ian Philips
6ac467764d Fix unsubscribe all update 2022-10-04 08:38:20 -06:00
Ian Philips
79af4b2be0 Compare to the const ya sillyhead 2022-10-04 08:11:04 -06:00
Ian Philips
094bcaea17 Convert confirmation daisy buttons to tailwind 2022-10-04 08:03:21 -06:00
Ian Philips
c6e5e04e65 Convert confirmation daisy buttons to tailwind 2022-10-04 08:02:20 -06:00
Pico2x
ee4d3947b8 moar contracts in mentions 2022-10-04 14:38:07 +01:00
Marshall Polaris
45b281fac5 Kill a console log 2022-10-04 01:34:17 -07:00
Marshall Polaris
31c6cb7739
Rewrite portfolio graphs with new machinery (#986)
* Fix chart `onMouseOver` propagation

* Make generic charts support money on y-axis

* Fix somewhat ridiculous `formatMoney` to work with negative amounts

* Make margins on charts configurable

* Implement color as function of point on SingleValueHistoryChart

* Rewrite portfolio history graphs with new graph machinery

* Toast Nivo
2022-10-04 01:18:22 -07:00
mantikoros
23ca3ff56a order book label 2022-10-03 23:56:39 -05:00
Sinclair Chen
c3ffac34a1
Remove tag parsing (#956)
* Remove #tag parsing

* Remove #tag linkifying

* lint
2022-10-03 18:28:21 -07:00
Sinclair Chen
375a4e089f
Hide spoiler content in emails / notifs (#957) 2022-10-03 18:25:44 -07:00
James Grugett
efd83eaad4 Home: Don't show duplicate contracts across groups 2022-10-03 18:28:41 -05:00
James Grugett
8d70dc4800 Increase trending group count again. (It's uglier, but it's way more useful!) 2022-10-03 18:22:18 -05:00
Ian Philips
a1dcf8d168 Add best sort to FR contracts 2022-10-03 16:38:12 -06:00
James Grugett
84aaeece9f Filter out unlisted from trending, new, and daily trending on home page 2022-10-03 17:33:51 -05:00
James Grugett
27d765a4a1 Add most popular sort 2022-10-03 17:28:31 -05:00
James Grugett
5214f27be3 Fix daily prob showing global daily prob 2022-10-03 17:23:22 -05:00
mantikoros
d0d223f7ad Auto-prettification 2022-10-03 22:21:56 +00:00
mantikoros
0c9226de41 EditableCloseDate: add pencil icon 2022-10-03 17:20:53 -05:00
ingawei
ce48016f80
added mana cannot be traded for real money disclaimer in welcome modal (#990) 2022-10-03 16:38:10 -05:00
Marshall Polaris
1515d8cab2 Fix lint on Austin's script 2022-10-03 14:30:34 -07:00
Marshall Polaris
28cad9caf8
Bump heroicons yarn.lock version to 1.0.6 (#992) 2022-10-03 14:22:31 -07:00
Marshall Polaris
9a950dc080
Mark scripts as modules to avoid global name collision (#991) 2022-10-03 14:21:21 -07:00
Ian Philips
42cc07e4a6 Hide market title in notifs if grouped 2022-10-03 14:59:27 -06:00
Ian Philips
a5490c903f Reduce streak and max to 5, 25 respectively 2022-10-03 14:52:21 -06:00
Ian Philips
71975f307c Show resolve loading indicator 2022-10-03 14:33:00 -06:00
Ian Philips
ae4136348d Unique bettors email default on 2022-10-03 14:12:55 -06:00
Sinclair Chen
67de983aac Fix links in spoilers 2022-10-03 11:55:10 -07:00
Sinclair Chen
59b128dbe7
Redo add funds modal without daisy and actually use it (#963) 2022-10-03 10:15:58 -07:00
Ian Philips
074a1fdde2 Hide sprig on non-prod envs 2022-10-03 10:59:18 -06:00
Austin Chen
7c34805eeb Upload script to bulk-resolve markets from API 2022-10-03 12:52:48 -04:00
Pico2x
77a5f8b9dd Revert the merge revert (double revert) 2022-10-03 17:31:07 +01:00
Ian Philips
5ae9049295 Show resolution panel above recommented markets 2022-10-03 10:11:24 -06:00
Ian Philips
d5d1284306 Properly handle null id 2022-10-03 09:57:27 -06:00
Ian Philips
adb8bc476f Show whether market is unlisted 2022-10-03 09:36:49 -06:00
Ian Philips
f92f098f82 Allo creators to unlist markets 2022-10-03 09:26:39 -06:00
Ian Philips
370edec890 Remove unsubscribe options for market closure 2022-10-03 08:30:21 -06:00
FRC
f5a3abf0bc
Add spinner (#987) 2022-10-03 15:27:15 +01:00
Ian Philips
27e6534d94 Persist preferred comment sort order by contract 2022-10-03 08:15:27 -06:00
Ian Philips
1caf75d3b5 Do not refund comment bounties 2022-10-03 07:49:26 -06:00
Ian Philips
051c2905e1
Allow user to opt out of all unnecessary notifications (#974)
* Allow user to opt out of all unnecessary notifications

* Unsubscribe from all response ux

* Only send one response
2022-10-03 07:41:39 -06:00
Pico2x
1f7b9174b3 Update index.tsx 2022-10-03 13:45:38 +01:00
FRC
06571a3657
Flag incorrectly resolved markets, warn about unreliable creators (#945)
* Flag incorrectly resolved markets, warn about unreliable creators

* Address James' review nits

* Added a loading state and some copy-changes

* Fix missing refactor

* Fix vercel error

* Fix merging issues
2022-10-03 10:49:19 +01:00
Pico2x
3fb43c16c4 Revert "Merge branch 'main' of https://github.com/manifoldmarkets/manifold"
This reverts commit 603201a00f, reversing
changes made to b517817ee3.
2022-10-03 10:02:38 +01:00
Pico2x
603201a00f Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-03 08:47:23 +01:00
Pico2x
b517817ee3 Fix indentation iphone 2022-10-03 08:47:21 +01:00
James Grugett
80693620f0 Make alt contract card listen for updates 2022-10-02 22:46:04 -05:00
mantikoros
f1ae54355d cowp: pointer cursor 2022-10-02 20:44:24 -05:00
Marshall Polaris
503038d2a2 Fix a dumb bug on pseudo-numeric charts 2022-10-02 16:59:49 -07:00
Marshall Polaris
bf8dca25b2
Rewrite stats graphs using new machinery (#985)
* Make curve configurable on generic charts

* Extract SizedContainer helper component

* Use new charts for stats page

* Move analytics charts component

* Fix up start date logic for graphs excluding data
2022-10-02 16:56:29 -07:00
James Grugett
a82f447965 Fix free response comment threading 2022-10-02 18:20:37 -05:00
FRC
1f8c72b4c9
Overview page on groups (#961)
* Frontpage on groups

wip

* Fix James's nits
2022-10-03 00:02:31 +01:00
James Grugett
40c51c3d59 Add emojis to /labs 2022-10-02 17:14:11 -05:00
James Grugett
86ceea831b Add stats to /labs 2022-10-02 17:10:18 -05:00
mantikoros
efb9ef7602 add padding to embeds 2022-10-02 17:04:28 -05:00
James Grugett
8c1131ebab Tweak home search bar spacing on mobile 2022-10-02 17:04:04 -05:00
mantikoros
2c223160ed comment button styling 2022-10-02 16:58:04 -05:00
mantikoros
11bd658c68 hide comment sort, trade tab if no items 2022-10-02 16:58:04 -05:00
James Grugett
39638a3888 Update mtg link 2022-10-02 15:27:29 -05:00
James Grugett
234820ecd4 Add /labs SEO 2022-10-02 15:24:02 -05:00
James Grugett
4d996c2476 Margin tweak 2022-10-02 15:23:16 -05:00
mantikoros
9ecf10496c Auto-remove unused imports 2022-10-02 20:17:27 +00:00
mantikoros
42b27fcedd update midterms dashboard 2022-10-02 15:15:37 -05:00
jahooma
7bf59bcdd0 Auto-prettification 2022-10-02 20:15:07 +00:00
James Grugett
043b18da0e Add referral link to your user page 2022-10-02 15:13:03 -05:00
mantikoros
64951e691e update midterms dashboard 2022-10-02 15:11:40 -05:00
James Grugett
9a90cc3835 Move manalinks into labs 2022-10-02 15:03:29 -05:00
James Grugett
10e361bcac Load daily movers at top level as well 2022-10-02 14:59:02 -05:00
James Grugett
a7f6cb7cfa Fix labs layout 2022-10-02 14:51:28 -05:00
James Grugett
359a768e14 Move challenges into /labs 2022-10-02 14:49:08 -05:00
James Grugett
42aea03415 Add search bar to home 2022-10-02 14:41:44 -05:00
James Grugett
0fb263efa4 Revert "Test loading user from localstorage on first render"
This reverts commit 701d0a06cd.
2022-10-02 14:16:54 -05:00
James Grugett
747977556b Add /labs to More menu 2022-10-02 14:13:19 -05:00
James Grugett
37e8cfbbed Tweak padding 2022-10-02 14:12:33 -05:00
James Grugett
701d0a06cd Test loading user from localstorage on first render 2022-10-02 14:08:05 -05:00
Sinclair Chen
0ffd6c129a
Make small embeds into cards (#976)
* Fix embed style (adjust input, strikethrough)

* Turn small embeds into contract cards

* Use media query instead of conditional render

* Open embed card clicks in new tab
2022-10-02 11:55:47 -07:00
James Grugett
758dbfe398 Add labs cards 2022-10-02 13:51:42 -05:00
James Grugett
33dfce3e16 Remove dating docs from More menu 2022-10-02 13:43:14 -05:00
James Grugett
af66d94c84 Manifold labs 2022-10-02 13:42:44 -05:00
mantikoros
290a34bc64 useTracking 2022-10-02 13:39:29 -05:00
mantikoros
4c2f9011d0 track embed hostname 2022-10-02 13:39:19 -05:00
mantikoros
57b592b5aa show toast after comment tips 2022-10-02 12:55:58 -05:00
mantikoros
fd31b7eaca set comment sort default to newest 2022-10-02 12:50:49 -05:00
Sinclair Chen
1d645e5ff8 trim copy on sort & bounty tooltips 2022-10-02 08:52:53 -07:00
mantikoros
0b0b84a6ad show tips on own comments again 2022-10-01 16:22:19 -05:00
mantikoros
2baae33a77 show market tip total 2022-10-01 16:16:34 -05:00
mantikoros
fac87f8e0c tips: display total 2022-10-01 16:10:17 -05:00
mantikoros
670c6faea8 tip button: remove border color 2022-10-01 16:00:39 -05:00
mantikoros
09e4864b32 consistent tip amount (M$10) 2022-10-01 15:57:47 -05:00
mantikoros
a445d9b7fa make tip button green 2022-10-01 15:54:14 -05:00
mantikoros
cb613705e9
Consistent tips (#984)
* consistent tip button

* hide tips for self

* prettier
2022-10-01 15:51:08 -05:00
mantikoros
aeeb47bdbe don't block on tipping 2022-10-01 15:06:09 -05:00
mantikoros
0844e5620a create: remove visilbity section 2022-10-01 14:30:31 -05:00
mantikoros
2d6fe308b8 better group sort 2022-10-01 14:14:03 -05:00
James Grugett
759685258a Turn off autofocus for amount input. (Fixes FR answer bug; IMO better UX) 2022-10-01 13:48:13 -05:00
James Grugett
b53e4acea6 API: Cache markets for 15 seconds at least 2022-10-01 13:37:56 -05:00
Marshall Polaris
2f1221f094
Size-aware chart tooltip positioning (#980) 2022-10-01 00:10:17 -07:00
mantikoros
2f3ae5192e embed: disable clicking contract details 2022-09-30 20:30:45 -05:00
James Grugett
b0b1d72ba6 Cleaner home page loading! 2022-09-30 20:07:50 -05:00
Marshall Polaris
dc0b6dc6a6
Don't render stuff whenever window size changes (#978) 2022-09-30 18:01:48 -07:00
Marshall Polaris
89e26d077e
Clean up chart sizing code (#977)
* Clean up chart sizing code

* Do all the chart sizing work in same batch
2022-09-30 16:57:48 -07:00
Marshall Polaris
38b7c898f6
More refactoring to make chart tooltips more flexible (#975) 2022-09-30 16:16:04 -07:00
Austin Chen
1fc2f15dae Try extending /stats to 180 days 2022-09-30 18:46:54 -04:00
Sinclair Chen
3d146dd57d decrease trending group count 2022-09-30 14:52:51 -07:00
ingawei
a219680701
Inga/scroll to top (#965)
- adding scroll to top button for markets, removing predict button at the bottom of comments
2022-09-30 15:16:27 -05:00
James Grugett
1e2df99054 Change format money to round up if within epsilon 2022-09-30 15:05:49 -05:00
Sinclair Chen
37beb584ef fix comment bounty overflow style 2022-09-30 12:54:48 -07:00
IanPhilips
9815e7301f Auto-prettification 2022-09-30 19:48:04 +00:00
Ian Philips
ac97e62f2e Add portfolio updates to notification settings 2022-09-30 13:45:57 -06:00
mantikoros
17d1b8575c comment bounty styling 2022-09-30 14:45:23 -05:00
Ian Philips
a25acbe1db Parse ian's email prefs on dev 2022-09-30 13:36:34 -06:00
Phil
b2f81c1149
Twitch minor fix (#973)
* Made Twitch copy link buttons links so right-click -> copy URL works.

* Added Twitch OBS screenshot to public folder.
2022-09-30 20:01:51 +01:00
James Grugett
9d81e3b6d1 Fix import 2022-09-30 13:22:10 -05:00
James Grugett
ab883ea777 Order home group sections by daily score. 2022-09-30 12:00:16 -05:00
Ian Philips
3677de58c3 Add tooltip and badge on contract for bounties 2022-09-30 10:00:55 -06:00
Ian Philips
31de3636fd Fix comment tab title 2022-09-30 09:34:58 -06:00
Ian Philips
a90b765670
Bounty comments (#944)
* Adding, awarding, and sorting by bounties

* Add notification for bounty award as tip

* Fix merge

* Wording

* Allow adding in batches of m250

* import

* imports

* Style tabs

* Refund unused bounties

* Show curreantly available, reset open to 0

* Refactor

* Rerun check prs

* reset yarn.lock

* Revert "reset yarn.lock"

This reverts commit 4606984276.

* undo yarn.lock changes

* Track comment bounties
2022-09-30 09:27:42 -06:00
Ian Philips
55f854115c Remove green circle from resolution prob input 2022-09-30 08:48:33 -06:00
Ian Philips
138f34fc66 Add close now button to contract edit time 2022-09-30 08:40:46 -06:00
Ian Philips
c16e5189f7 Don't send portfolio email to user less than 5 days old 2022-09-30 07:53:47 -06:00
Marshall Polaris
1bc1debbe8 Fix default sizes on charts to make more sense 2022-09-30 00:05:36 -07:00
Marshall Polaris
608ee7b865
Chart visual style adjustment (#971)
* Adjust area fill opacity on line charts

* Light gray border on tooltips
2022-09-30 00:03:31 -07:00
mantikoros
95c47aba1a midterms: add CO, additional markets 2022-09-30 01:30:45 -05:00
James Grugett
f892c92e26 Save portfolio sort and filter to local storage! 2022-09-30 01:11:04 -05:00
Marshall Polaris
7e91133229
Change styles on contract tooltips to be more like portfolio graph (#966) 2022-09-29 22:45:51 -07:00
Marshall Polaris
523689b525
Keep tooltip within bounds of chart (well, for non-FR charts) (#970) 2022-09-29 22:45:31 -07:00
ingawei
b83e5db563
getting rid of daisy buttons (#969)
* getting rid of daisy buttons so bet button does not turn black on mobile
2022-09-30 00:41:22 -05:00
James Grugett
13b3613460 Show number of limit orders 2022-09-29 23:57:45 -05:00
Marshall Polaris
715bae57e0
Fix date memoization in charts (#972)
* Memoize on numbers, not dates

* Use numbers instead of dates to calculate visible range
2022-09-29 21:35:20 -07:00
Marshall Polaris
5b5a919ed7
Expose onMouseOver chart event to hook into from outside (#967) 2022-09-29 20:18:33 -07:00
Ian Philips
2625ab1549 Portfolio email ux 2022-09-29 18:13:33 -06:00
ingawei
262183e0e6
Inga/quick toggle fix (#964)
getting rid of unused component
2022-09-29 18:53:36 -05:00
Sinclair Chen
b7df1a7043
Add ||spoilers|| (#942)
* Add ||spoilers||
* Add spoiler button to format menu
2022-09-29 14:28:04 -07:00
Marshall Polaris
8929b2e6ba
Improve typing for chart tooltip stuff (#962) 2022-09-29 12:51:38 -07:00
mantikoros
9fc1e855ff portfolio graph: put profit first 2022-09-29 13:53:43 -05:00
Pico2x
1755fb15d4 SEO for posts 2022-09-29 19:38:36 +01:00
Olivia Appleton
1e6b72059e
Expose multiple choice answer probabilities (#939)
* Expose multiple choice answer probabilities

* Run prettier

* Update api.md

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-29 14:17:52 -04:00
Olivia Appleton
2d1fd07834
Add documentation for newer market types (#934) 2022-09-29 13:27:07 -04:00
Ian Philips
ec1a9fab77 Show change in M$ 2022-09-29 13:06:12 -04:00
James Grugett
2cc08ba9e7 Daily movers cleanup 2022-09-29 11:42:17 -05:00
Ian Philips
35aa6c0429 Test sample of users' portfolios 2022-09-29 12:32:47 -04:00
Ian Philips
cd7ddae133 Add profit of bets made within last week 2022-09-29 12:30:58 -04:00
Sinclair Chen
46fab105d9 Fix tipper icon progression 2022-09-29 07:26:19 -07:00
Sinclair Chen
4cc985634a Put slider z-index under bottom menu 2022-09-29 07:04:11 -07:00
Marshall Polaris
15cd8b1f94
Fix a couple small chart bugs (#960)
* Fix time clamping causing little visual glitch

* Fix tick formatting glitch
2022-09-28 23:27:42 -07:00
Marshall Polaris
8862425120
Clean up chart tooltip handling (#959) 2022-09-28 21:43:04 -07:00
Marshall Polaris
be010da9f5
Refactor chart tooltip stuff, add bet avatar to tooltips (#958)
* Use objects instead of tuples for chart data

* Carry bet data down into charts

* Refactor to invert control of chart tooltip display

* Jazz up the chart tooltips with avatars

* Tidying
2022-09-28 21:14:34 -07:00
Marshall Polaris
7f7e7acd61
Make binary and pseudonumeric charts re-render less on contract diff (#955) 2022-09-28 18:03:30 -07:00
Sinclair Chen
1f2c7271b7 put edit profile z-index below side menu 2022-09-28 14:30:00 -07:00
Marshall Polaris
83de206e9e
Simply don't print zero (#954) 2022-09-28 14:20:28 -07:00
James Grugett
d55cedb36c Load comments via static props 2022-09-28 13:11:26 -04:00
James Grugett
eb762d9b9e Make loading more sequential for updateMetrics to prevent firebase error. 2022-09-28 12:28:40 -04:00
James Grugett
dba938032f Listen for updates on daily mover contract 2022-09-28 12:28:40 -04:00
ingawei
7c8e977d60
order book things (#953)
Adding order book to limit orders in mobile modal. This is pretty ugly and just a quick fix because people are complaining.
2022-09-28 09:04:47 -05:00
ingawei
e0e6838711 Auto-remove unused imports 2022-09-28 13:46:41 +00:00
ingawei
513cf7b290 added order book 2022-09-28 06:45:32 -07:00
Marshall Polaris
89c3ea559c
Clamp time range in history chart scales (#952) 2022-09-28 01:18:11 -07:00
Marshall Polaris
9238b20242
Modularize d3 imports (#951) 2022-09-28 01:00:39 -07:00
Marshall Polaris
925a9e850f
Hack up brush rendering to fix possible Chrome bug (#950) 2022-09-28 00:58:51 -07:00
Marshall Polaris
8f88af4e2a
Fix an edge case with chart mouseover tooltips (#949) 2022-09-28 00:56:43 -07:00
Marshall Polaris
5b54e7d468
Limit max width of FR legend tooltip labels (#948) 2022-09-27 22:25:37 -07:00
Pico2x
f52127237e COWP for cows 2022-09-28 01:21:38 -04:00
Pico2x
95f2604479 Cowp SEO friendly 2022-09-28 01:04:38 -04:00
Pico2x
a5b943965c Create cowp.tsx 2022-09-28 00:59:24 -04:00
Marshall Polaris
c16adb9ec9
Fix potential clock sync issues with graph updating (#947) 2022-09-27 21:18:22 -07:00
Marshall Polaris
e0d9b4d335
Rewrite contract graphs (#935)
* Fiddle around with everything, WIP FR charts

* Implement numeric chart

* Reorganize everything into neat little files

* Add `AreaWithTopStroke` helper

* Tidying, don't gratuitously use d3.format

* Remove duplicate code

* Better tooltip bisection

* `NumericPoint` -> `DistributionPoint`

* Add numeric market tooltip

* Make numeric chart bucket points less wrong

* Clean up numeric bucket computation

* Clean up a bunch of tooltip stuff, add FR legend tooltips

* Fix a dumb bug

* Implement basic time selection

* Fix fishy Date.now inconsistency bugs

* Might as well show all the FR outcomes now

* Make tooltips accurate on curveStepAfter charts

* Make log scale PN charts work properly

* Adjust x-axis tick count

* Display tooltip on charts only for mouse

* Fix up deps

* Tighter chart tooltips

* Adjustments to chart time range management

* Better date formatting

* Continue tweaking time selection handling to be perfect

* Make FR charts taller by default
2022-09-27 20:24:42 -07:00
James Grugett
9dc0d1696e Fix bug 2022-09-27 19:36:32 -04:00
James Grugett
a7abdbb1db Add to dating group 2022-09-27 19:10:35 -04:00
James Grugett
13dad9a10c Date doc: Remove photo as first-class feature 2022-09-27 19:03:14 -04:00
Austin Chen
14c008234a Script: Add liquidity to all markets in a group 2022-09-27 18:55:30 -04:00
Austin Chen
b87e29d7c0 Rename script 2022-09-27 18:55:30 -04:00
James Grugett
3ed29877ce Add dating docs to menu bar 2022-09-27 18:55:08 -04:00
mantikoros
80d4bffc95
US Elections map (#943)
* usa map

* state election map

* senate midterms

* iframe

* fix

* /midterms

* listen for updates
2022-09-27 17:50:43 -05:00
James Grugett
b21daa1248
Date docs on Manifold (#941)
* Date docs

* Create date doc

* Create and show a date market as well

* Move url to date-docs

* Date doc individual page

* Add share button

* Edit date docs

* Layout

* Add comments for create-post

* Add comments and back nav

* Fix urls

* Tweaks
2022-09-27 17:30:07 -05:00
Austin Chen
419c7ab636 Navigate to ?tab=portfolio 2022-09-27 17:16:48 -04:00
Barak Gila
e2047210b7
add to queue rather than invoking sprig object directly, as it's still being setup (#940) 2022-09-27 15:13:11 -04:00
mantikoros
5e34b5a911 greyscale bet button if outcome is undefined 2022-09-27 13:15:13 -04:00
mantikoros
723d9dbece
Better bet summary (#936)
* show position, expected value, profit instead of "invested"

* move bet summary outside trades on market page

* refactor

* pass in userbets

* hide only if no bets; show invested on desktop

* various
2022-09-27 12:09:54 -05:00
Barak Gila
7ba19c274b
basic sprig integration with possible page URL events (#932)
* basic sprig integration with possible page URL events

* iteration 0

* iteration 1

* run prettier; attempt to remove expect error

* readd expect error messages

* typescript comment fixes

* add identify

* remove package-lock.json

* extract to separate file

* fix linting

* fix lint

* fix lint

* fix missing config
2022-09-27 12:02:03 -05:00
ingawei
a12ed78813
Getting rid of console log, fixing multiple choice markets (#938) 2022-09-26 23:48:00 -05:00
mantikoros
aa93ec060d user page: put markets first 2022-09-27 00:46:30 -04:00
ingawei
fd90bc353b multiple choice betting fix 2022-09-26 21:40:56 -07:00
ingawei
e17a59ae23
Inga/mobilebetting (#911)
* mobile binary betting
2022-09-26 19:28:54 -05:00
ingawei
2fe9fe593d
Inga/profile (#937)
- Changed edit profile button
- got rid of banner
- merged stats and trades tab on profile
- made multicolored profit graph
2022-09-26 18:01:13 -05:00
Ian Philips
d612192109 Send market close notifs for each close time 2022-09-26 18:13:15 -04:00
SirSaltyy
13cffcdaf1 Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-09-26 18:12:28 -04:00
SirSaltyy
1b9811ce28 Update twitch page copy 2022-09-26 18:12:24 -04:00
Ian Philips
3ed3b6fb42 Set email sent flag if skipped over 2022-09-26 18:05:50 -04:00
Ian Philips
f7bf42d2e0 Rename & correct spelling 2022-09-26 17:54:48 -04:00
Ian Philips
df316fc4da
Portfolio update emails (#928)
* Stats computing correctly

* Styles propagating - testing in prod now

* Formatting html

* Reset portfolio flag on mondays at 12am

* Add profit, styling

* More styling, less reports

* Cleanup

* Comments

* comment

* Try to send higher signal emails

* Send emails to proper email address
2022-09-26 17:49:06 -04:00
James Grugett
2ef025a151 Only set daily score on contracts that are at least day old 2022-09-26 17:43:27 -04:00
James Grugett
90eaf83775 Redirect from '/home' to '/' if not logged in 2022-09-26 17:04:08 -04:00
Ian Philips
94ffac287e Payout resolution notifications styling 2022-09-26 15:57:38 -04:00
Ian Philips
a10e4c115e Fix dpm MULTI resolution payouts bug 2022-09-26 15:57:21 -04:00
Ian Philips
cc3b44891b Add user to market followers in create answer 2022-09-26 15:56:47 -04:00
Ian Philips
d9292f7a95 Switch order of my groups and all tabs 2022-09-26 11:30:41 -04:00
Ian Philips
bf92c4fb06 Fix 500 on non-existant group page 2022-09-26 11:28:54 -04:00
James Grugett
68120ec2b2 Revert "Clean up and fix stuff on answers panel (#914)"
This reverts commit 721448f408.
2022-09-25 23:29:13 -04:00
Marshall Polaris
be2c60d3f3
Fix some rendering issues on contract page (#933)
* Memoize calculating sale amount on your bets list

* Don't re-render more than necessary with `useIsMobile` hook

* Use `useIsMobile` hook in `AmountInput`
2022-09-25 16:43:53 -07:00
Austin Chen
c1c3a360fd Add CART contest to /tournaments 2022-09-25 13:08:34 -04:00
Austin Chen
ae4d49d960 Generate markets for the Criticism and Red Teaming contest 2022-09-25 11:29:59 -04:00
James Grugett
21c7130d3b Filter out markets with undefined probChanges in dev 2022-09-23 18:58:05 -04:00
Marshall Polaris
d990bc2f07
Remove images config from next.config.js (#931) 2022-09-23 14:55:27 -07:00
Marshall Polaris
e2a8df6c3a
Nivo 0.74.0 -> 0.80.0 (#929) 2022-09-23 14:55:17 -07:00
Marshall Polaris
96dc060a0a
Move react-masonry-css dependency to web package.json (#930) 2022-09-23 14:55:06 -07:00
Austin Chen
d04304bdac Fix blank page on nav to groups 2022-09-23 17:04:32 -04:00
Austin Chen
2891a47d8c Support navigating to /about pages 2022-09-23 16:49:14 -04:00
James Grugett
490734db00 If no user, show loading on home 2022-09-23 16:43:23 -04:00
James Grugett
77ddc456a2 Add new home section to top. 2022-09-23 16:39:17 -04:00
James Grugett
1a5dcdedcc Delay prefetch by 1000ms. Don't prefetch portfolio history. 2022-09-23 16:30:44 -04:00
James Grugett
0ab82a7bd4 Delete some unused code 2022-09-23 15:40:48 -04:00
James Grugett
deb8397ee9 Add Daily Trending section (daily-score for you.) Remove recently updated 2022-09-23 15:33:50 -04:00
James Grugett
57190e7876 Daily trending sort option 2022-09-23 15:33:50 -04:00
FRC
5a10132e2b
Add a "Posts" tab to groups (#926)
* Add a "Posts" sidebar item to groups

* Fix James's nits

* Show "Add Post" button only to users
2022-09-23 15:11:50 -04:00
Sinclair Chen
ebcecd4fe9 remove unused files 2022-09-23 15:01:48 -04:00
Austin Chen
61a9224a7d Move Civid Dashboard and Research.Bet to Alumni 2022-09-23 12:10:48 -04:00
Austin Chen
47c97c36db Add Alignment Markets to Awesome Manifold 2022-09-23 12:01:33 -04:00
Ian Philips
5483955590 Remove contractId from required JSON for /close 2022-09-23 10:21:11 -04:00
Ian Philips
91f89ccb3d Add docs fo /close, allow to pass closeTime 2022-09-23 10:14:41 -04:00
Ian Philips
08202c3ede Add close market endpoint 2022-09-23 10:02:40 -04:00
jahooma
70bc5b2c4a Auto-prettification 2022-09-22 21:58:40 +00:00
James Grugett
c6d034545a
Home: Prob change cards. Sort by daily score. (#925)
* Add dailyScore: product of unique bettors (3 days) and probChanges.day

* Increase memory and duration of scoreContracts

* Home: Smaller prob change card for groups. Use dailyScore for sort order (algolia)

* Add back hover
2022-09-22 16:57:48 -05:00
Sinclair Chen
eaaa46294a fix empty comment send button style 2022-09-22 17:07:51 -04:00
Sinclair Chen
2240db9baa fix profile tab styling 2022-09-22 16:24:57 -04:00
Marshall Polaris
a1c3d0a2dd
Fix up comment permalink stuff (#915)
* Eliminate needless state/effects to highlight comments

* Scroll to comment on render if highlighted
2022-09-22 12:58:40 -07:00
Marshall Polaris
7704de6904
Next.js 12.2.5 -> 12.3.1 (#922) 2022-09-22 12:46:48 -07:00
Marshall Polaris
721448f408
Clean up and fix stuff on answers panel (#914) 2022-09-22 12:40:55 -07:00
Marshall Polaris
6ee8d90bdb
Eliminate redundant showReply/replyTo state (#917) 2022-09-22 12:40:44 -07:00
Marshall Polaris
6fe0a22a48
Improve contract leaderboard computation (#918)
* Fix and clean up top comment stuff

* Make leaderboard code generic on entry type

* No need to look up users on contract leaderboard
2022-09-22 12:40:27 -07:00
mantikoros
b9fffcfa30 sort: add back 24h volume, remove most traded 2022-09-22 14:20:44 -04:00
mantikoros
0c0e7b5582 Auto-prettification 2022-09-22 18:02:17 +00:00
mantikoros
06db5515f6 add qr code to share dialog 2022-09-22 14:01:37 -04:00
FRC
a5e293c010
Bring back tabs in groups (#923) 2022-09-22 12:12:53 -04:00
Sinclair Chen
4412d0195c
Add tooltips to market header icons (#924) 2022-09-22 11:53:55 -04:00
mantikoros
c15285aa64 pare down sorts; only show high/low prob on groups 2022-09-22 00:32:20 -04:00
Austin Chen
9ff2b62740 Remove console log 2022-09-21 23:10:25 -04:00
Sinclair Chen
e9ab234d61 copy: manifold dollars -> mana 2022-09-21 17:49:32 -07:00
ingawei
7988fdde60
simplify binary graphs (#921) 2022-09-21 18:49:20 -05:00
Pico2x
b875ac563d Revert "Bring back tabs in groups (#919)"
This reverts commit b4a59cfb21.
2022-09-21 19:14:05 -04:00
FRC
b4a59cfb21
Bring back tabs in groups (#919) 2022-09-21 18:27:49 -04:00
Austin Chen
d922900bda Increase tip size to M$10 2022-09-21 18:25:56 -04:00
ingawei
24766740c5
cleaning up search bar for mobile (#916)
* cleaning up search bar for mobile
2022-09-21 16:48:32 -05:00
Austin Chen
73fad2e34b Remove F2P Tournament 2022-09-21 15:31:45 -04:00
Marshall Polaris
a10605e74c
More work on contract page and tabs (#912)
* Consolidate comment thread component code

* Move `visibleBets` work down into bets tab

* Remove unnecessary cruft from contract page props

* Don't load all comments in contract page static props anymore

* Tidy up props a bit

* Memoize bets tab

* Memoize recommended contracts widget
2022-09-21 00:02:10 -07:00
Marshall Polaris
c7f29af2ee
Clean up some stuff in AnswersPanel (#902)
* Tidy up messy markup on FR answers panel

* Clean up obsolete feed-related answer stuff

* Slight fixup per James feedback
2022-09-20 22:07:40 -07:00
James Grugett
ea1579975c Increase memory of update functions 2022-09-20 23:56:14 -05:00
Marshall Polaris
6e2aa622ab
Refactor, improve efficiency of contract tabs stuff (#909)
* Move comments and tips fetching down into comments tab rendering

* Consolidate `contract-activity.tsx` into `contract-tabs.tsx`

* Move LP fetching into bets tab
2022-09-20 21:02:17 -07:00
mantikoros
54778ec1b1
Fix twitch onboarding (#910)
* don't show welcome dialog for twitch users

* handle sign up race conditions with more hooks

* content organization and copy tweaks

* lint

* fix import
2022-09-20 19:23:18 -05:00
Marshall Polaris
8870f0d356
Don't always require tips to render comments (#898) 2022-09-20 15:58:47 -07:00
Marshall Polaris
be4def49a2
Kill counts of comments and trades on contract page (#900) 2022-09-20 15:53:35 -07:00
James Grugett
589bf9651d Track viewing full daily movers 2022-09-20 17:40:31 -05:00
Marshall Polaris
60c79141aa
Move comment-bet association code into comment creation trigger (#899)
* Move comment-bet association code into comment creation trigger

* Add index for new comments query
2022-09-20 15:25:58 -07:00
Marshall Polaris
faaf502114
Remove old check in free answer comment rendering (#906) 2022-09-20 14:11:26 -07:00
Marshall Polaris
30ce80d0c9
Extract signup UI from contract tabs component (#901) 2022-09-20 14:04:07 -07:00
Marshall Polaris
8145b128ad
Move recommended contracts to own widget (#896) 2022-09-20 14:03:52 -07:00
Marshall Polaris
a2d9e8e3d2
Cleanup free answer comment stuff (#897)
* Remove unused most-recent-bet-time stuff

* Remove strange reply box hiding behavior

* Tidying markup
2022-09-20 14:03:33 -07:00
mantikoros
106dc232b8 send market guide onboarding email after 96 hrs 2022-09-20 16:03:17 -05:00
mantikoros
379e736e51
hide liquidity panel (#904) 2022-09-20 15:57:27 -05:00
mantikoros
8920241c39 space out onboarding emails 2022-09-20 15:56:28 -05:00
mantikoros
ac952f1164 Revert "Don't send creator guide email & interesting markets on create user"
This reverts commit a4399aaee9.
2022-09-20 15:49:46 -05:00
Ian Philips
6d7fbd69c7 Lint 2022-09-20 12:17:37 -04:00
Ian Philips
a4399aaee9 Don't send creator guide email & interesting markets on create user 2022-09-20 12:15:23 -04:00
Ian Philips
6c3338f5d7 Remove unused unsubscribe attributes 2022-09-20 09:59:48 -04:00
Ian Philips
272ba921a0 Add memory to weekly email functions 2022-09-20 09:45:14 -04:00
Pico2x
fdd7dcc0ab Rm group about/short description 2022-09-20 14:42:41 +01:00
Ian Philips
5ab86c8362 Check new weekly email notification preferences 2022-09-20 09:36:44 -04:00
Ian Philips
c6a60a6678 Streak & uniques bonus in transaction 2022-09-20 08:42:09 -04:00
James Grugett
62f20694bf Exclude resolved from daily movers 2022-09-20 00:49:25 -05:00
James Grugett
c338dce3ce Add daily activation rate. Remove top tenth actions. Cleanup 2022-09-20 00:10:05 -05:00
marsteralex
44deaf7b0a
WIP: add artist category (#866)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

* remove generated cards from digital commanders

* fix counterspell setting default

* added difficulty rating

* updated padding

* add dfc support for commanders

* add artists

* use latest non-digital if possible

* change vsCode settings for python

* update with latest non-digital printing

* update artist list

* update algo to select k samples

* cleanup code

* equally weight artists

* weight everything equally

* updated for all artists

* update artists

* add allowlist

* update artists to min 50 art

* allow promo to be replaced

* update jsons

* update with min 100 arts

* update code to be smaller jsons

* updated to 18 artists per game

* update ui

* update importing artists

* update to 21

* move num artists to top of js file

* update artistList to not include artist sigs

* update to 50 artists

* update for ub

* update artist list

* update ub defaults

* update jsons

* allow non-english cards to be replaced

* update allowlist

* update jsons

* add watermark

* update jsons

* update jsons

* make jsons slightly smaller

* add checkmarks and x's

* remove python

* add no answer and checkbox and x

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-19 18:10:14 -07:00
James Grugett
4dc3eada1f Add more stats. Fix timezone. Group retention and new user retention 2022-09-19 18:41:24 -05:00
James Grugett
d0973de2b4 Use percent chart, save unrounded stats 2022-09-19 17:45:52 -05:00
James Grugett
b4244ea75d Change stats date to be by Pacific time zone 2022-09-19 17:10:12 -05:00
mantikoros
a2b01e28c9 daily movers: filter out numeric markets 2022-09-19 16:29:21 -05:00
James Grugett
935c550733 Revalidate static props on market resolve 2022-09-19 16:26:34 -05:00
jahooma
c101337c38 Auto-remove unused imports 2022-09-19 21:25:50 +00:00
James Grugett
f3ff6d99c8 Remove colored background for daily movers to make it not feel like your own profit / loss 2022-09-19 16:24:36 -05:00
James Grugett
de8e4df04c Revalidate static props for new comments 2022-09-19 16:07:27 -05:00
James Grugett
1a82ce193d Add node-fetch, add secret to function config 2022-09-19 15:25:16 -05:00
James Grugett
fb27fac524 Revalidate getStaticProps after each bet 2022-09-19 14:55:37 -05:00
James Grugett
6f5d69ec9c Increase memory of updateMetrics function 2022-09-19 14:34:48 -05:00
mantikoros
55a68d4fec email wording 2022-09-19 14:22:04 -05:00
mantikoros
24cf42284f replace "predictor" => "trader" 2022-09-19 14:03:52 -05:00
FRC
6aa45a2d12
Move group navbar to top (#895) 2022-09-19 17:29:17 +01:00
Ian Philips
5d65bb5bb1 Add message about unique bonuses withdrawn on n/a 2022-09-19 07:31:04 -06:00
Marshall Polaris
bfe00595e7
Make comments with bet outcome but no answer outcome appear (#894) 2022-09-19 00:53:10 -07:00
James Grugett
b93af31d2f Add D1 and W1 (new users) to stats 2022-09-19 01:28:18 -05:00
Marshall Polaris
a9e5020904
Render free response comment threads more simply without bets (#893) 2022-09-18 19:16:48 -07:00
Marshall Polaris
58dcbaaf6e
Precalculate and store current positions for users who make comments (#878) 2022-09-18 15:57:50 -07:00
mantikoros
e37b805b49 disable liquidity bonus (for now) 2022-09-18 17:49:29 -05:00
James Grugett
8ebf829169 Change groups default sort to Trending 2022-09-18 17:16:13 -05:00
James Grugett
56b4889b94 Add user.homeSections to firestore rules 2022-09-18 17:00:35 -05:00
James Grugett
17453e5618 Improve hook that was spamming in dev 2022-09-18 16:57:20 -05:00
James Grugett
ae6437442b Fix unsaved changes warning erronously appearing 2022-09-18 16:36:45 -05:00
James Grugett
373cfc5d10 Format firestore /group rules 2022-09-18 16:23:09 -05:00
mantikoros
540915eb65 homepage: fix betting streaks error 2022-09-18 16:05:22 -05:00
James Grugett
f111d6e24f Fix console errors from svg non-camelcase attributes 2022-09-18 15:55:39 -05:00
James Grugett
676bcc159d Fix missing key 2022-09-18 15:53:04 -05:00
James Grugett
1da4373335 Creating a group from create market adds it immediately 2022-09-18 14:24:29 -05:00
James Grugett
c9e782faa7 Simplify create group dialog 2022-09-18 13:49:28 -05:00
Marshall Polaris
39119a3419
Twitch bot deployment work (#892)
* Point at production Twitch bot endpoint

* Move Twitch endpoints into env config
2022-09-18 01:13:10 -07:00
James Grugett
65166f2fcb Fix import 2022-09-18 01:10:34 -05:00
James Grugett
eb021f30f5 Fix loans (user without a portfolio throws error) 2022-09-18 01:05:55 -05:00
James Grugett
4aea3b96d7 Save initial home sections for new users 2022-09-17 23:58:18 -05:00
James Grugett
987274ad2d Fix bug part 2 2022-09-17 20:01:00 -05:00
James Grugett
2166169608 Fix bug 2022-09-17 19:56:20 -05:00
James Grugett
d8e9e7812a Don't show Daily movers if there are none. Threshold is 1% 2022-09-17 19:47:04 -05:00
James Grugett
3bddda37d2 Add plus to trending group button 2022-09-17 19:25:19 -05:00
jahooma
42f66b11f4 Auto-prettification 2022-09-18 00:20:50 +00:00
James Grugett
436646cc47 Use algolia to fetch daily movers so it's faster. 2022-09-17 19:18:48 -05:00
James Grugett
a14e7d3947 Move Algolia bits to own file in web/lib/service 2022-09-17 19:18:48 -05:00
mantikoros
47cc313aef add back leaderboards link 2022-09-17 19:15:26 -05:00
mantikoros
44f9a1faa2 fix labels 2022-09-17 19:12:44 -05:00
mantikoros
f71791bdd5 fix labels 2022-09-17 19:10:34 -05:00
James Grugett
350ab35856 Tweak padding 2022-09-17 18:52:14 -05:00
mantikoros
37cff04e39 share dialog styling 2022-09-17 18:49:24 -05:00
James Grugett
e7ed893b78 Round prob in Daily movers 2022-09-17 18:45:40 -05:00
mantikoros
8f30ef38d9 fix imports 2022-09-17 18:40:45 -05:00
James Grugett
1fbadf8181 Improve Customize UI 2022-09-17 18:30:29 -05:00
mantikoros
438c12da57 refactor sidebar; add to mobile navbar 2022-09-17 18:26:02 -05:00
James Grugett
191ec9535c Show more rows on daily movers all 2022-09-17 18:00:24 -05:00
James Grugett
b74fd57912 Show absolute prob in daily movers as well 2022-09-17 17:58:08 -05:00
James Grugett
a54f060ccb New for you => New 2022-09-17 15:15:37 -05:00
mantikoros
fdde57e334 'predictor' => 'trader' 2022-09-17 15:10:16 -05:00
mantikoros
fde90be5a2 fix resolve prob notification text 2022-09-17 15:01:45 -05:00
mantikoros
d2471e2a02 group selector dialog: loading indicator, tracking 2022-09-17 14:59:02 -05:00
James Grugett
f35799c129 Only autofocus search if no query params set 2022-09-17 14:54:55 -05:00
mantikoros
6a21067440 update /about redirect 2022-09-17 14:46:59 -05:00
mantikoros
340b21c53e halve referral bonus 2022-09-17 14:38:52 -05:00
mantikoros
fc5807ebbe halve MAX_QUESTION_LENGTH 2022-09-17 14:38:52 -05:00
Austin Chen
e0806cf0e0 Fix links to group /about and /leaderboards 2022-09-16 20:36:52 -07:00
James Grugett
94c448ee8b Most predictions => Most traded 2022-09-16 17:43:27 -05:00
Sinclair Chen
3e9f046a29 Always focus search bar on search page 2022-09-16 15:37:52 -07:00
James Grugett
9340d827d9 Replace new with recently updated 2022-09-16 17:29:46 -05:00
James Grugett
612066d96c Tweak margin 2022-09-16 17:26:00 -05:00
James Grugett
015e86afcb Add search to bottom nav bar. Add back Home title to home page. 2022-09-16 17:24:31 -05:00
James Grugett
7c710ba598 Rename file nav-bar to bottom-nav-bar 2022-09-16 17:24:31 -05:00
Sinclair Chen
5b8fc12163
Make add group button same height as group names (#890) 2022-09-16 15:21:07 -07:00
James Grugett
ab3ed3fbf1 Save last sort to local storage 2022-09-16 17:09:12 -05:00
James Grugett
22d5c74818 Add search nav items 2022-09-16 16:46:24 -05:00
James Grugett
1321139e7f Prefetch daily movers 2022-09-16 16:35:04 -05:00
mantikoros
70ef9e1836 group sidebar, navbar tweaks 2022-09-16 16:30:06 -05:00
James Grugett
e4cfd92bb2 Track home actions 2022-09-16 16:21:13 -05:00
James Grugett
25ee793208
🏠 New home (#889)
* Factor out section header

* Remove daily balance change

* Remove dead code

* Layout, add streak

* Fix visibility observer to work on server

* Tweak

* Search perserved by url

* Add pill query param

* Add search page

* Extract component for ProbChangeRow

* Explore groups page

* Add search row

* Add trending groups section

* Add unfollow option for group

* Experimental home: accommodate old saved sections.

* Tweaks to search layout

* Rearrange layout

* Daily movers page

* Add streak grayed out indicator

* Use firebase query instead of algolia search for groups

* Replace trending group card with pills

* Hide streak if you turned off that notification

* Listen for group updates

* Better UI for adding / removing groups

* Toast feedback for join/leave group. Customize button moved to bottom.

* Remove Home title

* Refactor arrange home

* Add new for you section

* Add prefetch

* Move home out of experimental!

* Remove unused import

* Show non-public markets from group
2022-09-16 16:12:24 -05:00
mantikoros
f7164ddd7d
group selector dialog (#888)
* group selector dialog

* cache groups to prevent ui jumping

* welcome dialog display logic

* show fewer groups, more filtering
2022-09-16 14:58:36 -05:00
Sinclair Chen
dd2b09830e update Clearer Thinking end date to Sep 30 2022-09-16 12:13:45 -07:00
Phil
52ecd79736
Twitch prerelease (#887)
* Bot linking button functional.

* Implemented initial prototype of new Twitch signup page.

* Removed old Twitch signup page.

* Moved new Twitch page to correct URL.

* Twitch account linking functional.

* Fixed charity link.

* Changed to point to live bot server.

* Slightly improve spacing and alignment on Twitch page

* Tidy up, handle some errors when talking to bot

* Seriously do the thing where Twitch link is hidden by default

* Fixed secondary Get Started button. Copy overlay and dock link now functional.

* Add/remove bot from channel working.

* Removed legacy Twitch controls from user profile.

* Links provided by dock/overlay buttons are now correct.

* Minor profile cleanup post merge.

* Fixed unnecessary relinking Twitch account when logging in on Twitch page.

* Added confirmation popup to refresh API key. Refreshing API key now requires a user to relink their Twitch account.

* Removed legacy twitch-panel.tsx

Co-authored-by: Marshall Polaris <marshall@pol.rs>
2022-09-16 08:43:49 -07:00
IanPhilips
c316d49957 Auto-prettification 2022-09-16 15:30:09 +00:00
Ian Philips
68f2277def Just put 'you' on mobile 2022-09-16 09:28:39 -06:00
Ian Philips
a2d912bb5a Add more info to limit order notif 2022-09-16 09:02:58 -06:00
Ian Philips
c183315d52 Don't notify of updated close time when resolving market 2022-09-16 08:15:16 -06:00
Ian Philips
6a5873f8d4 Try more restrictive detault notification settings 2022-09-16 07:43:27 -06:00
FRC
456aed467c
Move tabs to sidebar (#873)
* Move tabs to sidebar

* Address all feedback

Fix icon names
Extract navbar component into a separate function
Rm arrow and indentation
Move group name under logo
Fix visual sidebar stretchy thing
Fix visual bug

* Extra nits
2022-09-16 14:32:15 +01:00
ingawei
256fd89fd2
market close fix oopsies (#886)
* market close fix
2022-09-16 02:38:09 -05:00
Phil
833ec518b4
Twitch prerelease (#882)
* Bot linking button functional.

* Implemented initial prototype of new Twitch signup page.

* Removed old Twitch signup page.

* Moved new Twitch page to correct URL.

* Twitch account linking functional.

* Fixed charity link.

* Changed to point to live bot server.

* Slightly improve spacing and alignment on Twitch page

* Tidy up, handle some errors when talking to bot

* Seriously do the thing where Twitch link is hidden by default

Co-authored-by: Marshall Polaris <marshall@pol.rs>
2022-09-16 00:22:13 -07:00
Austin Chen
1321b95eb1
%mentions for embedding contract card, take 2 (#884)
* Revert "Revert "Use %mention to embed a contract card in rich text editor (#869)""

This reverts commit e0634cea6d.

* Overwrite name to prevent breakages

* Fix '%' mentioning if you escape out

* Cleanup: merge render functions
2022-09-15 23:37:17 -07:00
Austin Chen
ca4a2bc7db Remove console log 2022-09-15 23:00:58 -07:00
mantikoros
430ad1acb0 "unique bettors"; "Unknown" => "0" 2022-09-15 23:18:27 -05:00
ingawei
1ce989f3d6
Inga/bettingfix embedfix (#885)
* Revert "Revert "Inga/bettingfix (#879)""
This reverts commit 176acf959f.
* added embed fix
2022-09-15 19:41:25 -05:00
mantikoros
5a1cc4c19d getCpmmInvested: fix NaN issue 2022-09-15 18:32:38 -05:00
James Grugett
e0634cea6d Revert "Use %mention to embed a contract card in rich text editor (#869)"
This reverts commit 140628692f.
2022-09-15 18:19:22 -05:00
Sinclair Chen
ebbb8905e2
Add clearer thinking Regrant to tournaments (#883) 2022-09-15 16:05:56 -07:00
Austin Chen
140628692f
Use %mention to embed a contract card in rich text editor (#869)
* Bring up a list of contracts with @

* Fix hot reload for RichContent

* Render contracts as half-size cards

* Use % as the prompt; allow for spaces

* WIP: When there's no matching question, create a new contract

* Revert "WIP: When there's no matching question, create a new contract"

This reverts commit efae1bf715.

* Rename to contract-mention

* WIP: Try to merge in @ and % side by side

* Add a different pluginKey

* Track the prosemirror-state dep
2022-09-15 15:12:26 -07:00
Ian Philips
e9fcf5a352 Space 2022-09-15 16:12:05 -06:00
Ian Philips
3362b2f953 Capitalize 2022-09-15 15:51:39 -06:00
Ian Philips
61c672ce4c Show negative payouts 2022-09-15 15:50:26 -06:00
Ian Philips
7628713c4b Enrich contract resolved notification 2022-09-15 15:25:19 -06:00
Marshall Polaris
b903183fff
Paginate contract bets tab (#881)
* Apply pagination to bets list on contract

* Make contract trades tab number actually match number of entries
2022-09-15 13:47:07 -07:00
Austin Chen
1476f669d3 Fix capitalization 2022-09-15 13:45:51 -07:00
Ian Philips
8c6a40bab7 Enrich limit order notification 2022-09-15 13:39:46 -06:00
Sinclair Chen
69c2570ff9 fix copy to make clear referrals aren't limited 2022-09-15 12:29:57 -07:00
Ian Philips
b3e6dce31e Capitalize 2022-09-15 09:57:14 -06:00
Ian Philips
be91d5d5e0 Avatars don't link during contract selection 2022-09-15 09:51:52 -06:00
Ian Philips
e9f136a653 Single source of truth for predict 2022-09-15 09:12:56 -06:00
Ian Philips
4c10c8499b Take back unique bettor bonuses on N/A 2022-09-15 09:12:44 -06:00
Pico2x
718218c717 Update bet-inline.tsx 2022-09-15 15:51:14 +01:00
Pico2x
772eeb5c93 Update [contractSlug].tsx 2022-09-15 15:45:49 +01:00
Ian Philips
ada9fac343 Add logs to on-create-bet 2022-09-15 08:07:42 -06:00
Ian Philips
733d206517 Add txn types 2022-09-15 07:50:35 -06:00
Ian Philips
4a5c6a42f6 Store bonus txn data in data field 2022-09-15 07:45:11 -06:00
Ian Philips
e5428ce525 Watch market modal copy 2022-09-15 07:14:59 -06:00
Pico2x
176acf959f Revert "Inga/bettingfix (#879)"
This reverts commit 8aaaf5e9e0.
2022-09-15 13:55:57 +01:00
ingawei
8aaaf5e9e0
Inga/bettingfix (#879)
* making betting action panels much more minimal, particularly for mobile
* added tiny follow button
2022-09-15 01:46:58 -05:00
ingawei
ccf02bdba8
Inga/admin rules resolve (#880)
* Giving admin permission to resolve all markets that have closed after 7 days.
2022-09-14 22:28:40 -05:00
Ian Philips
9aa56dd193 Only show prev opened notif setting section 2022-09-14 17:25:17 -06:00
Ian Philips
3efd968058 Allow one-click unsubscribe, slight refactor 2022-09-14 17:17:32 -06:00
Sinclair Chen
68b0539fc1 Enable search exclusion and exact searches
like `-musk` to remove Elon results or `"eth"` for Ethereum results
2022-09-14 15:06:11 -07:00
Sinclair Chen
7aaacf4d50 Center tweets 2022-09-14 13:19:12 -07:00
Ian Philips
050bd14e46 Update script 2022-09-14 10:29:48 -06:00
Ian Philips
7ba2eab65e Rename user notification preferences 2022-09-14 10:26:08 -06:00
Ian Philips
edbae16c8e Betting streak reset indicator 2022-09-14 08:56:05 -06:00
Ian Philips
d6b0a1edc0 Betting streak reset to 7am UTC and store streak data on notif 2022-09-14 07:27:20 -06:00
mantikoros
a2d61a1daa
Twitch integration (#815)
* twitch account linking; profile page twitch panel; twitch landing page

* fix import

* twitch logo

* save twitch credentials cloud function

* use user id instead of bot id, add manifold api endpoint

* properly add function to index

* Added support for new redirect Twitch auth.

* Added clean error handling in case of Twitch link fail.

* remove simulator

* Removed legacy non-redirect Twitch auth code. Added "add bot to channel" button in user profile and relevant data to user type.

* Removed unnecessary imports.

* Fixed line endings.

* Allow users to modify private user twitchInfo firestore object

* Local dev on savetwitchcredentials function

Co-authored-by: Phil <phil.bladen@gmail.com>
Co-authored-by: Marshall Polaris <marshall@pol.rs>
2022-09-14 01:52:31 -07:00
Marshall Polaris
7144e57c93
Denormalize user display fields onto bets (#853)
* Denormalize user display fields onto bets

* Make bet denormalization script fast enough to run it on prod

* Make `placeBet`/`sellShares` immediately post denormalized info
2022-09-14 01:33:59 -07:00
Marshall Polaris
1ebb505752
Fix liquidity feed display to look right (#877) 2022-09-14 01:13:53 -07:00
mantikoros
273b815e54 hide house liquidity on feed 2022-09-14 00:51:43 -05:00
mantikoros
e7d8cfe7e0
House liquidity (#876)
* add house liquidity for unique bettors

* hide notifications from house liquidity

* up bonus liquidity to  M$20
2022-09-14 00:26:47 -05:00
mantikoros
be851b8382 fix typo 2022-09-13 21:23:36 -05:00
mantikoros
58ef43a8ec intro panel: use gradient image 2022-09-13 21:12:01 -05:00
Ian Philips
f6feacfbc9 Fix lint and persistent storage key 2022-09-13 17:18:16 -06:00
Sinclair Chen
74335f2b01
Adjust market modal styles (#875)
* Refactor add market modals into one component
* Adjust style: stickier search, scroll auto
2022-09-13 16:16:07 -07:00
Ian Philips
df3d7b591d Componentize notification line setting, don't use useEffect 2022-09-13 17:00:34 -06:00
James Grugett
c9d323c83f
Small updates to experimental/home (#874)
* Factor out section header

* Remove daily balance change

* Remove dead code

* Layout, add streak

* Fix visibility observer to work on server

* Tweak

* Formatting
2022-09-13 17:47:29 -05:00
Ian Philips
34bad35cb8 Betting=>predicting 2022-09-13 16:19:52 -06:00
Ian Philips
c423326270 Send users emails when they hit 1 and 6 unique bettors 2022-09-13 16:12:53 -06:00
Ian Philips
4398fa9bda Add new market from followed user email notification 2022-09-13 09:54:51 -06:00
Ian Philips
2c922cbae6 Send no-bet resolution emails to those without bets 2022-09-13 08:16:23 -06:00
Ian Philips
55b895146b Find multiple choice resolution texts as well 2022-09-13 07:54:37 -06:00
Ian Philips
8b1776fe3b Remove contracts number badge from groups tab 2022-09-13 07:53:01 -06:00
Ian Philips
de8c27c970 Filter None answer earlier 2022-09-13 07:48:41 -06:00
James Grugett
483838c1b2 Revert "Make parse.richTextToString more efficient (#848)"
This reverts commit cb143117e5.
2022-09-12 19:06:37 -05:00
Marshall Polaris
cb143117e5
Make parse.richTextToString more efficient (#848) 2022-09-12 16:11:03 -07:00
Sinclair Chen
22d2248951
Add floating menu (bold, italic, link) (#867)
* Add floating menu (bold, italic, link)
* Sanitize and href-ify user input
2022-09-12 16:10:32 -07:00
Ian Philips
2351403674 Replies to answers are comments 2022-09-12 17:04:06 -06:00
Ian Philips
018eb8fbfc Send notif to all users in reply chain as reply 2022-09-12 17:01:59 -06:00
James Grugett
f49cb9b399 Only show 'Show more' for free response answers if there are more answers to show 2022-09-12 17:40:19 -05:00
mantikoros
d66a81bc6b Auto-prettification 2022-09-12 22:35:32 +00:00
mantikoros
8e41b39936 landing page: use next image for logo 2022-09-12 17:34:13 -05:00
mantikoros
0e5b1a7742 market intro panel 2022-09-12 17:30:51 -05:00
mantikoros
3d3caa7a42 remove comment bet area 2022-09-12 16:50:38 -05:00
Pico2x
a3da8a7c3c Make update-metrics actually write cached group leaderboards 2022-09-12 22:01:37 +01:00
Pico2x
2a96ee98f4 Fix type error in update metrics pt.3 2022-09-12 21:49:15 +01:00
Ian Philips
5c6fe08bdb Website => Web 2022-09-12 14:48:42 -06:00
Ian Philips
747d5d7c7c In app => website 2022-09-12 14:48:16 -06:00
Ian Philips
3a814a5b5d Detect just settings tab w/o section 2022-09-12 14:41:30 -06:00
Ian Philips
e35c0b3b52 Only notify followers of new public markets 2022-09-12 14:36:54 -06:00
Ian Philips
0af1ff112b Allow users to see 0% FR answers via show more button 2022-09-12 14:30:15 -06:00
Pico2x
4456a771fd fix type error in update-metrics pt.2 2022-09-12 21:25:45 +01:00
Ian Philips
86422f90ea Set all overflow notifs to seen 2022-09-12 14:17:39 -06:00
Pico2x
7d9908dbd0 Fix type error in update-metrics 2022-09-12 20:58:12 +01:00
FRC
ff81b859d1
"Fix "500 internal error" in large groups (#872)
* Fix "500 internal error" in large groups (#856)

This reverts commit 28f0c6b1f8.

* Ship without touching prod and with some logs.
2022-09-12 20:54:11 +01:00
Ian Philips
3cb36a36ec Separate email and browser ids list 2022-09-12 11:00:24 -06:00
James Grugett
4f19220778 Experimental home: accommodate old saved sections. 2022-09-12 11:56:20 -05:00
Ian Philips
5c6328ffc2
[WIP] Fully customizable notifications (#860)
* Notifications Settings page working

* Update import

* Linked notification settings to notification rules

* Add more subscribe types

* It's alive... It's alive, it's moving, it's alive, it's alive, it's alive, it's alive, IT'S ALIVE'

* UI Tweaks

* Clean up comments

* Direct & highlight sections for notif mgmt from emails

* Comment cleanup

* Comment cleanup, lint

* More comment cleanup

* Update email templates to predict

* Move private user out of getDestinationsForUser

* Fix resolution messages

* Remove magic

* Extract switch to switch-setting

* Change tab in url

* Show 0 as invested or payout

* All emails use unsubscribeUrl
2022-09-12 10:34:56 -06:00
FRC
28f0c6b1f8
Revert "Fix "500 internal error" in large groups (#856)" (#871)
This reverts commit a6ed8c9228.
2022-09-12 17:26:46 +01:00
FRC
a6ed8c9228
Fix "500 internal error" in large groups (#856)
* Members to memberIds

* Moved to update-metrics
2022-09-12 16:44:24 +01:00
James Grugett
c1287a4a25
Small updates to experimental home (#870)
* Line clamp question in prob change table

* Tweaks

* Expand option for daily movers

* Snap scrolling for carousel

* Add arrows to section headers

* Remove carousel from experimental/home

* React querify fetching your groups

* Edit home is its own page

* Add daily profit and balance

* Merge branch 'main' into new-home

* Make experimental search by your followed groups/creators

* Just submit, allow xs on pills

* Weigh in

* Use next/future/image component to optimize avatar images

* Inga/challenge icon (#857)

* changed challenge icon to custom icon
* fixed tip button alignment

* weighing in and trading "weigh in" for "trade"

* Delete closing soon, mark new as New for you, trending is site-wide

* Delete your trades. Factor out section item

* Don't allow hiding of home sections

* Convert daily movers into a section

* Tweaks for loading daily movers

* Prob change table shows variable number of rows

* Fix double negative

Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-09-12 00:39:04 -05:00
Sinclair Chen
f8d346a404 Clean up charity styles
- center cards on mobile
- make notes more professional
2022-09-11 11:42:05 -07:00
mantikoros
93033b5b24 Revert "Yes and no buttons on contract page (#868)"
This reverts commit b39e0f304f.
2022-09-10 21:57:50 -05:00
mantikoros
18815caed4 Revert "Put sale value above quick bet button"
This reverts commit 9ee7173305.
2022-09-10 21:57:35 -05:00
Ian Philips
9ee7173305 Put sale value above quick bet button 2022-09-10 17:48:35 -06:00
Ian Philips
b39e0f304f
Yes and no buttons on contract page (#868)
* Yes and no buttons on contract page

* Cheating by adding 0.05 to max shares but gives better quickbet UX
2022-09-10 17:07:23 -06:00
mantikoros
e17234ecce typo 2022-09-10 17:43:52 -05:00
Austin Chen
33bcc1a65e Clean up /tournaments styling 2022-09-10 12:00:01 -07:00
Austin Chen
e61591622e Feature a few other semi-tournaments 2022-09-10 11:50:03 -07:00
jahooma
11ba65ec4a Auto-remove unused imports 2022-09-09 22:43:33 +00:00
FRC
26f83ac4f6
Adds investmentValue to group leaderboard calculation (#855)
* Adds investmentValue to group leaderboard calculation

* Initial investment is no longer counted, only the profit

* Simplify scoring calculation

* Remove console.log

* Group bets by user first

Co-authored-by: James Grugett <jahooma@gmail.com>
2022-09-09 17:42:51 -05:00
akrolsmir
cca870ced5 Auto-remove unused imports 2022-09-09 21:27:16 +00:00
Austin Chen
fdf123b875 Remove console.logs from common code
This makes it easier to debug in local; we shouldn't be checking in console.log into the codebase, as a general rule
2022-09-09 14:26:23 -07:00
Austin Chen
a737ae9f46 Link to tournament /about pages 2022-09-09 14:14:22 -07:00
mantikoros
43660387fa modal positioning 2022-09-09 16:08:42 -05:00
mantikoros
7729bdd2dc bet panel: higher threshold for warning; no autofocus on mobile 2022-09-09 15:58:26 -05:00
marsteralex
1ae0f0e273
add dfc support to commanders (#865)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

* remove generated cards from digital commanders

* fix counterspell setting default

* added difficulty rating

* updated padding

* add dfc support for commanders

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-09 13:54:54 -07:00
Austin Chen
18466afc78 Fix API URLs from non "manifold.markets" domains 2022-09-09 11:43:23 -07:00
Austin Chen
4c801f76b4
Submit comments on ctrl/cmd-enter (#830)
* Submit comments on ctrl/cmd-enter

* Remove unused import

* Tweak padding on /tournaments

* Always submit on ctrl+enter

Since we took out group chats, this should be fine for all comments
2022-09-09 11:09:31 -07:00
Austin Chen
6a69f44f07 Tweak padding on /tournaments 2022-09-09 10:16:25 -07:00
marsteralex
aa5876fe0d
added difficulty rating (#864)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

* remove generated cards from digital commanders

* fix counterspell setting default

* added difficulty rating

* updated padding

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-09 09:51:20 -07:00
Ian Philips
e639cb654e Add group endpoint note in api docs 2022-09-09 07:22:31 -06:00
Austin Chen
1408908959 List the Manifold F2P tournament 2022-09-09 01:46:25 -07:00
mantikoros
cd1d8ecd8a WarningConfirmationButton for bets 2022-09-09 01:02:30 -05:00
mantikoros
0dbb42aa69 Auto-prettification 2022-09-09 05:03:05 +00:00
mantikoros
2ebb83418c bet panel: disable input focus on mobile 2022-09-09 00:02:14 -05:00
mantikoros
eac56b1f4f slider: smarter step increments; disable clicking on track 2022-09-09 00:02:14 -05:00
James Grugett
987ebccdfd Contract tabs: used passed in bets 2022-09-08 23:45:26 -05:00
James Grugett
cf74a195b2 Redeem shares: pay back a smaller frac of your loan 2022-09-08 22:59:25 -05:00
marsteralex
677b20a7ba
fix brawl commanders for digital (#862)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

* remove generated cards from digital commanders

* fix counterspell setting default

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-08 20:23:31 -07:00
Austin Chen
e3e80a5fd0 Change user info using bulkWriter 2022-09-08 20:21:32 -07:00
marsteralex
8aeb544f7e
add commander category (#861)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-08 18:38:48 -07:00
James Grugett
b9ae919fda Add staleTime option for prefetching 2022-09-08 16:59:05 -05:00
mantikoros
f25460a647 smaller font for "Predict" 2022-09-08 15:10:36 -05:00
Sinclair Chen
28f3694e8f
Fix editor rerendering when you load it (#831)
* Don't rerender entire editor for user list

also fixes bug where you are the only mention

* Cache with react query instead of memoize
2022-09-08 13:02:50 -07:00
Pico2x
caa3fc06e6 Minor tailwind/indent fixes to posts 2022-09-08 17:32:42 +01:00
Pico2x
1e645f911a Add Fede to admin pt2 2022-09-08 17:14:40 +01:00
Pico2x
adf2086141 Add Fede to admins 2022-09-08 16:51:58 +01:00
FRC
d9bb7d1926
Edit posts (#859) 2022-09-08 16:23:19 +01:00
Ian Philips
5547b30364 Add david to admins 2022-09-08 09:16:54 -06:00
Ian Philips
3932a3dbd4 I predict this will do better than trade 2022-09-08 07:40:16 -06:00
James Grugett
bff4eff719 Persist user page markets on back (Marshall's machinery) 2022-09-08 01:39:01 -05:00
James Grugett
54c227cf6c
Updates to experimental home (#858)
* Line clamp question in prob change table

* Tweaks

* Expand option for daily movers

* Snap scrolling for carousel

* Add arrows to section headers

* Remove carousel from experimental/home

* React querify fetching your groups

* Edit home is its own page

* Add daily profit and balance

* Merge branch 'main' into new-home

* Make experimental search by your followed groups/creators

* Just submit, allow xs on pills

* Weigh in

* Use next/future/image component to optimize avatar images

* Inga/challenge icon (#857)

* changed challenge icon to custom icon
* fixed tip button alignment

* weighing in and trading "weigh in" for "trade"

Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-09-08 01:36:34 -05:00
mantikoros
edbebb7e67 weighing in and trading "weigh in" for "trade" 2022-09-08 00:16:48 -05:00
ingawei
004671f032
Inga/challenge icon (#857)
* changed challenge icon to custom icon
* fixed tip button alignment
2022-09-07 23:51:52 -05:00
Austin Chen
45a965476e Use next/future/image component to optimize avatar images 2022-09-07 20:59:00 -07:00
Ian Philips
bcee49878b Weigh in 2022-09-07 21:39:21 -06:00
Ian Philips
35de4c485a Just submit, allow xs on pills 2022-09-07 21:39:14 -06:00
James Grugett
4439447a6d Persist group markets and scroll position on back (Marshall's machinery) 2022-09-07 21:33:36 -05:00
mantikoros
e6c6f64077 fix mobile nav for trades tab 2022-09-07 21:16:58 -05:00
FRC
0acdec787d
Adds comments to posts (#844)
* Adds comments to posts

* Uncoupled CommentInput from Contracts

* Fix nits
2022-09-07 23:09:20 +01:00
mantikoros
ce52f21ce9 fix sidebar profile link to your trades 2022-09-07 15:13:17 -05:00
mantikoros
b3343c210a more "bet" => "trade" 2022-09-07 15:04:34 -05:00
mantikoros
b4e0e9ebc0 "A market for every question" 2022-09-07 15:01:02 -05:00
mantikoros
28af2063c3 "bet" => "trade" 2022-09-07 14:45:04 -05:00
FRC
cce14cbe1f
Toggle monthly leaderboards (#790)
* Toggle monthly leaderboards

I didn't get to enabling monthly leaderboards after my work trial was over (I enabled daily/weekly/alltime). The cache has been filled out for a while now, this toggles it on.

* Fix nits
2022-09-07 17:04:30 +01:00
Ian Philips
87060488f5 Convert market to lite market for Phil 2022-09-07 07:13:34 -06:00
James Grugett
ad18987e65 Update Daily movers UI 2022-09-07 01:18:11 -05:00
James Grugett
a40bdc28be Remove some excess spacing on user page 2022-09-06 23:39:50 -05:00
James Grugett
082125bd2f Remove some margin 2022-09-06 23:31:02 -05:00
James Grugett
21870d7edb User page: Move portfolio graph and social stats to new tab 2022-09-06 23:24:58 -05:00
mantikoros
85be84071a track embedded markets separtely 2022-09-06 22:43:28 -05:00
mantikoros
a9627bb2b6 market page: regenerate static props after 5 seconds 2022-09-06 22:12:18 -05:00
Sinclair Chen
537962a7dc Stop links from opening twice 2022-09-06 16:55:33 -07:00
Ian Philips
f7d027ccc9 Create button=>Site link 2022-09-06 16:38:01 -06:00
Ian Philips
8759064ccb new bettors 2022-09-06 16:30:58 -06:00
Ian Philips
c16e7c6cfd Add membership indicators and link to see group 2022-09-06 16:20:43 -06:00
James Grugett
668f30dd55 Free market creation shows cost striked through 2022-09-06 16:55:52 -05:00
Ian Philips
45e54789b7 Groups search shares query, sorted by contract & members 2022-09-06 15:51:36 -06:00
mantikoros
c59de1be2e bet slider: decrease step size 2022-09-06 11:53:09 -05:00
Ian Philips
a038ef91eb Show num contracts in group selector 2022-09-06 09:58:24 -06:00
Ian Philips
74af54f3c0 Remove chance from FR og-images 2022-09-06 09:36:41 -06:00
Ian Philips
7c44abdcd7 Comment out unused script functions 2022-09-06 09:27:50 -06:00
Ian Philips
5af92a7d81 Update groups API 2022-09-06 09:24:26 -06:00
Ian Philips
2ee067c072 Remove member and contract ids from group doc 2022-09-06 08:14:13 -06:00
Ian Philips
39d7f1055b Fix spacing on challenge modal 2022-09-06 07:58:00 -06:00
Ian Philips
a3b18e5bea Add challenge back to share modal 2022-09-06 07:57:52 -06:00
FRC
59f3936dad
Fix bug (#854) 2022-09-06 14:17:21 +01:00
mantikoros
450b140f5f show challenge button on mobile 2022-09-05 18:19:13 -05:00
James Grugett
f21711f3dc Fix type error 2022-09-05 18:13:01 -05:00
James Grugett
cd8bb72f94 Daily movers table in experimental/home 2022-09-05 18:09:03 -05:00
mantikoros
837a4d8949 Revert "Show challenge on desktop, simplify modal"
This reverts commit 8922b370cc.
2022-09-05 18:07:44 -05:00
mantikoros
8952b100ad add answer panel mobile formatting, slider 2022-09-05 17:59:19 -05:00
mantikoros
2d724bf2c8 make slider black 2022-09-05 17:44:21 -05:00
mantikoros
374c25ffb3 Auto-prettification 2022-09-05 22:40:48 +00:00
mantikoros
96cf1a5f7f mobile slider styling 2022-09-05 17:39:59 -05:00
mantikoros
ae40999700 mobile bet slider 2022-09-05 17:11:32 -05:00
mantikoros
30d73d6362 remove parantheses from balance text 2022-09-05 16:59:35 -05:00
mantikoros
97e0a78806 "join group" => "follow" 2022-09-05 16:51:09 -05:00
James Grugett
d812776357 Remove show hot volume param 2022-09-05 16:25:48 -05:00
mantikoros
9a49c0b8fe remove numeric, multiple choice markets from create market page 2022-09-05 13:33:58 -05:00
Austin Chen
70eec63533 Adding in "Highest %" and "Lowest %" sort options
Quick alternative to https://github.com/manifoldmarkets/manifold/pull/850/files courtesy of James.

One downside of this approach is that the % only update every 15 minutes; but maybe users won't notice?
2022-09-05 10:07:33 -07:00
Marshall Polaris
6ef2beed8f
Denormalize betAmount and betOutcome fields on comments (#838)
* Create and use `betAmount` and `betOutcome` fields on comments

* Be robust to ridiculous bet IDs on dev
2022-09-04 14:28:45 -07:00
James Grugett
a15230e7ab Smartest money => Best bet. Don't show amount made for comment. 2022-09-04 14:06:29 -05:00
James Grugett
a21466d877 Add sort for 24 hour change in probability 2022-09-03 16:20:57 -05:00
Marshall Polaris
89b30fc50d Fix tournaments page loading indicator and turn page size back down 2022-09-03 14:07:34 -07:00
James Grugett
9060abde8e Cache prob and prob changes on cpmm contracts 2022-09-03 15:06:42 -05:00
mantikoros
085b9aeb2a remove simulator 2022-09-03 14:58:42 -05:00
Marshall Polaris
c0383bcf26
Make tournament page efficient (#832)
* Make tournament page efficient

* Fix URL to Salem contract

* Use totalMembers instead of deprecated field

* Increase page size to 12

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-03 09:55:10 -07:00
Ian Philips
0938368e30 Capitalize yes/no resolution outcomes 2022-09-03 07:29:35 -06:00
Ian Philips
272658e5dc Use most up-to-date user on groups page 2022-09-03 06:52:51 -06:00
Marshall Polaris
861fb7abbd
Use the magic auth prop for groups SSR (#851) 2022-09-03 06:51:55 -06:00
Ian Philips
2d88675f42 Move & more out of the loop 2022-09-03 06:33:33 -06:00
James Grugett
bfa88c3406 Turn off react-query notification subscription because it's buggy 2022-09-02 22:57:23 -05:00
Marshall Polaris
784c081663
Enable source maps in production (#852) 2022-09-02 19:43:22 -07:00
Marshall Polaris
8318621d51
Some changes to make auth better (#846)
* Handle the case where a user is surprisingly not in the DB

* Only set referral info on user after creation

* More reliably cache current user info in local storage

* Don't jam username stuff into user listener hook
2022-09-02 19:39:27 -07:00
Marshall Polaris
e924061c54
Don't re-create visibility observer for no reason (#849)
* Don't re-create visibility observer for no reason

* `IntersectionObserver.unobserve` instead of `.disconnect`
2022-09-02 19:39:07 -07:00
Ian Philips
25a0276bf7 Auth user server-side on groups page 2022-09-02 19:52:38 -06:00
Ian Philips
c74d972caf Pass user and members via props 2022-09-02 19:36:49 -06:00
Ian Philips
57b74a5d09 Use cached values 2022-09-02 18:12:55 -06:00
Ian Philips
9577955d2d Remove null check 2022-09-02 18:08:53 -06:00
Ian Philips
cf508fd8b6
Members and contracts now subcollections of groups (#847)
* Members and contracts now documents

* undo loans change?

* Handle closed group

* Slight refactoring

* Don't allow modification of private groups contracts

* Add back in numMembers

* Update group field names

* Update firestore rules

* Update firestore rules

* Handle updated groups

* update start numbers

* Lint

* Lint
2022-09-02 18:06:48 -06:00
James Grugett
2f53cef36f Move metrics calculation to common 2022-09-02 18:45:42 -05:00
James Grugett
af68fa6c42 Fix typo in email followup 2022-09-02 16:20:04 -05:00
James Grugett
231d3e65c4 Fix incorrect error message for no bets 2022-09-02 16:19:10 -05:00
James Grugett
00de66cd79
Leaderboard calc: update profit even when portfolio didn't change (#845)
* Leaderboard calc: remove didProfitChange optimization that was incorrect

* Put back didPortfolioChange for deciding whether to create new history doc.
2022-09-02 15:59:32 -05:00
mantikoros
b6449ad296 fix bet panel warnings 2022-09-02 15:32:54 -05:00
Marshall Polaris
d1e1937195
Remove custom token generation machinery (#840) 2022-09-02 13:04:00 -07:00
Marshall Polaris
245627a347 Temporarily patch groups loading to make dev deploy work 2022-09-02 13:00:38 -07:00
Marshall Polaris
a429a98a29
Tidy up some dead code and markup in sidebar (#842) 2022-09-02 12:52:27 -07:00
Marshall Polaris
b1bb6fab5b
Disable SSR on /home (#839) 2022-09-02 12:51:41 -07:00
Marshall Polaris
21b9d0efab
Clean up some old pre-Amplitude tracking code (#841) 2022-09-02 12:51:27 -07:00
Marshall Polaris
4c429cd519
Remove some old code related to the old feed (#843) 2022-09-02 12:51:14 -07:00
mantikoros
0cb20d89ed numeric market labels: LOW/HIGH instead of MIN/MAX; eliminate payout <= MIN, etc. 2022-09-02 10:35:41 -05:00
James Grugett
8029ee49a4 Fix loans bug 2022-09-01 23:06:57 -05:00
Marshall Polaris
4406e53121
Make prefetching correctly use context cache (#835) 2022-09-01 19:38:09 -07:00
Marshall Polaris
dca7205a47
Disable group prefetching from contract links (#836)
* Kill dead code

* Stop prefetching groups when viewing contract

* Tidy markup
2022-09-01 19:37:41 -07:00
Sinclair Chen
04e8bb248b Fix Salem Center market url 2022-09-01 18:15:10 -07:00
Austin Chen
51fe44f877 Show the number of open markets on each groups page 2022-09-01 16:12:09 -07:00
Ian Philips
00ba3b0c48
Show avatars of tippers and unique bettors (#837)
* Show avatars of tippers and unique bettors

* Make transparent the avatar bg

* fix import
2022-09-01 16:23:12 -06:00
Marshall Polaris
7508d86c73
Clean up contract overview code (#823)
* Don't call Date.now a million times in answers graph

* Refactor contract overview code so that it's easier to understand
2022-09-01 14:42:50 -07:00
Ian Philips
8d853815d6
Show resolution on og card image (#834)
* Handle resolved markets

* Add in group names as hashtags
2022-09-01 13:55:24 -06:00
FRC
1208694d2d
http to https to avoid blocked requests (#833) 2022-09-01 17:54:46 +01:00
FRC
96be4e8992
Add embedded ContractGrid to Posts (#822)
* Add embedded market grids

* Hacky way to set height

I haven't figured out a way yet to get the height of the actual iframe's content, so I did some bad estimate for now to unblock shipping the feature, while I continue investigating.
2022-09-01 17:47:45 +01:00
Ian Philips
7310cf3d4a fix import 2022-09-01 10:11:08 -06:00
Ian Philips
8922b370cc Show challenge on desktop, simplify modal 2022-09-01 10:02:41 -06:00
Ian Philips
fecf976ab9 Show all group contracts if less than 5 open 2022-09-01 09:11:14 -06:00
Ian Philips
0823414360 Adjust group name padding on mobile 2022-09-01 08:52:49 -06:00
Ian Philips
c6eac97b64 Show group based on most recent creator added group 2022-09-01 08:29:56 -06:00
Ian Philips
6706fe7350 Show user balance on bet panels 2022-09-01 08:12:46 -06:00
Ian Philips
a7c8b8aec4 Hide bet panel when signed out 2022-09-01 07:34:02 -06:00
Ian Philips
5dec6b4a22 Medium includes 10 bettors 2022-09-01 07:23:43 -06:00
Ian Philips
a8d7e91a02 Clean comments 2022-09-01 07:01:49 -06:00
Ian Philips
fec4e19c1d Selectively force long polling for ios only 2022-09-01 07:01:02 -06:00
Marshall Polaris
0568322c82
Dramatically improve server auth stuff (#826) 2022-08-31 22:13:26 -07:00
Marshall Polaris
42548cea2a
Fix prefetching to not populate useless state (#827) 2022-08-31 21:59:58 -07:00
mantikoros
879d6fb2dd bury profile stats in Comments until we find a better place for them 2022-08-31 23:20:20 -05:00
mantikoros
2a17bcb8b2 eslint 2022-08-31 23:00:39 -05:00
Sinclair Chen
7c1e663b26
Editor tweaks (#829)
* Show border around selected embeds

* Make editor tooltips not animate
2022-08-31 20:52:12 -07:00
mantikoros
2c3cd34444 Auto-remove unused imports 2022-09-01 03:34:22 +00:00
mantikoros
e0ebdc644d market close email: remove mention of creator fee 2022-08-31 22:33:37 -05:00
James Grugett
ee76f4188b For you: remove contracts bet on by anyone you follow. 2022-08-31 21:57:11 -05:00
Sinclair Chen
58e671e640 Upload dropped images 2022-08-31 17:18:35 -07:00
Austin Chen
bc1ec414de
Update awesome-manifold.md 2022-08-31 16:29:42 -07:00
Austin Chen
5514eeff2d
Update awesome-manifold.md 2022-08-31 16:18:53 -07:00
Austin Chen
7a9b159909
Update awesome-manifold.md 2022-08-31 15:40:23 -07:00
Ian Philips
74b6df2e44 Unwatch applies to email comment notifs too 2022-08-31 16:18:48 -06:00
Ian Philips
26aba26da5
force long polling (#824) 2022-08-31 15:38:55 -06:00
Austin Chen
7c8b33597a Add "Duplicate Contract" into "..." menu 2022-08-31 14:33:24 -07:00
James Grugett
3660830ec1 Don't server side render Notifications page for improved perf 2022-08-31 15:41:34 -05:00
James Grugett
83696cca21 Fix dayjs fromNow bug (it requires plugin, so use our util instead) 2022-08-31 15:35:47 -05:00
Sinclair Chen
d06b725f52
Let admins add and edit posts to any group (#820)
- show add post UI to admins
- change firebase permissions
2022-08-31 11:29:49 -07:00
Marshall Polaris
149204f6ca Fix my other foolish bug 2022-08-31 11:17:36 -07:00
Ian Philips
5a9d8e3f5d Show how much you've tipped a market 2022-08-31 09:27:37 -06:00
Ian Philips
37d2be9384 Show only relative time if same day on close date 2022-08-31 08:49:35 -06:00
Ian Philips
5df594e46a Make details fit on one line, make group a link 2022-08-31 08:29:35 -06:00
Ian Philips
91e5abe76a Add query to help avoid timeout 2022-08-31 08:03:51 -06:00
Marshall Polaris
27b46f4306 Fix order of comments in threads and under answers 2022-08-31 01:16:57 -07:00
Marshall Polaris
d336383a93 Fix my foolish bug 2022-08-31 01:02:10 -07:00
James Grugett
a3569280a4 Add your bets section to /experimental/home 2022-08-31 00:30:31 -05:00
James Grugett
ccb6fd291e Move components out of /pages into /components 2022-08-30 23:53:12 -05:00
James Grugett
849402ed70 Rearrange home sections. Load more in carousel. 2022-08-30 23:47:09 -05:00
Marshall Polaris
7dddff52b8
Tidying some feed code up (#818)
* Clean up some markup & dead code

* Order comments in Firestore instead of on client

* Order bets in Firestore instead of on client

* Make indexes file up to date with production
2022-08-30 20:28:30 -07:00
mantikoros
40f1c09002 Auto-remove unused imports 2022-08-31 01:56:03 +00:00
mantikoros
ec90b041ee upgrade firebase, nextjs versions 2022-08-30 20:54:32 -05:00
Sinclair Chen
c202c5de68 clarify closed/open group copy 2022-08-30 16:28:49 -07:00
Ian Philips
aad5f6528b
new market view (#819)
* Show old details on lg, don't unfill heart

* Hide tip market if creator

* Small ui tweaks

* Remove contract. calls

* Update high-medium-low

* Remove unused bets prop

* Show uniques

* Remove unused bets prop
2022-08-30 17:13:25 -06:00
James Grugett
3e1e84ee5e Experimental Home: Add links. Single layer carousel for < 6 cards 2022-08-30 17:14:22 -05:00
James Grugett
f83b62cf50 Implement double carousel for /experimental/home 2022-08-30 16:18:42 -05:00
Austin Chen
d658a48b66 Revert "hide quick bet on mobile"
This reverts commit 3d073da97e.
2022-08-30 10:31:35 -07:00
Ian Philips
876abef040 Only send dev weekly trending emails to ian 2022-08-30 10:02:51 -06:00
Ian Philips
74c9406191 Use cached user ids while likes is loading 2022-08-30 09:52:14 -06:00
Ian Philips
a0402830c5
liking markets with tip/heart (#798)
* WIP liking markets with tip

* Refactor Userlink, add MultiUserLink

* Lint

* Lint

* Fix merge

* Fix imports

* wip liked contracts list

* Cache likes and liked by user ids on contract

* Refactor tip amount, reduce to M

* Move back to M

* Change positioning for large screens
2022-08-30 09:38:59 -06:00
FRC
e1f19c52ab
Post in a group about page. (#803)
* Dashboards in Group about page

* Rename group dashboard to 'About Post'

* Fixed James nits
2022-08-30 13:39:10 +01:00
Marshall Polaris
7debc4925e
De-feedify contract tab contents (#808)
* De-feedify contract bets list

* De-feedify contract comments lists

* Clean up a bunch of duplicated work in the comments list stuff

* Remove wrapper markup from comment replies list

* Fix sort order on comments I broke

* Kill now unhelpful `CommentRepliesList` wrapper component

* More random cleanup

* More cleanup and fix some styling I had broken

* Make bet calculations less wrong

* Keep up to date with master

* Make copy link component copy better URL

* Make highlighted comments align properly

* Make user header left align with content on comments

* Fix some free response UI stuff up
2022-08-30 02:41:47 -07:00
Marshall Polaris
1e3a0ca3d9
Upgrade Typescript, ESLint, Prettier (#817)
* Bump Typescript to 4.8.2, eslint, prettier

* Fix some loose typing

* Fix prettier complaint
2022-08-30 01:44:45 -07:00
James Grugett
c7452796f0 Recommend contracts you haven't bet on 2022-08-30 00:22:12 -05:00
Marshall Polaris
1369f3b967
WIP persistence work (#762)
* WIP persistence work

* Fix up close date filter, kill custom scroll restoration

* Use built-in Next.js scroll restoration machinery

* Tweaking stuff

* Implement 'history state' idea

* Clean up and unify persistent state stores

* Respect options for persisting contract search

* Fix typing in common lib

* Clean up console logging
2022-08-29 21:56:11 -07:00
James Grugett
1d948821ca Turn off react query subscription for user bets and portfolio history 2022-08-29 16:47:21 -05:00
Ian Philips
0318f7a12b Add missing parentheses 2022-08-29 13:47:24 -06:00
James Grugett
84432e5ac4 Add creatorId to lite market 2022-08-29 14:25:58 -05:00
FRC
851cffd73e
Dashboards (#791)
* Create backend for Dashboards

* Rm lastupdatetime for now

* Added a create-dashboard and sharable view dashboard page

* Various nit fixes.

* Renamed Dashboards to Posts

* Fix nits
2022-08-29 16:06:17 +01:00
Austin Chen
1d1b09c938 Append question changed text to end of description (instead of start) 2022-08-28 23:23:40 -07:00
Austin Chen
8f338a8d88
Prevent embeds from breaking in Chrome incognito (#814) 2022-08-28 22:40:57 -07:00
James Grugett
7ea6777d6b Add margin bottom to tournament cards to reveal shadow 2022-08-29 00:29:59 -05:00
James Grugett
ecacce0796 Remove console.log. Log onIdTokenChanged error. 2022-08-29 00:26:12 -05:00
James Grugett
71dfcc4dd9 Add tracking for clicking recommended card & tournament card 2022-08-29 00:23:31 -05:00
Austin Chen
6c64c9f1cd Remove hot volume from /tournaments 2022-08-28 22:21:28 -07:00
mantikoros
6facf3b7a7 sidebar ordering 2022-08-29 00:01:04 -05:00
James Grugett
62e72b2091 Loan dialog wording tweak 2022-08-28 23:51:43 -05:00
James Grugett
4dad954820 Change limit order label "at" => "up to" or "down to" 2022-08-28 23:47:11 -05:00
Austin Chen
f0727a65fc Add SF 2022 Ballot to /tournaments 2022-08-28 21:33:13 -07:00
akrolsmir
c7be227865 Auto-remove unused imports 2022-08-29 04:00:14 +00:00
Austin Chen
cf58fc9fd4 Remove Groups from sidebar 2022-08-28 20:59:14 -07:00
James Grugett
996b4795ea
Cache user bets tab with react query!! (#813)
* Convert useUserBets to react query

* Fix duplicate key warnings

* Fix react-query workaround to use refetchOnMount: always'

* Use react query for portfolio history

* Fix useUserBet workaround

* Script to back fill unique bettors in all contracts

* React query for user bet contracts, using uniqueBettorsId!

* Prefetch user bets / portfolio data
2022-08-28 18:03:00 -05:00
mantikoros
7e00f29189 back to "sign up to bet" 2022-08-28 16:55:29 -05:00
mantikoros
1ff453d64c eslint 2022-08-28 16:38:59 -05:00
mantikoros
e4c66e08f5 clean up add markets dialog 2022-08-28 16:26:36 -05:00
mqp
3fd07da1b0 Auto-prettification 2022-08-28 21:15:31 +00:00
mantikoros
eb070f0b07 Auto-remove unused imports 2022-08-28 21:14:44 +00:00
mantikoros
c88621de19 hide group edit dialog when signed out 2022-08-28 16:14:02 -05:00
mantikoros
2e96721a5c "sign in" => "add my answer" 2022-08-28 16:14:02 -05:00
mantikoros
0a5fb4752a CreateQuestionButton: use Button component 2022-08-28 16:14:02 -05:00
mantikoros
cae2154893 sign in button 2022-08-28 16:14:02 -05:00
mantikoros
926929880a "Sign up to bet" => "Place my bet"; "Sign in to comment" => "Add my comment"; rename button to BetSignUpPrompt 2022-08-28 16:14:02 -05:00
James Grugett
9c15d5b96c
React-query-ify notifications (#812)
* Use single react query to subscribe to notifications

* Remove 'preferred' in variable names
2022-08-28 15:20:21 -05:00
mantikoros
3d073da97e hide quick bet on mobile 2022-08-28 14:07:19 -05:00
mantikoros
d63dd12056 admin unlisted toggle 2022-08-28 13:37:34 -05:00
mantikoros
133e7a9c3f change label to admin 2022-08-28 13:37:34 -05:00
mantikoros
98861ccc19 remove typo 2022-08-28 13:37:34 -05:00
Marshall Polaris
1e11491369 Tidy up rendering of info tooltips 2022-08-28 01:43:13 -07:00
James Grugett
7c798a063c Improve edit close date UI 2022-08-28 00:35:24 -05:00
mantikoros
03e07037ea
ban users from posting (#810) 2022-08-28 00:23:25 -05:00
James Grugett
2acc1a8433 Double daily loans rate to 2% 💰💰 2022-08-28 00:11:28 -05:00
James Grugett
9dd23b4a08 Fix weird new crash in updateMetrics: contract.id missing? 2022-08-28 00:11:13 -05:00
James Grugett
e4f46c48f1 Fix recommended markets not updating when navigating 2022-08-27 22:35:46 -05:00
James Grugett
cb08a114ae Better recommended contracts. Include from first group. 2022-08-27 22:26:39 -05:00
James Grugett
e7f369e2b4 Load recommended markets even when navigating from home 2022-08-27 22:26:39 -05:00
mantikoros
f31db2f9ed emails: make banner a link 2022-08-27 22:15:14 -05:00
Marshall Polaris
b21051ced5 Fix up copy link toast styling 2022-08-27 19:15:55 -07:00
Marshall Polaris
ef77c7c9a3
Clean up markup in CopyLinkDateTimeComponent (#809) 2022-08-27 19:05:46 -07:00
James Grugett
36fa9078f5 Fix absolute import within functions 2022-08-27 17:18:39 -05:00
James Grugett
a80d1f194c Don't redeem shares if there's only epsilon shares to redeem 2022-08-27 17:14:41 -05:00
James Grugett
d7793841d1 Fix NaN invested (floating point error) 2022-08-27 17:13:29 -05:00
Marshall Polaris
4b513a894d
Make tooltip rendering more efficient (#807)
* Don't use very slow dayjs formatter on timestamp tooltips

* Kill dead code in feed-bets.tsx

* Clean up tooltip markup
2022-08-27 13:46:35 -07:00
mantikoros
eeed9eef10 market resolution email: link in header image, show only non-negative investment amount 2022-08-27 14:38:09 -05:00
mantikoros
305acbb18f "current value" => "expected value" 2022-08-27 14:17:19 -05:00
Marshall Polaris
5d8f5d41fc
Fix some efficiency problems with ContractProbGraph (#806)
* Memoize bets input to ContractOverview

* Optimize a bunch of nonsense in `ContractProbGraph`
2022-08-27 01:09:17 -07:00
Marshall Polaris
3e976eadac
Make portfolio graph loading more efficient (#805)
* Make portfolio graph on profile not load extra data

* Clean up unused props

* Tidy up markup

* Enable "daily" option again on portfolio history picker
2022-08-27 01:09:01 -07:00
James Grugett
51ceb62871 Fix console error on create page 2022-08-27 01:14:24 -05:00
James Grugett
a9ea335cd1 Fix create page serverside vs clientside render discrepancy console error 2022-08-27 01:07:39 -05:00
James Grugett
a040df2732 Fix console error from non-react-style attributes on trophy icon 2022-08-27 01:02:59 -05:00
James Grugett
2e3c2d4dcb Tweak to add market to group UI 2022-08-27 00:59:00 -05:00
mqp
5ff847fba3 Auto-prettification 2022-08-27 05:01:29 +00:00
jahooma
f641569bcc Auto-remove unused imports 2022-08-27 05:00:37 +00:00
James Grugett
8e4dd407f6 Test with unused import 2022-08-26 23:59:56 -05:00
James Grugett
b88f9a4fc1
Set up github action to remove unused imports 2022-08-26 23:56:38 -05:00
James Grugett
86cf956894 Add eslint plugin to remove unused imports 2022-08-26 23:49:03 -05:00
James Grugett
e4d6bb35b5 Fix floating button to be on top of quick bet arrows. Switch icon. 2022-08-26 23:10:10 -05:00
Marshall Polaris
902d9e140c
Create and use new usePagination hook for paginating loading (#769)
* Create and use new `usePagination` hook for paginating loading

* Fix index for new comment list code
2022-08-26 20:18:08 -07:00
Sinclair Chen
9698895c22 Update fr chart colors 2022-08-26 17:39:46 -07:00
Austin Chen
a2da319e7c Remove unused import 2022-08-26 17:35:59 -07:00
Austin Chen
1dbef921b0 Sort markets on /tournaments by % 2022-08-26 17:13:49 -07:00
Sinclair Chen
59aa76a474
Add Salem Center tournaments (card screenshots) (#804) 2022-08-26 16:23:44 -07:00
James Grugett
99bff6b794 Improve group market selection UI 2022-08-26 18:17:15 -05:00
James Grugett
5735864fd1 Add pencil to edit group on contract page 2022-08-26 17:25:05 -05:00
Sinclair Chen
8903b1ef95
Replace style props with tailwind classes (#800)
* add utility class for `word-break: break-word`

* refactor visuallyHidden style out of Page

* refactor out ref sizing hack in sidebar

* replace style props with tailwind classes
2022-08-26 14:23:06 -07:00
Austin Chen
3255806891 Support Figma embeds 2022-08-26 12:41:39 -07:00
Austin Chen
ba7d0f45db Close Add Market modal on Cancel 2022-08-26 12:41:29 -07:00
Sinclair Chen
490115d890
Add tournaments to sidebar (#802)
* Add tournaments to sidebar

* Remove unused import

* Reposition tournaments tab

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-26 10:45:01 -07:00
Sinclair Chen
803091db06
Add tournament home page (#797)
* Add tournament home page

* Preload markets, follow count

* organize imports

* Fix card width

* Make entire title clickable

* plural /tournament -> /tournaments

* prettier

* Fix /tournaments when groupIds are invalid

* Restyle /tournaments page

* Reintroduce Salem, tweak styles

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-26 10:31:25 -07:00
James Grugett
31db330319 Show "(max)" on streak payout if it is the max payout 2022-08-26 11:38:08 -05:00
James Grugett
b1ccee73fd If there is a group for a market on market page, clicking it goes to group 2022-08-26 01:23:50 -05:00
James Grugett
74ce98913c Make graph start from left side for new markets 2022-08-26 01:08:16 -05:00
Austin Chen
26a2eb2391 Switch to a different color scheme 2022-08-25 22:31:05 -07:00
James Grugett
6f2e2a3bbb Render graph for multiple choice embeds 2022-08-26 00:23:50 -05:00
James Grugett
539bfba70c Decrease starting time window for free response graph 2022-08-26 00:21:06 -05:00
James Grugett
e5777f02d8 Expand notifications by default if <= 3 items 2022-08-26 00:00:44 -05:00
Sinclair Chen
1b029ce8dd
bump tiptap version to fix multi-italic bug (#801) 2022-08-25 20:56:38 -07:00
Austin Chen
4faab4fcdc Clean up Featured code 2022-08-25 19:42:40 -07:00
Austin Chen
0f49effade Tweak Featured badge design 2022-08-25 19:17:50 -07:00
James Grugett
7773234138 Add debug console.log 2022-08-25 17:21:51 -05:00
Ian Philips
91bb4dfab2 With play money on numeric & center text 2022-08-25 12:06:42 -06:00
James Grugett
97b648a51e Move recommended markets below market white bg onto gray bg 2022-08-25 12:59:26 -05:00
Ian Philips
b785d4b047 With play money 2022-08-25 10:02:22 -06:00
Ian Philips
90e1fdb586 Add david to admins 2022-08-25 07:54:50 -06:00
Ian Philips
dc89d5d4d0 Feature markets on trending 2022-08-25 07:05:26 -06:00
Ian Philips
b9f0da9d3b Give all users 5 free markets 2022-08-25 05:51:56 -06:00
Ian Philips
6e3b8fdd4d Show old streak for old streak notifs 2022-08-25 05:10:38 -06:00
Ian Philips
465e219bfc Show old streak for old streak notifs 2022-08-25 05:10:24 -06:00
mantikoros
18f2550e4d resolution email: show n/a for canceled numeric 2022-08-24 23:44:30 -05:00
James Grugett
93739e7990 Fix betting streak number 2022-08-24 23:40:27 -05:00
James Grugett
51380febd4 Increase memory for updateStats function 2022-08-24 22:50:52 -05:00
James Grugett
cffd5dcd31 Weekly => daily loans 2022-08-24 22:03:07 -05:00
James Grugett
0caa5e24e8 Some other follow to watch changes 2022-08-24 21:23:12 -05:00
James Grugett
25eca71846 Convert heart to eye and follow to watch 2022-08-24 21:16:38 -05:00
Ian Philips
535e50eeac Betting streak bonus per day:10, max:50 2022-08-24 17:40:03 -06:00
Ian Philips
bca34dad60 Set max betting bonus to M 2022-08-24 17:31:35 -06:00
James Grugett
a8da5719fe Create experimental home page 2022-08-24 18:30:31 -05:00
Ian Philips
7a38d67c5b Reduce share row top margin on mobile 2022-08-24 17:11:48 -06:00
Ian Philips
7a22c7d76a Gap adjustment 2022-08-24 17:09:07 -06:00
Ian Philips
8d1cebf4db Move share button back down, small spacing tweaks 2022-08-24 17:07:22 -06:00
Ian Philips
b6e636cbc0 Small ux tweaks for signed out market page 2022-08-24 16:41:46 -06:00
mantikoros
5bf135760e fix sidebar tracking 2022-08-24 17:23:34 -05:00
SirSaltyy
74a0479cbd
Change about button (#796)
About button name change and now directs to "Help and About Center" super.so
2022-08-25 06:51:33 +09:00
Ian Philips
52a89d0783 Remove bolded More from navbar 2022-08-24 15:42:09 -06:00
Ian Philips
d553aae71e Shrink icon 2022-08-24 15:11:38 -06:00
Ian Philips
5365fa6175 💔💔💔 2022-08-24 15:09:28 -06:00
mantikoros
d5ac560f0c eslint 2022-08-24 15:36:57 -05:00
mantikoros
de74b2987a eslint 2022-08-24 15:34:00 -05:00
mantikoros
d390b39e0a eliminate fees 2022-08-24 15:29:48 -05:00
James Grugett
d6d1e8e86f Fix types 2022-08-24 13:29:35 -05:00
James Grugett
1c323c5a7f Simple recommended contracts based on contract creator 2022-08-24 12:59:23 -05:00
Ian Philips
3eb1b66e9a Lint 2022-08-24 11:58:32 -06:00
Ian Philips
c72bf506c3 Heart button on xl style 2022-08-24 11:53:29 -06:00
Ian Philips
432ee387ec Show all groups on sidebar 2022-08-24 11:23:07 -06:00
Ian Philips
a5812a5a73 Remove group chat display 2022-08-24 11:15:38 -06:00
Ian Philips
5dcaae7af6 Fix import 2022-08-24 10:51:21 -06:00
Ian Philips
480371cf9f Fix import 2022-08-24 10:50:55 -06:00
Ian Philips
f50b4775a1
Allow to follow/unfollow markets, backfill as well (#794)
* Allow to follow/unfollow markets, backfill as well

* remove yarn script edit

* add decrement comment

* Lint

* Decrement follow count on unfollow

* Follow/unfollow button logic

* Unfollow/follow => heart

* Add user to followers in place-bet and sell-shares

* Add tracking

* Show contract follow modal for first time following

* Increment follower count as well

* Remove add follow from bet trigger

* restore on-create-bet

* Add pubsub to dev.sh, show heart on FR, remove from answer trigger
2022-08-24 10:49:53 -06:00
James Grugett
78780a9219 Dedup contract leaderboards code from contract slug (merge error?) 2022-08-23 19:25:57 -05:00
Sinclair Chen
7da4eb8fe9
Fix bet modal probability sticking (#793)
* Fix button group styles
* Reset prob strike-out when bet modal closed
2022-08-23 14:31:52 -07:00
Ian Philips
bea94d58c5 Add extra text-sm 2022-08-23 07:55:26 -06:00
mantikoros
1c73d21925 weeklyMarketsEmails: send different markets to different users 2022-08-23 00:27:07 -05:00
Sinclair Chen
b476a7e3f8
Take descriptions out of LiteMarket (#789) 2022-08-22 18:18:51 -07:00
Sinclair Chen
baa27a3c85 Make Sinclair an admin 2022-08-22 17:50:59 -07:00
Sinclair Chen
20fd286756
Fix link classes duplicating on paste (#788) 2022-08-22 17:45:23 -07:00
Austin Chen
552f9add70 Reduce min time on contract graph to 1h
Allows more resolution on real-time markets, where a lot of trading happens within minutes
2022-08-22 17:23:59 -07:00
Ian Philips
3bea983662 Be more explicit after unsubscribing from weekly trending 2022-08-22 16:56:28 -06:00
Ian Philips
6929076740 Be more specific about unsubscribe 2022-08-22 16:43:08 -06:00
Ian Philips
e1775681aa Add weekly email sent flag, filter out manifold grouped markets 2022-08-22 16:36:39 -06:00
Sinclair Chen
ec4d0f6b4a
Fix notification for updated questions (#782)
* Fix update notification for question, description

* Don't notify on updated description
2022-08-22 15:26:54 -07:00
Ian Philips
b9a667b126 Add logs to weekly emails 2022-08-22 14:59:11 -06:00
mantikoros
571cf80e13 markets api: only load 500 markets by default 2022-08-22 14:42:23 -05:00
mantikoros
3313b55853 listAllContracts: sort by createdTime by default 2022-08-22 14:23:52 -05:00
Austin Chen
650aa68bcd Fix imports 2022-08-22 11:31:33 -07:00
Austin Chen
7736f1e3c1 Make duplicating better: description, closetime, logscale
Known issue: some markets like https://manifold.markets/FFSX/rojo-ronald-jones don't duplicate because too much stuff in JSON...?
2022-08-22 10:49:54 -07:00
Austin Chen
0cd61eb214 DX: Link to Firestore console from "..." 2022-08-22 10:48:21 -07:00
mantikoros
2530171721 don't run next-sitemap post build 2022-08-22 12:09:16 -05:00
mantikoros
009c85b61a listAllContracts: order by popularity score 2022-08-22 12:07:05 -05:00
mantikoros
7a0d64e72f host sitemap manually (delete nextjs sitemap) 2022-08-22 12:02:08 -05:00
mantikoros
40a22b31f3 fix sitemap 2022-08-22 11:52:05 -05:00
mantikoros
8ea9a79760 loan emoji 2022-08-22 10:31:23 -05:00
James Grugett
e6db99e810 Check loans calc for isFinite 2022-08-22 10:20:22 -05:00
Ian Philips
571d3e71b5 Only show most recent streak notif, relative econ imports, pubsub emulator 2022-08-22 06:31:30 -06:00
Austin Chen
b7790a9678 Show Referrals count for each user 2022-08-21 22:53:02 -07:00
Austin Chen
88bf678ce3
Allow custom environments to override any economic aspect (#787)
* Extract monetary constants to a single file economy.ts

* Add missing import

* Allow environments to override any econ variable

* Update imports

* Update more imports

* Fix import
2022-08-21 22:37:26 -07:00
James Grugett
8b7cd20b6f
Loans2: Bets return you some capital every week (#588)
* Remove some old loan code

* Almost complete implementation of updateLoans cloud function

* Merge fixes

* Use invested instead of sale value, check if eligible, perform payouts

* Run monday 12am

* Implement loan income notification

* Fix imports

* Loan update fixes / debug

* Handle NaN and negative loan calcs

* Working loan notification

* Loan modal!

* Move loans calculation to /common

* Better layout

* Pay back loan on sell shares

* Pay back fraction of loan on redeem

* Sell bet loan: negate buy bet's loan

* Modal tweaks

* Compute and store nextLoanCached for all users

* lint

* Update loans with newest portfolio

* Filter loans to only unresolved contracts

* Tweak spacing

* Increase memory
2022-08-22 00:22:49 -05:00
Austin Chen
3158740ea3 Minor tweaks for custom instances 2022-08-21 22:13:42 -07:00
Austin Chen
258b2a318f Default to showing weekly bet graph 2022-08-21 21:02:56 -07:00
Austin Chen
aa3647e0f3 Handle case when no charity txns 2022-08-21 20:55:04 -07:00
James Grugett
d18dd5b8fb Fix a case of limit order sorting 2022-08-21 15:58:49 -05:00
James Grugett
645cfc65f4 Update sort order of limit orders (older bets first b/c they are filled first) 2022-08-21 12:57:00 -05:00
mantikoros
97b38c156f Revert "create contract: ante no longer user liquidity provision"
This reverts commit 56e9b5fa2f.
2022-08-20 15:34:52 -05:00
mantikoros
c6dc852cd8 send creator guide on D1 2022-08-20 15:34:34 -05:00
mantikoros
ef127ea335 update welcome email 2022-08-20 15:34:34 -05:00
mantikoros
43bbc9ec24 send followup email on D2 2022-08-20 15:34:34 -05:00
Marshall Polaris
2439317408
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
2022-08-20 13:32:12 -07:00
James Grugett
f4ebb2b504 Fix wrapping on "Add market" button 2022-08-20 15:19:05 -05:00
Sinclair Chen
a9f846e8fc
Fix tooltip styles in your bets (#783)
* Only show answer tooltips if truncated

* Always wrap in tooltip

* refactor. make title in dialog less big
2022-08-20 13:05:33 -07:00
James Grugett
099764a931 Show unlisted markets in groups and under your markets 2022-08-20 14:38:15 -05:00
James Grugett
09e8993cd4 Implement visibility option for new markets 2022-08-20 14:31:32 -05:00
mantikoros
dd6c5dc97a betting streaks copy 2022-08-20 13:47:26 -05:00
James Grugett
2fef413d88 Don't show fantasy football in newest sort 2022-08-20 13:46:14 -05:00
James Grugett
474304d284 Revert "🔥🔥🔥🔥🔥🔥🔥🔥"
This reverts commit fc8487dca0.
2022-08-20 11:45:13 -05:00
Austin Chen
6791da0fc8 Fix "Most Recent Donor" on /charity 2022-08-19 17:28:08 -07:00
Sinclair Chen
c850cfe97f Revert "Revert "fix firefox visual glitch - single card wrapping""
This reverts commit 63a5241b2e.
2022-08-19 16:59:42 -07:00
Austin Chen
51c843d765
Use masonry on contract cards, sorted correctly (#773)
* Revert "Revert "Tile contract cards in masonry layout (#761)""

This reverts commit 62728e52b7.

* Sort the contracts in the correct masonry order

* Fix ordering on single columns

* Use react-masonry-css to accomplish masonry view

* Improve comment

* Remove gridClassName

Everything is spaced with m-4, too bad
2022-08-19 16:57:23 -07:00
Ian Philips
03d98a7ad7 Reset hour to 12am utc 2022-08-19 17:16:17 -06:00
mantikoros
0cbc0010c1 schedule emails from onCreateUser; send interesting markets on D1 2022-08-19 17:03:00 -05:00
Ian Philips
fc8487dca0 🔥🔥🔥🔥🔥🔥🔥🔥 2022-08-19 16:00:40 -06:00
James Grugett
b67a26ad61 Don't show bets streak modal on navigate each tab 2022-08-19 16:51:52 -05:00
Ian Philips
39c312cf9f Explicitly pass utc timezone 2022-08-19 15:19:52 -06:00
Ian Philips
1196ec4375 Send 6 trending emails to all users monday 12pm PT 2022-08-19 15:01:53 -06:00
Ian Philips
634196d8f1 Slice the popular emails to the top 20 2022-08-19 14:45:04 -06:00
Ian Philips
36bfbe8f42 Change betting streak modal, tweak trending email query 2022-08-19 14:37:16 -06:00
Ian Philips
a0f62ba172
Markets emails (#764)
* Send out email template for 3 trending markets

* Rich text to plaintext descriptions, other ui changes

* Lint

* Filter for closed markets

* Change sign

* First order must be closeTime

* Send 6 emails, check flag twice

* Exclude contracts with trump and president in the name

* interesting markets email

* sendInterestingMarketsEmail

* Change subject line back

Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-08-19 11:43:57 -06:00
Ian Philips
ba5dabd613 Increase gap between profit and streak 2022-08-19 11:24:28 -06:00
Ian Philips
00c9fa61c3
betting streaks (#777)
* Parse notif, show betting streaks modal, schedule function

* Ignore streaks of 0

* Pass notifyFills the contract

* Turn 9am into a constant

* Lint

* Up streak reward, fix timing logic

* Change wording
2022-08-19 11:10:32 -06:00
Sinclair Chen
4f3202f90b
Simple bet interface in embeds (#775)
* rename BetRow -> BetButton

* Replace bet modal in embed with inline betting

- Also simplifies graph height calculation

* Move bet row above graph, in "mini modal"

* Show signup button if not signed up

* Show probability change

* Show error after modal

- Show balance if insufficient funds
- Clear error from amount input if amount deleted entirely

* Fix error state conditions

- Reset amount input on success
- Reset success state on user input

* Make input smaller (80px)
2022-08-19 10:07:48 -07:00
Sinclair Chen
98a0ed99c9 Fix (i) alignment 2022-08-19 09:53:16 -07:00
Sinclair Chen
4d7df00a68
Make Avatar component update when avatarUrl updates (#780) 2022-08-19 09:47:00 -07:00
Barak Gila
f51ad2224b
add YIMBY Law and CaRLA as charities (#781) 2022-08-19 10:52:01 -05:00
Marshall Polaris
0972de9025
Make typing for comments more fancy (#776) 2022-08-19 01:06:40 -07:00
Sinclair Chen
f2764e9258
Remove keyboard accessibility for tooltips (#779)
Headless UI's Modal component autofocuses the first focusable item
inside it when opened. This is by design for accessibility reasons.
See https://headlessui.com/react/dialog#managing-initial-focus

Ironically this means we'll have to remove keyboard focus for tooltips
because this causes the tooltips to pop up unnecessarily for all users
whenever the dialog is opened. The alternative is managing focus
manually for several dialogs, which may not be possible as some of our
modals lack a sensible element to focus by default.
2022-08-18 18:54:09 -07:00
Sinclair Chen
2537663a57
Fix user avatar in mention list not updating (#778) 2022-08-18 17:20:40 -07:00
Marshall Polaris
0cf9a90cfb
Remove some dead code related to tags, categories, and old feed stuff (#765)
* Remove dead image storage code

* Kill tag page

* Kill tag and categories related component UI

* Kill some old algo feed kind of code
2022-08-18 15:46:11 -07:00
Keri Warr
4f6d478211
List manifold-sdk on the Awesome Manifold page (#774) 2022-08-18 16:59:18 -05:00
Marshall Polaris
06ced7042d Fix a typo in my script 2022-08-18 12:49:01 -07:00
Marshall Polaris
c37997bcb7
Add comment type field to comments (#772) 2022-08-18 12:47:35 -07:00
Austin Chen
c2db558b85 Describe why subsidizing is good 2022-08-18 10:12:38 -07:00
James Grugett
097000c9da Don't scroll to top on search change except on home 2022-08-18 11:23:16 -05:00
mantikoros
d216b298ba "create-contract.ts" => "create-market.ts" 2022-08-18 11:14:16 -05:00
mantikoros
56e9b5fa2f create contract: ante no longer user liquidity provision 2022-08-18 11:14:16 -05:00
Ian Philips
c9c3a95d2a Condense user profile bits 2022-08-18 09:54:30 -06:00
mantikoros
87561503c1 accept challenge: redeem shares 2022-08-18 10:39:48 -05:00
Ian Philips
68a949de35 Change Challenge page wording 2022-08-18 08:22:37 -06:00
Ian Philips
33edd3c0fb
Create challenge without previous market (#718)
* Create challenge without previous market

* Check if they've balance to create both on fe

* Change wording slightly

* Finish merge
2022-08-18 08:15:20 -06:00
Ian Philips
c3d09e5323 Add links to challenge page 2022-08-18 07:53:19 -06:00
Marshall Polaris
97fa5fa636
Replace /markets with /home (#766)
* Make /home not kick out logged out users

* Point people at /home instead of /markets
2022-08-17 23:15:25 -07:00
marsteralex
fb67010c0e
include draft innovation basics (#771)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-17 18:17:29 -07:00
marsteralex
5bf39a7a92
sort by set name (#770)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-17 18:01:59 -07:00
marsteralex
2c97be815b
remove check from original (#768)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-17 16:03:02 -07:00
mantikoros
159723ed0c market creation email 2022-08-17 17:36:52 -05:00
marsteralex
ce3d092497
Add Basic Lands to MTG Guesser (#716)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-17 12:40:59 -07:00
Sinclair Chen
99009f841b Make text of old chats same size as current 2022-08-17 10:45:30 -07:00
mantikoros
bf64f5b3a9 redirect /about; more content in welcome email 2022-08-17 11:21:29 -05:00
James Grugett
770a8d049c Check limit prob with floating equals 2022-08-17 10:55:47 -05:00
Ian Philips
a00857cb45 Fix wrapping close date and truncate group name 2022-08-16 16:03:55 -06:00
Sinclair Chen
59565416b6
Api fixes (#704)
* Add min, max, isLogScale to numeric market API return

* Add lastUpdatedTime to market API

* Return a string description in market API

* Accept string descriptions in market POST api

* install prettier eslint config. fix import

* fix another import
2022-08-16 15:01:03 -07:00
James Grugett
8c2f3c56d3 Limit orders: Subtract fees from "profit if both filled" 2022-08-16 15:51:04 -05:00
James Grugett
814c4aa01d Change limit prob validation to be only on Binary markets (not numeric) 2022-08-16 15:44:58 -05:00
James Grugett
62728e52b7 Revert "Tile contract cards in masonry layout (#761)"
This reverts commit 4002c23bee.
2022-08-16 13:03:04 -05:00
James Grugett
63a5241b2e Revert "fix firefox visual glitch - single card wrapping"
This reverts commit ec7263da18.
2022-08-16 13:02:55 -05:00
mantikoros
c58ed8bd2c personal followup email 2022-08-16 11:45:58 -05:00
mantikoros
c3eaf0351b one week email changes 2022-08-16 11:44:01 -05:00
Marshall Polaris
59ca1f7640
Denormalize some contract comment fields (#760)
* Make `groupConsecutive` more capable

* Put denormalized `contractQuestion` and `contractSlug` on comments

* Update user profile UI to use new denormalized fields

* `/Austin` -> `/market`
2022-08-15 22:43:46 -07:00
Marshall Polaris
d00fe7bcd2
Backend robustness to email sending or analytics tracking failures (#728)
* Make `sendEmail` functions await email send success

* Make tracking and email sending not throw on failure
2022-08-15 22:13:38 -07:00
Marshall Polaris
186befd0ac
Bail out earlier if createmarket is called with invalid group ID (#745)
* Bail out earlier if `createmarket` is called with invalid group ID

* Fix typing in `createmarket`
2022-08-15 22:12:43 -07:00
Sinclair Chen
ec7263da18 fix firefox visual glitch - single card wrapping 2022-08-15 22:08:09 -07:00
James Grugett
f2f77cb51e Resolve market emails: fix negative amount bug with better invested calculation 2022-08-15 21:48:01 -05:00
James Grugett
e5aef763cd Calculate invested properly for DPM 2022-08-15 21:48:01 -05:00
James Grugett
aef14e49bb Update bet type to explain dpm props 2022-08-15 21:48:01 -05:00
Ian Philips
cd520e6cfe lint 2022-08-15 19:47:58 -06:00
Ian Philips
d56435b9cd Signed out home page shows dynamic trending markets 2022-08-15 19:34:45 -06:00
Sinclair Chen
4002c23bee
Tile contract cards in masonry layout (#761) 2022-08-15 17:41:53 -07:00
James Grugett
997d68a574 Compute invested & display in your bets 2022-08-15 19:04:37 -05:00
Austin Chen
34e8138e50 Show placeholder when avatarUrl errors 2022-08-15 16:33:02 -07:00
Ian Philips
428d9a3692 Move avatar to below card on mobile 2022-08-15 13:49:33 -06:00
James Grugett
2ff2d6c1fc Scroll to top for fresh query 2022-08-15 14:26:18 -05:00
mantikoros
5c49461449 new welcome email 2022-08-15 11:12:33 -05:00
James Grugett
c80f82a3f7 Home page hack: discard NextJS router state 2022-08-15 11:06:42 -05:00
Marshall Polaris
972f215f0c
Rewrite useQueryAndSortParams machinery to be faster/simpler/better (#758)
* Rewrite useQueryAndSortParams machinery to be faster/simpler/better

* Politely debounce Algolia querying

* Tidy some stuff up

* Style changes suggested by James
2022-08-14 22:09:25 -07:00
mantikoros
5d14d79e6e share dialog: remove native sharer; use toast for embed 2022-08-15 00:03:05 -05:00
mantikoros
b57c84bbd9 notifications title/seo 2022-08-14 23:55:11 -05:00
James Grugett
4e1fae5b5f Require a whole percentage for limitProb in back end 2022-08-14 20:51:10 -05:00
Marshall Polaris
0b711be480
Clean up a bunch of markup and CSS on contract cards (#753)
* Remove random unnecessary top-level divs

* Remove wrapper in MiscDetails

* Remove another wrapper in ContractCard

* Fix a bunch of weird CSS stuff
2022-08-14 01:05:17 -07:00
Marshall Polaris
69c49679f1
Move search controls into separate component (#757)
* Move search controls into separate component

* Fix up typing on pill groups thingy

* More precise comparison per James

* Make sure `additionalFilter` is passed into controls
2022-08-13 16:34:03 -07:00
Marshall Polaris
0085ffcb0b
Simplify and fix inefficiencies in contract search component (#756)
* Simplify and fix inefficiencies in contract search component

* Add react-dom types

* Add a clarifying comment

* Improve search per some feedback
2022-08-13 13:15:11 -07:00
James Grugett
0a9df3ac6b Group horizontal margin on tabs 2022-08-13 13:50:26 -05:00
James Grugett
aeea66491a Group question => market 2022-08-13 13:49:25 -05:00
Marshall Polaris
456d9398a1
Revamp a lot of stuff on the user page to make it usably efficient (#751)
* Load bets and comments tabs data on user page independently

* Implement basic pagination on profile comments list

* Tweak server auth to return `null` instead of `undefined`

* Switch to SSR for user page

* Fix lint

* Fix broken contract fetching in user bets list

* Tidying
2022-08-12 20:42:58 -07:00
Sinclair Chen
dcc3c61f52
Only calculate position when tooltip is shown (#755) 2022-08-12 20:35:08 -07:00
Marshall Polaris
0f7f55ec0a Fix embarrassing bug in server auth 2022-08-12 20:14:24 -07:00
Marshall Polaris
e4239d0122
Tweak Firestore user rules to be more robust (#750) 2022-08-12 20:13:09 -07:00
Sinclair Chen
facb19a347
fix dependency peer-dep warnings, mostly (#752) 2022-08-12 17:49:08 -07:00
Marshall Polaris
96a378ec4b
Make RelativeTimestamp a little more efficient (#754)
* Don't do extra dayjs work in timestamp components

* Remove extra wrapper from `RelativeTimestamp`
2022-08-12 17:48:41 -07:00
Marshall Polaris
79be0c555b Fix tiny bug in auth context code 2022-08-12 13:45:38 -07:00
Marshall Polaris
3cb28cdecb
Teach AuthContext to manage the private user doc (#738)
* Return both user and privateUser from `createuser`

* Make `useStateCheckEquality` more flexible

* Make `AuthContext` track the private user doc

* Change `usePrivateUser` hook to use the auth context data

* Pass both user and private user through SSR to auth context

* Fix bug in create user flow
2022-08-12 13:41:00 -07:00
James Grugett
3cbf5a6f7d Always show search placeholder 2022-08-12 14:35:27 -05:00
Sinclair Chen
20ab313c6c Improve profile comments vis d 2022-08-12 12:10:45 -07:00
Sinclair Chen
88535e5512 fix lint error 2022-08-12 12:10:07 -07:00
Sinclair Chen
df858f916b
Migrate daisy tooltips to our own to fix cutoffs (#748)
* Make all tooltips use our component

* Stop mobile tooltip crop (daisy -> floating-ui)

* Show tooltip on tap for touch devices

Except tooltips on buttons

* migrate another daisy tooltip to ours

* Prevent hidden tooltip from covering click/hover
2022-08-12 12:04:23 -07:00
mantikoros
d2b634c775 template email tracking 2022-08-12 11:33:02 -05:00
mantikoros
8ebccd05ec market movement warning; add bankroll warning to FR markets 2022-08-12 11:24:08 -05:00
Marshall Polaris
80fd38990f Experimentally do not optimizeCss 2022-08-11 21:07:54 -07:00
Austin Chen
7ad8af848a Replace DaisyUI buttons with TailwindUI buttons
Maybe this should use the button component...? But that's styled differently, the rest of /create uses standard TailwindUI
2022-08-11 20:54:12 -07:00
Marshall Polaris
e2eae01ad8
Add a shitload of logging to the server auth code (#749) 2022-08-11 20:46:18 -07:00
Marshall Polaris
38d9e8190c
Only load portfolio history inside user page bets tab (#747) 2022-08-11 20:44:51 -07:00
Austin Chen
af4c442105 Support Twitch video and channel embeds 2022-08-11 20:23:33 -07:00
Austin Chen
9311652bed
Support Youtube, Tweet, and Metaculus embeds in editor (#744)
* Embed a tweet by URL

* Clean up imports

* Prevent tweetId from getting interpreted as a number

* Use a single place to embed iframe, Youtube, and Tweets

* Support Manifold and Metaculus embeds

* Remove unused import

* Simplify Manifold paste logic

* Clean up embed rewrite code

* Add back comment

* Rejigger config so tsx is only in web/

* Clean up comment

* Revert unnecessary tsconfig change

* Fix placeholder

* Lighten placeholder
2022-08-11 20:18:01 -07:00
Sinclair Chen
daba28423a
Improve create page UI (#746)
* Adjust create page styles

* Keep answers when switching market type
2022-08-11 14:41:21 -07:00
Austin Chen
dc95587cca
Add editor toolbar to choose and embed markets (#702)
* Embed markets using the "add markets" template

* Override dev domain

* Improve market modal style

- contract searchbar now sticky
- entire card clickable to select (if quickbet is hidden)
- adjust selected card styles

* remove extra export

* Hide pills

* Fix browser redirect warning

* Insert all markets instead of just one

* fix type error

* fixup "Insert all markets instead of just one"

Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
2022-08-11 14:32:02 -07:00
Marshall Polaris
4e8b94a28c
Componentize confetti to isolate re-renders due to window size (#740)
* Componentize confetti to isolate re-renders due to window size

* Clean up debug logging
2022-08-11 12:55:25 -07:00
Marshall Polaris
b9f347b7f4
Use UserFollowButton instead of FollowButton in UserPage (#742) 2022-08-11 12:54:09 -07:00
Marshall Polaris
ad75ecdc87
Move liquidity provision fetch down into ContractTabs (#741) 2022-08-11 12:53:54 -07:00
Marshall Polaris
daa86fa330
Change tabs to keep all individual tab components in the DOM (#743) 2022-08-11 12:53:42 -07:00
Sinclair Chen
99326eb65a fix spacing of long group names on markets 2022-08-11 12:30:34 -07:00
James Grugett
3f6ca6c8ed Make Manifold account able to resolve markets 2022-08-11 00:38:15 -05:00
James Grugett
6e93f11a59 Fix bolded group chat not getting unbolded 2022-08-11 00:00:40 -05:00
James Grugett
61ae481a03 Document cancel bet 2022-08-10 18:43:11 -05:00
Marshall Polaris
8c537537a1
Add cache headers to avatars (#737)
* Set cache headers on newly uploaded avatars

* Go fix up all the old avatars to have cache headers
2022-08-10 11:03:55 -07:00
Austin Chen
b5b77be188 Accept URLs in the iframe editor
TODO: Update placeholder text to mention this
2022-08-10 11:03:39 -07:00
James Grugett
d7b021b79f Clear entered limit probs on submit limit order 2022-08-10 12:37:51 -05:00
James Grugett
654790315c Fix missing key console error 2022-08-10 12:33:29 -05:00
mantikoros
35df201e2e prob bar for multiple choice 2022-08-10 12:32:30 -05:00
SirSaltyy
e591de8b29
Increase description max length (#739) 2022-08-11 02:31:28 +09:00
James Grugett
4d953d58a1 Move group chat back into a tab 2022-08-10 12:28:02 -05:00
mantikoros
dc26db2864 add salem to sidebar; clean up code 2022-08-10 12:17:40 -05:00
jahooma
3d30a1adbc Auto-prettification 2022-08-10 17:06:34 +00:00
James Grugett
05c9d3513a Don't reference window outside useEffect or click event. 2022-08-10 12:05:56 -05:00
mantikoros
52a2a3d842 track search 2022-08-10 11:50:21 -05:00
Marshall Polaris
521c479abf Fix an embarrassing bug in getPrivateUser 2022-08-09 23:55:41 -07:00
Marshall Polaris
818c90a95e
Refactor tipper (#734)
* Clean up tipping components

* Pass comment into tip callback
2022-08-09 23:05:56 -07:00
mantikoros
5f77a026aa fix modal 2022-08-09 21:59:40 -05:00
mantikoros
63538ae925 referral link on user page, manalinks, market share dialog; native sharer on mobile 2022-08-09 21:51:08 -05:00
Sinclair Chen
0b9ca6b7ee
Editor improvements (#735)
* Allow focus on all parts of editor

* Fix background and text colors

* Restrict height of image in comment

* Remove "Type *markdown*" placeholder

it's a little misleading (can't do markdown links) and messes with focus

to be replaced with a highlight menu in the future
2022-08-09 19:04:55 -07:00
Marshall Polaris
c07daafb8d
Make homepage load user via SSR, pass it to contract stuff (#729) 2022-08-09 15:28:52 -07:00
Marshall Polaris
847d3d0f27
Fix efficiency problems with visibility checking code (#730)
* Fix problems with visibility checking code

* Tear out old contract tracking stuff per James

* Use `useEvent` in VisibilityObserver per James suggestion
2022-08-09 15:28:27 -07:00
Marshall Polaris
5715b0e44a
Random contract page fixups (#712)
* Remove some divs and so on

* Correctly align bet avatars and text in feed

* Extract sidebar component on contract page
2022-08-09 13:25:42 -07:00
mantikoros
1e3c5cb936 add qr code to referrals 2022-08-09 12:27:52 -05:00
Sinclair Chen
914fc476ce
Remove top/bottom margin from indented list items (#733) 2022-08-09 10:17:44 -07:00
Austin Chen
49541d3eec Stop interpolating on portfolio value graph 2022-08-09 10:08:14 -07:00
Marshall Polaris
592125b5e7
Fix broken useBets filters (#731) 2022-08-09 10:50:11 -05:00
Marshall Polaris
e7f1d3924b
Fix up several pages to load user data on the server (#722)
* Fix up several pages to load user data on the server

* Add key prop to `EditUserField`
2022-08-08 22:43:04 -07:00
Marshall Polaris
5649161348
Pass page props user to auth provider if present (#724)
* Pass page props user to auth provider if present

* Rename `user` -> `serverUser`

* Don't load from local storage if server told us a user
2022-08-08 22:42:52 -07:00
Austin Chen
fd308151b3 Disable bouncing Challenge 2022-08-08 15:24:30 -07:00
Marshall Polaris
85e55312ca
What will be removed, is removed (#721) 2022-08-08 15:05:25 -07:00
James Grugett
98806a806f Fix query params on emulator/private instance 2022-08-07 18:07:36 -07:00
James Grugett
8fb3b42ea1 Default to trending. Fix close date being opposite 2022-08-07 17:48:43 -07:00
Austin Chen
a910e5dc17 Revert "Revert "Fix a bug with expiration of refresh and custom tokens""
This reverts commit 012b67e3c5.
2022-08-07 09:57:18 -07:00
Austin Chen
012b67e3c5 Revert "Fix a bug with expiration of refresh and custom tokens"
This reverts commit abd344b951.
2022-08-07 09:56:42 -07:00
Marshall Polaris
abd344b951 Fix a bug with expiration of refresh and custom tokens 2022-08-06 19:24:50 -07:00
James Grugett
1f8aef2891 Disable challenges for private instances 2022-08-06 17:45:21 -07:00
Sinclair Chen
da977f62a9
Make added text go after img instead of replacing (#725) 2022-08-06 15:43:41 -07:00
Sinclair Chen
5892ccee97
Rich text in comments, fixed (#719)
* Revert "Revert "Switch comments/chat to rich text editor (#703)""

This reverts commit 33906adfe4.

* Fix typing after being weird on Android

Issue: character from mention gets deleted. Most weird on Swiftkey:
mention gets duplicated instead of deleting.

See https://github.com/ProseMirror/prosemirror/issues/565
https://bugs.chromium.org/p/chromium/issues/detail?id=612446

The keyboard still closes unexpectedly when you delete :(

* On reply, put space instead of \n after mention

* Remove image upload from placeholder text

- Redundant with image upload button
- Too long on mobile

* fix dependency
2022-08-06 13:39:52 -07:00
Marshall Polaris
d43b9e1836
Vercel auth phase 2 (#714)
* Add cloud function to get custom token from API auth

* Use custom token to authenticate Firebase SDK on server

* Make sure getcustomtoken cloud function is fast

* Make server auth code maximally robust

* Refactor cookie code, make set and delete more flexible
2022-08-05 20:49:29 -07:00
James Grugett
e0196f7107 Rename file contracts-list to contracts-group 2022-08-05 17:46:32 -07:00
James Grugett
b3b06896be Add loading indicator when algolia search is initially loading 2022-08-05 17:44:55 -07:00
Marshall Polaris
48ac21ffad Add comment explaining fishy token 2022-08-05 16:08:30 -07:00
Marshall Polaris
bf3ba8ac3f Remove test file 2022-08-05 16:07:02 -07:00
mqp
bba9f9a555 Auto-prettification 2022-08-05 23:03:25 +00:00
Marshall Polaris
7e0634aee0 Try using a personal access token for formatter job 2022-08-05 16:02:46 -07:00
Marshall Polaris
f05db0ef0f Give formatting workflow even more permissions... 2022-08-05 15:56:10 -07:00
Marshall Polaris
db3b0c2cf5 Give repo write permission to formatting workflow 2022-08-05 15:38:22 -07:00
Marshall Polaris
d9ddabcfd4 Commit some un-pretty code 2022-08-05 15:35:57 -07:00
Marshall Polaris
67139b99f9
Add workflow to automate prettier running on main (#720) 2022-08-05 15:33:34 -07:00
mantikoros
5e89628593 challenge bet tracking 2022-08-05 13:42:09 -07:00
mantikoros
f11c9a3341 bouncing challenge button (temporary gimmick) 2022-08-05 13:42:09 -07:00
James Grugett
ced404eb74 Local search filters on groups, exclude contractIds 2022-08-05 12:01:16 -07:00
Ian Philips
60ebadbbe5 Add not about donating winnings to charity 2022-08-05 09:58:02 -06:00
Ian Philips
f47b70dd3c Darken numeric preview text 2022-08-05 07:08:41 -06:00
Ian Philips
de6d5b388a Lint 2022-08-05 06:58:39 -06:00
Ian Philips
1c80bf1faf Chat icon => users icon 2022-08-05 06:58:29 -06:00
Ian Philips
97e3de4e0f Show numeric values in card preview 2022-08-05 06:56:10 -06:00
Ian Philips
d90901b4e3 Check creator balance again upon acceptance 2022-08-05 05:03:47 -06:00
mantikoros
f3704633ee liquidity panel styling 2022-08-05 00:03:38 -07:00
mantikoros
5988dd1e48 improved create challenge modal; 2xs button 2022-08-04 23:42:35 -07:00
mantikoros
16f4fb9490 disable clicking on group in embed 2022-08-04 22:47:59 -07:00
mantikoros
4d153755c1 delete challenge button 2022-08-04 22:33:56 -07:00
mantikoros
1e66f4d140
Share row (#715)
* Challenge bets

* Store avatar url

* Fix before and after probs

* Check balance before creation

* Calculate winning shares

* pretty

* Change winning value

* Set shares to equal each other

* Fix share challenge link

* pretty

* remove lib refs

* Probability of bet is set to market

* Remove peer pill

* Cleanup

* Button on contract page

* don't show challenge if not binary or if resolved

* challenge button (WIP)

* fix accept challenge: don't change pool/probability

* Opengraph preview [WIP]

* elim lib

* Edit og card props

* Change challenge text

* New card gen attempt

* Get challenge on server

* challenge button styling

* Use env domain

* Remove other window ref

* Use challenge creator as avatar

* Remove user name

* Remove s from property, replace prob with outcome

* challenge form

* share text

* Add in challenge parts to template and url

* Challenge url params optional

* Add challenge params to parse request

* Parse please

* Don't remove prob

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Add to readme about how to dev og-image

* Add emojis

* button: gradient background, 2xl size

* beautify accept bet screen

* update question button

* Add separate challenge template

* Accepted challenge sharing card, fix accept bet call

* accept challenge button

* challenge winner page

* create challenge screen

* Your outcome/cost=> acceptorOutcome/cost

* New create challenge panel

* Fix main merge

* Add challenge slug to bet and filter by it

* Center title

* Add helper text

* Add FAQ section

* Lint

* Columnize the user areas in preview link too

* Absolutely position

* Spacing

* Orientation

* Restyle challenges list, cache contract name

* Make copying easy on mobile

* Link spacing

* Fix spacing

* qr codes!

* put your challenges first

* eslint

* Changes to contract buttons and create challenge modal

* Change titles around for current bet

* Add back in contract title after winning

* Cleanup

* Add challenge enabled flag

* Spacing of switch button

* market share row

* Add lite market endpoint

* 500 mana email (#687)

* Create 500-mana.html

* Update 500-mana.html

Fixed typos and links not working

* Added "create a good market" guide

added page creating-market.html
For Stephen to set up condition (email 3 days after signing up)

* Update 500-mana.html

updated 500 Mana email (still need to make changes to create market guide)

* email changes

* sendOneWeekBonusEmail logic

* add dayjs as dependency

* don't use mailgun scheduling

Co-authored-by: mantikoros <sgrugett@gmail.com>

* Challenge Bets (#679)

* Challenge bets

* Store avatar url

* Fix before and after probs

* Check balance before creation

* Calculate winning shares

* pretty

* Change winning value

* Set shares to equal each other

* Fix share challenge link

* pretty

* remove lib refs

* Probability of bet is set to market

* Remove peer pill

* Cleanup

* Button on contract page

* don't show challenge if not binary or if resolved

* challenge button (WIP)

* fix accept challenge: don't change pool/probability

* Opengraph preview [WIP]

* elim lib

* Edit og card props

* Change challenge text

* New card gen attempt

* Get challenge on server

* challenge button styling

* Use env domain

* Remove other window ref

* Use challenge creator as avatar

* Remove user name

* Remove s from property, replace prob with outcome

* challenge form

* share text

* Add in challenge parts to template and url

* Challenge url params optional

* Add challenge params to parse request

* Parse please

* Don't remove prob

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Add to readme about how to dev og-image

* Add emojis

* button: gradient background, 2xl size

* beautify accept bet screen

* update question button

* Add separate challenge template

* Accepted challenge sharing card, fix accept bet call

* accept challenge button

* challenge winner page

* create challenge screen

* Your outcome/cost=> acceptorOutcome/cost

* New create challenge panel

* Fix main merge

* Add challenge slug to bet and filter by it

* Center title

* Add helper text

* Add FAQ section

* Lint

* Columnize the user areas in preview link too

* Absolutely position

* Spacing

* Orientation

* Restyle challenges list, cache contract name

* Make copying easy on mobile

* Link spacing

* Fix spacing

* qr codes!

* put your challenges first

* eslint

* Changes to contract buttons and create challenge modal

* Change titles around for current bet

* Add back in contract title after winning

* Cleanup

* Add challenge enabled flag

* Spacing of switch button

* Put sharing qr code  in modal

Co-authored-by: mantikoros <sgrugett@gmail.com>

* See challenges you've accepted too

* Remove max height

* Notify mentioned users on market publish (#683)

* Add function to parse at mentions

* Notify mentioned users on market create

- refactor createNotification to accept list of recipients' ids

* Switch comments/chat to rich text editor (#703)

* Switch comments/chat to rich text editor

* Remove TruncatedComment

* Re-add submit on enter

* Insert at mention on reply

* Update editor style for send button

* only submit on enter in chat

* code review: refactor

* use more specific type for upload

* fix ESlint and errors from merge

* fix trigger on every render eslint warning

* Notify people mentioned in comment

* fix type errors

* Revert "Switch comments/chat to rich text editor (#703)"

This reverts commit f52da72115.

* merge conflict

* share modal

* merge issue

* eslint

* bigger link icion

Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: James Grugett <jahooma@gmail.com>
Co-authored-by: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com>
Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
2022-08-05 00:22:45 -05:00
James Grugett
33906adfe4 Revert "Switch comments/chat to rich text editor (#703)"
This reverts commit f52da72115.
2022-08-04 16:49:59 -07:00
Sinclair Chen
f52da72115
Switch comments/chat to rich text editor (#703)
* Switch comments/chat to rich text editor

* Remove TruncatedComment

* Re-add submit on enter

* Insert at mention on reply

* Update editor style for send button

* only submit on enter in chat

* code review: refactor

* use more specific type for upload

* fix ESlint and errors from merge

* fix trigger on every render eslint warning

* Notify people mentioned in comment

* fix type errors
2022-08-04 16:34:04 -07:00
Sinclair Chen
edae709f5f
Notify mentioned users on market publish (#683)
* Add function to parse at mentions

* Notify mentioned users on market create

- refactor createNotification to accept list of recipients' ids
2022-08-04 15:35:55 -07:00
Ian Philips
912ccad530 Remove max height 2022-08-04 16:09:33 -06:00
Ian Philips
c93f9c5483 See challenges you've accepted too 2022-08-04 15:58:48 -06:00
Ian Philips
798253f887
Challenge Bets (#679)
* Challenge bets

* Store avatar url

* Fix before and after probs

* Check balance before creation

* Calculate winning shares

* pretty

* Change winning value

* Set shares to equal each other

* Fix share challenge link

* pretty

* remove lib refs

* Probability of bet is set to market

* Remove peer pill

* Cleanup

* Button on contract page

* don't show challenge if not binary or if resolved

* challenge button (WIP)

* fix accept challenge: don't change pool/probability

* Opengraph preview [WIP]

* elim lib

* Edit og card props

* Change challenge text

* New card gen attempt

* Get challenge on server

* challenge button styling

* Use env domain

* Remove other window ref

* Use challenge creator as avatar

* Remove user name

* Remove s from property, replace prob with outcome

* challenge form

* share text

* Add in challenge parts to template and url

* Challenge url params optional

* Add challenge params to parse request

* Parse please

* Don't remove prob

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Add to readme about how to dev og-image

* Add emojis

* button: gradient background, 2xl size

* beautify accept bet screen

* update question button

* Add separate challenge template

* Accepted challenge sharing card, fix accept bet call

* accept challenge button

* challenge winner page

* create challenge screen

* Your outcome/cost=> acceptorOutcome/cost

* New create challenge panel

* Fix main merge

* Add challenge slug to bet and filter by it

* Center title

* Add helper text

* Add FAQ section

* Lint

* Columnize the user areas in preview link too

* Absolutely position

* Spacing

* Orientation

* Restyle challenges list, cache contract name

* Make copying easy on mobile

* Link spacing

* Fix spacing

* qr codes!

* put your challenges first

* eslint

* Changes to contract buttons and create challenge modal

* Change titles around for current bet

* Add back in contract title after winning

* Cleanup

* Add challenge enabled flag

* Spacing of switch button

* Put sharing qr code  in modal

Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-08-04 15:27:02 -06:00
SirSaltyy
2d3ca47b52
500 mana email (#687)
* Create 500-mana.html

* Update 500-mana.html

Fixed typos and links not working

* Added "create a good market" guide

added page creating-market.html
For Stephen to set up condition (email 3 days after signing up)

* Update 500-mana.html

updated 500 Mana email (still need to make changes to create market guide)

* email changes

* sendOneWeekBonusEmail logic

* add dayjs as dependency

* don't use mailgun scheduling

Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-08-04 13:03:02 -05:00
James Grugett
7e46188107 Add lite market endpoint 2022-08-03 22:21:22 -07:00
Ian Philips
d83e103fab Ignore clicks when hidden 2022-08-03 18:42:40 -06:00
Ian Philips
5bc905b358 Bottom padding works on mobile, broken on desktop :( 2022-08-03 16:42:51 -06:00
Ian Philips
b4c6b99e09 Remove bottom bar height correction 2022-08-03 16:38:00 -06:00
Ian Philips
756115ba54 Link tags aren't hiding sidebar on click 2022-08-03 16:30:05 -06:00
Ian Philips
fab83cfc33 Don't auotfocus on mobile 2022-08-03 16:16:46 -06:00
Ian Philips
aa3101baa9 Fix group chat padding 2022-08-03 16:10:02 -06:00
Ian Philips
82419d0b92
Groups chat ux (#713)
* Add in group chat bubble

* Show chat bubble on nav with unseen notifs

* Spacing

* More spacing

* Remove chat tab

* Show chat on help/welcome/updates/features groups

* Cleanup

* Scroll with updated height
2022-08-03 15:38:35 -06:00
James Grugett
a7d80d62cb Don't show cancel button for other people's limit orders 2022-08-03 14:30:59 -07:00
James Grugett
a761f8c65e Hide pills while searching 2022-08-03 14:17:45 -07:00
James Grugett
280308b625 Show # of bets equal to visible bets 2022-08-02 17:40:34 -07:00
James Grugett
b5d8acfef3 Switch profit in bets tab to match user page 2022-08-02 17:31:49 -07:00
James Grugett
3c9108de0d Document creating a limit order in API 2022-08-02 17:01:31 -07:00
Ian Philips
c24b4e77a8 Lint 2022-08-02 17:24:59 -06:00
James Grugett
d45edb7887 Add WagerWith.me and James' Bot to Awesome Manifold 2022-08-02 16:21:07 -07:00
Ian Philips
e700697423 Fix group referrals not working 2022-08-02 17:18:08 -06:00
Marshall Polaris
f8a74aa438
Allow admins to resolve any market (#711) 2022-08-02 15:34:20 -07:00
mantikoros
6563082746 move claim button 2022-08-02 15:22:53 -07:00
mantikoros
5e8b9711dc hide pagination if only one page 2022-08-02 15:22:53 -07:00
mantikoros
96c0876053 manalinks: fix focus 2022-08-02 15:22:53 -07:00
mantikoros
164d9ef079 manalinks: mention referral bonus 2022-08-02 15:22:53 -07:00
Austin Chen
53d89fa4ac Show the value to 2 decimal places on hover 2022-08-02 14:59:47 -07:00
Marshall Polaris
b83caf4dd9 Just make me endpoint forward the backend response 2022-08-02 00:21:51 -07:00
Keri Warr
cfeb50826c
Add endpoint for reading currently authenticated user (#710) 2022-08-02 00:06:23 -07:00
Marshall Polaris
6901507461
Allow unspecfied outcome as input to sellshares (#706)
* Allow unspecfied outcome as input to `sellshares`

* Fix small details
2022-08-01 23:53:12 -07:00
Ian Philips
0b06ded5e5
Groups contracts (#709)
* Update group links in trigger and api

* Remove extra call during creation

* Remove grouplinks on frontend

* Deserialize

* Consolidate logic

* Move function locally
2022-08-01 21:15:09 -06:00
Marshall Polaris
b4e8c5d602
Backfill missing group IDs (#708) 2022-08-01 16:40:04 -06:00
Marshall Polaris
ec84245dd4
Add some API endpoints for reading group info (#707) 2022-08-01 14:59:45 -07:00
Ian Philips
0819c3918f Most popular=> Trending 2022-08-01 09:03:46 -06:00
ingawei
ae2e7dfe30
noobify welcome demo (#699)
* noobify welcome
* also beginning to add greyscale color scheme
2022-07-30 02:50:03 -05:00
James Grugett
87f6949d80 Use custom search index for search results. Hide sort options while there's a query. 2022-07-29 18:13:53 -07:00
James Grugett
003301762c Ignore filter on contract status when searching 2022-07-29 17:37:53 -07:00
James Grugett
d6cf4332da Delete query param when empty 2022-07-29 17:37:34 -07:00
James Grugett
be01a15230
Refactor search to not use Algolia components (#705)
* In progress refactor to not use Algolia components

* Cleanup

* Only query when necessary

* Read and update url params for query and sort

* Don't push router

* Don't update url if using default sort

* Add popstate listener to improve home navigation

* Remove console.logs
2022-07-29 19:08:51 -05:00
mantikoros
079a2a3936 eslint 2022-07-29 16:06:22 -07:00
mantikoros
5812d8ed2e manalink qr code 2022-07-29 16:02:18 -07:00
mantikoros
779b6dfc0c manalink referrals 2022-07-29 15:09:48 -07:00
mantikoros
bdea739c55 multiple choice contract card 2022-07-29 09:20:49 -07:00
Austin Chen
693eb96d23 Include groupId when duplicating markets 2022-07-29 09:20:01 -07:00
mantikoros
ada3f0787c create: add bottom margin 2022-07-28 11:44:07 -07:00
Ian Philips
d3da6de5dd Groups default open 2022-07-28 11:37:26 -07:00
marsteralex
aa6d0d1750
add beasts (#700)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-07-28 11:31:58 -07:00
Ian Philips
05b0ca5cdb I want to see others' referrals 2022-07-28 11:16:48 -07:00
mantikoros
b6a70641a0 fix modal 2022-07-27 19:51:34 -07:00
mantikoros
1aaae93113
Multiple choice markets (#698)
* multipe choice answers

* create multiple choice cloud function

* multi choice market page

* show outcome '0'

* stats: multi choice type

* update place bet

* answer doc id = outcome

* update resolve market

* prettier

* fix

* fix resolution
2022-07-27 21:40:33 -05:00
James Grugett
b1c4f018f9 Expose cancel bet api 2022-07-27 17:38:25 -07:00
Ian Philips
013ff1d941 Show api error on create contract 2022-07-26 16:44:51 -07:00
Ian Philips
f32e995baa Show referrals banner on user-page 2022-07-26 15:24:16 -07:00
Marshall Polaris
b506e96548
Implement "sell all shares" functionality in sellshares and expose API (#696)
* Change `sellshares` to be able to sell all shares

* Sell all shares properly on bet panel UI

* Add API route for selling shares, document
2022-07-26 12:47:19 -07:00
Marshall Polaris
ad46a60c4f
Clean up rendering of user bets list (#694)
* Clean up crufty markup in bets list

* Don't render bet tables in bets list until expanded

* Don't look up unfilled bets for every sell button
2022-07-26 00:10:22 -07:00
Marshall Polaris
7e4f4b9a87
Clean up a bunch of crufty stuff on user comments list (#693) 2022-07-26 00:10:11 -07:00
SirSaltyy
0c2bcceae2
Update charity.ts (#695)
Added Founder's Pledge Global Health and Development Fund.

Made new logos for all the Founder's Pledge charities to help distinguish between them.
2022-07-26 11:10:22 +09:00
Ian Philips
af25a6c795 Allow adding multiple contracts to group in modal 2022-07-25 18:27:43 -07:00
mantikoros
ec0e25e5ed create user: remove ip check 2022-07-25 18:25:23 -07:00
mantikoros
3107c8fe30 large bet warning 2022-07-25 18:11:29 -07:00
mantikoros
24124ac86a show sign up button on mobile on market page 2022-07-25 17:45:42 -07:00
Marshall Polaris
06948bb98b
Make setNotificationsAsSeen return a promise (#692) 2022-07-25 16:37:23 -07:00
Marshall Polaris
64462d6ab4
Make tabs components better (#691)
* Make better tabs components, apply to user page

* Remove fishy unused href property from tabs

* Remove tab ID property

* Clean up crufty markup in tabs component

* Fix naming to be right (thanks James!)
2022-07-25 13:27:09 -07:00
TrueMilli
e4f8c14fab
Image compression (#689)
* added image compression

* removed TODO
2022-07-25 12:51:51 -07:00
mantikoros
d8f96876a0 PlayMoneyDisclaimer copy; hide order book for signed out users 2022-07-25 12:29:29 -07:00
mantikoros
d82c7d7f3e “added liquidity” ⇒ “added a subsidy” 2022-07-25 12:22:38 -07:00
mantikoros
d982d0332c play money wording 2022-07-24 23:38:57 -07:00
mantikoros
df91310d0f PlayMoneyDisclaimer; hide limit orders for signed out users; infobox styling 2022-07-24 23:28:05 -07:00
mantikoros
e389f4cc3b referrals text 2022-07-24 22:50:33 -07:00
Marshall Polaris
9840742927 Fix overaggressive emulator running in dev.sh 2022-07-24 02:30:28 -07:00
Marshall Polaris
312b244e2a
Small backend cleanups (#643)
* Reuse DAY_MS in update-metrics job

* More concise transaction in cancelbet

* Remove some meaningless awaits

* Do less work in onCreateLiquidityProvision

* Do less work in onCreateAnswer
2022-07-24 00:45:45 -07:00
Marshall Polaris
a1d5d161dd
Revamp backend code to support good local function development (#657)
* Move concurrently dep upwards

* Add express as explicit dependency

* Accept just one HTTP method per endpoint

* Fix endpoint option coalescing

* Expressification of cloud functions

* Nicer logging of API requests

* Refactor web package.json

* Add ts-node and nodemon to dev dependencies, bring back cors

* Add scaffolding to point dev server at local functions

* Enable emulator in dev server scaffolding

* Fix up a little stuff I broke
2022-07-24 00:26:38 -07:00
Austin Chen
6ad43b02c7 Show the number of comments and bets 2022-07-24 00:11:35 -07:00
Olivia Appleton
1f655acddb
Add my market manager tool (#690) 2022-07-23 23:33:19 -07:00
Sinclair Chen
6c89e5f18f
Add @ mentions to editor (#670)
* Add @ mentions to editor

* Fix mention list not loading

* Sort mention list by prefix, follow count

* Render at mention with Linkify component

- mentions are now Next <Link> rather than <a>
- fix bug where editor.getText() returns [object Object] for mentions
- fix mention rendering for posted markets
2022-07-23 20:37:34 -07:00
Ian Philips
f4e4582913 Add group slug during create 2022-07-23 15:04:11 -06:00
Marshall Polaris
6c8c068327
Write script to fix old comments without IDs and user IDs (#680) 2022-07-23 13:48:28 -07:00
James Grugett
64f2dbbe71 Fix unused var 2022-07-23 15:26:08 -05:00
James Grugett
f43df42449 Change card to show volume instead of pool 2022-07-23 15:23:47 -05:00
James Grugett
71b20eb61a Tweak visually hidden style 2022-07-23 15:10:54 -05:00
James Grugett
7f42796724 Update algolia filters to use groupLinks.slug isntead of deprecated groupSlugs field. 2022-07-23 15:02:08 -05:00
Austin Chen
71880dfc98
Add a toolbar for images and iframes (#688)
* Add a toolbar for images and iframes

* Insert embed code via modal
2022-07-23 09:19:49 -07:00
James Grugett
408027dd6a Fix bug 2022-07-22 22:44:21 -05:00
James Grugett
2116b86aec Fix infinite loop in numeric limit bet 2022-07-22 21:03:08 -05:00
Ian Philips
56a579ff91 Don't filter for group contract ids 2022-07-22 16:44:03 -06:00
Ian Philips
abde013ab6 Re-get contracts to get updated links 2022-07-22 16:40:37 -06:00
Ian Philips
5f074206de
Backfill and forward fill contracts with group info (#686)
* Backfill and forward fill contracts with group info

* No nested queries :(

* Fix filter

* Pass empty arrays instead of undefined
2022-07-22 16:28:53 -06:00
James Grugett
5899c1f3c0 Fix lints 2022-07-22 16:30:07 -05:00
James Grugett
135160dd92 Remove custom placeholders. Just show '0' for limit inputs 2022-07-22 16:18:36 -05:00
James Grugett
a1d51e3778 Update labels for numeric market outcomes 2022-07-22 16:07:59 -05:00
James Grugett
f800570845 Improve range limit order UI 2022-07-22 16:03:55 -05:00
Ian Philips
d319b654ce Add creator id to unique bettor ids 2022-07-22 14:15:42 -06:00
Ian Philips
63d8e6739b Add title, mobile flex 2022-07-22 13:53:19 -06:00
James Grugett
d3d472f5d2 Hide "Your bets" when signed out. "For you" becomes "Featured" when signed out. 2022-07-22 14:50:29 -05:00
Ian Philips
6fb9849007
Allow to add/remove from groups on market page (#685)
* Allow to add/remove from groups on market page

* remove lib

* Fix Sinclair's relative import from May

* Clean
2022-07-22 11:34:10 -06:00
mantikoros
163c990e9d "bettors" => "traders" 2022-07-22 12:03:33 -05:00
mantikoros
c3a0326b1e homepage seo 2022-07-22 12:01:52 -05:00
mantikoros
e13f4d3d4d charity description 2022-07-22 11:59:25 -05:00
mantikoros
2c80133856 add SEO tags to everything 2022-07-22 11:56:03 -05:00
mantikoros
de53a13c84 fix referrals seo 2022-07-22 11:25:48 -05:00
mantikoros
624df76393 search: sort by liquidity; remove oldest 2022-07-22 11:24:25 -05:00
Austin Chen
7cace82b83
Render iframes inside the rich text editor (#682)
* Try embedding iframes in tiptap

* When iframe code is pasted, inject it into the editor

* Code cleanups and comments

* Remove clsx dependency

Cuz it doesn't exist in `common` anyways

* Rename to tiptap-iframe
2022-07-22 09:12:23 -07:00
Austin Chen
87170894e2 Suppress eslint warning for script 2022-07-22 09:12:01 -07:00
Ian Philips
83cb0a6130 Allow clickable username in welcome message 2022-07-22 08:19:06 -06:00
Ian Philips
bfb11339ca Convert world and culture categories 2022-07-22 08:12:40 -06:00
Marshall Polaris
08fd27cb26
Make main login/logout buttons reload server side props (#677)
* Set cookies in auth handler before looking up user

* Make sidebar logout button trigger SSR reload

* Make sidebar login button trigger SSR reload
2022-07-22 00:03:16 -07:00
James Grugett
3b953a7c21
Range limit orders (#655)
* Prototype range limit order UI

* Conditionally show YES or NO max payout

* Range bet executes both bets immediately.

* Validate lowLimitProb < highLimitProb

* Show error if low limit is higher than high limit

* Update range order UI

* Revert "Validate lowLimitProb < highLimitProb"

This reverts commit c261fc2743.

* Revert "Range bet executes both bets immediately."

This reverts commit 30b95d75d9.

* Buy panel only non-limit orders

* Bet choice => outcome

* More iterating on range UI

* betChoice => outcome

* Lighten placeholder text
2022-07-22 00:57:56 -05:00
James Grugett
23b704ffe0 Fix excessive bottom margin on chart 2022-07-21 21:51:20 -05:00
James Grugett
ca5ca9b2b8 Refactor: Move ContractLeaderboard to its own file 2022-07-21 21:39:06 -05:00
ingawei
7474c0a0fd
Inga/manalinks pagination bug (#678)
* manalink pagination fix
* also fixed new manalink timing out bug
2022-07-21 20:22:17 -05:00
Sinclair Chen
4b4734531f
refactor createNotif - put ?: args in object (#681) 2022-07-21 17:08:09 -07:00
mantikoros
cded3f50ff "question" => "market" (controversial!) 2022-07-21 18:17:02 -05:00
mantikoros
80b27fdf6e Put leadeboards back in sidebar 2022-07-21 17:23:55 -05:00
Ian Philips
daca6ef482 Bonus=>10 2022-07-21 15:33:21 -06:00
mantikoros
91bec9c996
Referrals page (#676)
* Referrals page added to sidebar; useSaveReferral; InfoBox

* text color

* eslint

* migrate useUsers hook to react-query (#674)

* Remove bet button from free response comments

* Make answer replies more closely spaced together

* Host Ida and Alex's MTG Guesser game (#656)

* Copy over code from Mtg Guesser

* Run Prettier

* CSS Tweaks: Hover feedback, button positioning

* Hide all but counterspell & burn, for now

* Move to /mtg directory

* Fix prettierignore

* smaller jsons (#673)

limited burn to only red cards and also added limited json files to only have fields needed to play

* Add Ida's tweak to card position

Co-authored-by: marsteralex <bob.masteralex@gmail.com>

* Adjust card positioning

* Make the select screen index.html

* Remove other guessing games

* Remove alternate versions; add Alex's email

* Remove unused jsons

* Small FR comments tweaks

* Spacing tweak

* Changing manalinks table UI (#665)

From table to card view

* Fix comment spacing on non-FR

* Move "Send M$" lower in sidebar More list.

* Move leaderboards up in mobile nav

* eslint

* prettier

Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
Co-authored-by: James Grugett <jahooma@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: marsteralex <bob.masteralex@gmail.com>
Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
2022-07-21 14:43:10 -05:00
mantikoros
96e9f749d2 track search categories 2022-07-21 12:45:47 -05:00
Austin Chen
6603effd1b
Use https for hotlinked image
Editing main from my phone, fingers crossed
2022-07-21 01:16:21 -07:00
Marshall Polaris
03858e4a8c
Make a React context to be the source of truth for authenticated user (#675)
* Make a React context to manage the logged in user events

* Remove unnecessary new user creation promise machinery

* Slight refactoring to auth context code

* Improvements in response to James feedback
2022-07-21 00:38:26 -07:00
James Grugett
8aa360c853 Move leaderboards up in mobile nav 2022-07-21 00:52:11 -05:00
James Grugett
21c08aed30 Move "Send M$" lower in sidebar More list. 2022-07-21 00:50:28 -05:00
James Grugett
2ad7266283 Fix comment spacing on non-FR 2022-07-21 00:46:57 -05:00
ingawei
7a041fd753
Changing manalinks table UI (#665)
From table to card view
2022-07-21 00:45:53 -05:00
James Grugett
f7151f131d Spacing tweak 2022-07-20 22:37:43 -05:00
James Grugett
8f5e51a304 Small FR comments tweaks 2022-07-20 22:13:37 -05:00
Austin Chen
aba818a9de Remove unused jsons 2022-07-20 18:05:41 -07:00
Austin Chen
260f4641dd Remove alternate versions; add Alex's email 2022-07-20 18:04:54 -07:00
Austin Chen
edee910e2d Remove other guessing games 2022-07-20 18:00:18 -07:00
Austin Chen
6b5b9b42f5 Make the select screen index.html 2022-07-20 17:14:49 -07:00
Austin Chen
c3b825cc44 Adjust card positioning 2022-07-20 16:59:40 -07:00
Austin Chen
a3f150b1d9
Host Ida and Alex's MTG Guesser game (#656)
* Copy over code from Mtg Guesser

* Run Prettier

* CSS Tweaks: Hover feedback, button positioning

* Hide all but counterspell & burn, for now

* Move to /mtg directory

* Fix prettierignore

* smaller jsons (#673)

limited burn to only red cards and also added limited json files to only have fields needed to play

* Add Ida's tweak to card position

Co-authored-by: marsteralex <bob.masteralex@gmail.com>
2022-07-20 16:57:51 -07:00
James Grugett
528dd2b28a Make answer replies more closely spaced together 2022-07-20 18:35:09 -05:00
James Grugett
5ddf496dae Remove bet button from free response comments 2022-07-20 18:35:09 -05:00
Sinclair Chen
aa554ca9f6
migrate useUsers hook to react-query (#674) 2022-07-20 16:31:18 -07:00
Marshall Polaris
ace39ef73d
Update Next.js 12.1.2 -> 12.2.0 (#669)
* Update Next.js 12.1.2 -> 12.2.0

* Further bump Next to 12.2.2
2022-07-20 15:42:31 -07:00
mantikoros
49dcd97d70 feed bets: better prob display 2022-07-20 17:04:11 -05:00
mantikoros
75a1d606cb feed bets: show change in prob 2022-07-20 16:28:25 -05:00
mantikoros
302a635542 group page max width 2022-07-20 16:06:31 -05:00
James Grugett
c35d0a8bc6 Split out "Your bets" from "For you" 2022-07-20 15:30:07 -05:00
James Grugett
44afa92b58 Turn off for you by default 2022-07-20 15:05:48 -05:00
James Grugett
e45d81513c Don't filter by personal unless pills enabled 2022-07-20 14:49:16 -05:00
Sinclair Chen
0870397fea Show line in menu on mobile 2022-07-20 12:36:23 -07:00
Sinclair Chen
202132868f lint and prettier 2022-07-20 12:35:04 -07:00
mantikoros
d65a60984d make group invite link more prominent 2022-07-20 11:45:53 -05:00
mantikoros
45b883477d generic copy link button 2022-07-20 11:42:49 -05:00
mantikoros
b60892fada group 'rankings' => 'leaderboards' (friendlier, more consistent terminology) 2022-07-20 11:15:55 -05:00
Marshall Polaris
c8361f1748
Make it so that if you sign in on / you get redirected to /home (#672) 2022-07-20 01:59:14 -07:00
mantikoros
b517f7cfa7 eslint; remove unused tags import 2022-07-20 00:35:27 -05:00
mantikoros
2b13085dff search: use default categories for non-authed users 2022-07-20 00:23:00 -05:00
mantikoros
0013f76873 search defaults to 'for you'; hide pills for additional filters 2022-07-20 00:03:03 -05:00
mantikoros
83e9408d69 remove tags from info panel 2022-07-19 23:48:09 -05:00
Marshall Polaris
bacd546e5d Fix unused import from Ian's code 2022-07-19 20:10:54 -07:00
Marshall Polaris
61094ea17d
Properly handle expired ID token cookie, be robust to errors (#671) 2022-07-19 20:08:33 -07:00
James Grugett
b2c89d36cf Home: Show pills that are groups (in addition to All, For you) 2022-07-19 18:23:52 -05:00
Ian Philips
921ac4b2a9 Fix last 3 days number 2022-07-19 17:22:23 -06:00
mantikoros
b48e910f70 fix areaBaselineValue 2022-07-19 18:20:03 -05:00
mantikoros
bab828412b group: add question button 2022-07-19 18:16:03 -05:00
Ian Philips
1f0983a145 Find old contracts to decrement score on 2022-07-19 17:08:51 -06:00
Ian Philips
4aface583d Remove pesky loading spinner 2022-07-19 16:41:11 -06:00
Ian Philips
2152e5286a Score & sort by unique bettors in last 3 days 2022-07-19 16:29:41 -06:00
James Grugett
58d6286361 Fix chart area extending into labels below 2022-07-19 17:22:58 -05:00
James Grugett
6124ea01f6 Fix a DOM error in console 2022-07-19 16:57:32 -05:00
Marshall Polaris
6d3490cd68
Turn off Next.js font inlining (#668) 2022-07-19 14:20:23 -07:00
James Grugett
af6552958f Show all-time profit on UserPage 2022-07-19 16:05:50 -05:00
Marshall Polaris
f9aab39039
Clean up font loading and see if it fixes our problem (#667) 2022-07-19 13:57:32 -07:00
James Grugett
fc9e261601 Fix build 2022-07-19 15:45:47 -05:00
James Grugett
e9ad30cc74 Increase number of contracts shown in bets list 2022-07-19 15:43:37 -05:00
James Grugett
2684c8bcca Default to weekly leaderboard 2022-07-19 15:39:40 -05:00
James Grugett
6c070464dd Use static props to load leaderboard fast 2022-07-19 15:39:17 -05:00
Ian Philips
b5ef7490c3 NotificationSettings to its own file 2022-07-19 14:24:36 -06:00
Ian Philips
6dcad6225b Next/Previous => Older/Newer 2022-07-19 14:16:20 -06:00
James Grugett
b6c8390a46 Show order book button even on Quick bet 2022-07-19 15:01:15 -05:00
Ian Philips
a1e03c3a25 Allow opening notifs in new tabs, return newest notifs 2022-07-19 13:58:51 -06:00
James Grugett
93b9ace477 Comment email: Subject no longer varies between questions so emails are threaded in gmail 2022-07-19 14:54:42 -05:00
James Grugett
74760b1062 Reorder orderbook columns 2022-07-19 14:53:33 -05:00
Marshall Polaris
61cbb07bd5
Fix some broken stuff on the homepage contract search routing (#664)
* Use Next router more appropriately

* Replace instead of push when modifying search query params
2022-07-19 12:33:53 -07:00
mantikoros
12567074cc fix log scale graph 2022-07-19 12:31:26 -05:00
mantikoros
4b3370e374 fix formatting 2022-07-19 12:31:26 -05:00
Marshall Polaris
0d282a962c
Don't setQuery on group selector component during initial render (#660) 2022-07-19 09:35:43 -06:00
Ian Philips
a203f43142 Cache all notifs 2022-07-19 09:29:12 -06:00
Ian Philips
c236eb15b1
Cache notifs in local, gives instant load of old notifs (#662)
* Cache notifs in local, gives instant load of old notifs

* Small refactor, add ss auth

* unused vars

* Add back in replaceAll

* Save all notifs

* Memoize paginated notifs

* Replace all => replace with regexp
2022-07-19 09:04:47 -06:00
Austin Chen
2bae7dc200 Fix error on no portfolio history 2022-07-19 02:54:05 -07:00
Marshall Polaris
55775d9d37 Also handle case where there are no cookies yet 2022-07-19 01:35:34 -07:00
Marshall Polaris
c256e9c0cc Attempt to fix up overly sensitive cookie parsing 2022-07-19 01:33:00 -07:00
Austin Chen
f6d2c56e43 Fix /create 2022-07-19 01:23:36 -07:00
Marshall Polaris
a103a2ee2c
Initial draft of Vercel Firebase auth (#593)
* Set a cookie with an up-to-date Firebase ID token

* Implement server-side authentication cookie reading logic

* Change index page to redirect for authed users

* No branch necessary for logged in users on index page

* Add helpers for creating server-side redirects

* Add some common sense redirects
2022-07-19 00:50:11 -07:00
Austin Chen
d1ad0716c8 Fix import 2022-07-19 00:34:53 -07:00
Austin Chen
b501776e33 Remove quadratic matching from /charity 2022-07-19 00:20:18 -07:00
Austin Chen
dcd2ccae1b Allow environments to override the referral bonus 2022-07-18 23:29:32 -07:00
mantikoros
8793288dc8 contract description: less prominent edit buttons 2022-07-18 19:17:45 -05:00
Marshall Polaris
f2a7a145e4
Add React key prop to homepage filter widget (#661) 2022-07-18 18:37:46 -05:00
James Grugett
61a21d34b2 Order limit bets in sorted order on mobile 2022-07-18 18:19:30 -05:00
Austin Chen
47a27bf3fe
Label "Since June" for users who had an account prior to 2022-06-20 (#659) 2022-07-18 15:55:17 -07:00
Sinclair Chen
781de79b97 Make description text style more consistent
- links and blockquotes have light font weight, like other text
- font size in editor matches font size in description
- old descriptions have same style as new
- placeholder text matches editor style
- decrease line-height a bit
2022-07-18 14:03:05 -07:00
Ian Philips
e2a72dd0a2 Fix /create date input 2022-07-18 15:01:50 -06:00
Ian Philips
f2a16afc90 Update firestore rules for question editing 2022-07-18 14:52:28 -06:00
mantikoros
65e4f24531 groups: only change layout if sidebar chat, smaller leave button 2022-07-18 12:55:49 -05:00
Ian Philips
39c38a669e Referrals bug fix and attribute group 2022-07-18 10:40:44 -06:00
Ian Philips
db537a97ba Allow click on group card avatar 2022-07-18 08:35:59 -06:00
Ian Philips
229d270d25 Set max width on avatars 2022-07-18 08:34:20 -06:00
Ian Philips
eb4906cb97 Remove query from notif isSeen logic 2022-07-18 08:18:40 -06:00
SirSaltyy
d012561c50
Create 500-mana.html (#658) 2022-07-18 15:13:16 +01:00
Ian Philips
906cfc29c8 Endswith=>includes to handle sort query in group chat 2022-07-18 07:59:21 -06:00
Ian Philips
a247e6d0de Default to created time if no chat activity 2022-07-18 07:38:02 -06:00
Austin Chen
f393246e4f
Let users edit descriptions and questions (#654)
* Use rich text editor on the description

* Write a new line to description when the question is changed

* Stop showing categories

* Allow anyone to edit their own question
2022-07-17 22:22:44 -07:00
mantikoros
281b712258 move group chat to sidebar on desktop 2022-07-17 15:56:39 -05:00
mantikoros
b5f0b58898 usePing 2022-07-17 15:17:31 -05:00
mantikoros
07bfdadd25 remove OnlineUserList b/c of responsiveness issues 2022-07-17 14:40:21 -05:00
James Grugett
c1d77f48e3 Fix tag filter 2022-07-16 18:56:21 -05:00
James Grugett
1edc1993e1 Cache follows in localstorage 2022-07-16 14:58:25 -05:00
Austin Chen
bae55828a1 Simplify Firestore isAdmin rule 2022-07-16 12:52:59 -07:00
Austin Chen
60f4e43cf3 Prettier fix 2022-07-16 12:51:22 -07:00
mantikoros
a3975080a1 adjust sig figs 2022-07-16 14:50:16 -05:00
James Grugett
7feacbd961 Tweak wording 2022-07-16 14:37:03 -05:00
James Grugett
7b6344d976 Order book button opens full table of limit orders in dialog 2022-07-16 14:21:25 -05:00
Austin Chen
32cb19d01f Randomize image upload path to avoid collisions 2022-07-16 11:39:58 -07:00
Austin Chen
1bc49dc0a2 Tweak placeholder copy 2022-07-16 11:39:58 -07:00
Austin Chen
349772a2f9 Description typography: font-light, text-base 2022-07-16 11:39:58 -07:00
Austin Chen
916618be31 Disable quotation marks in quotes 2022-07-16 11:39:58 -07:00
James Grugett
6d8ad74b4d Redeem shares of makers after sellshares 2022-07-16 13:11:13 -05:00
ingawei
7d24a3e4a2
Inga/manalink bug fixes (#653)
* fixed manalinks bug of claiming own manalink, and also rerouting to home upon claiming if not logged in
* no more multiple hardcoded manalink messages
2022-07-15 20:42:37 -05:00
Ian Philips
eed7990c3c Lighten unseen notifs 2022-07-15 16:57:58 -06:00
Sinclair Chen
2543bdcdfc
refactor string matching (#649) 2022-07-15 14:16:00 -07:00
Marshall Polaris
38c26f8b5c
Add API endpoints for fetching user info by username and ID (#652)
* Add an API endpoint for fetching user info by username

* Add endpoint for querying users by ID, too

* Add very simple docs about user APIs
2022-07-15 14:03:34 -07:00
James Grugett
feba0b58ee Turn search filters into pills 2022-07-15 15:06:33 -05:00
Ian Philips
a6cbb6b759 Small notifications ux improvements 2022-07-15 11:53:30 -06:00
James Grugett
1ca73ecd4d Add size prop to button 2022-07-15 12:24:07 -05:00
James Grugett
ec682788e0 Put back old Yes/No bet buttons 2022-07-15 11:03:42 -05:00
Ian Philips
0be38c4e09 Online users list ui, remove from followers list 2022-07-15 09:32:03 -06:00
Ian Philips
50447cf8d3 Unused vars 2022-07-15 08:48:35 -06:00
Ian Philips
d54a72c431 Remove extra comment 2022-07-15 08:47:19 -06:00
Ian Philips
dd9d24e657 Show online users on desktop 2022-07-15 08:45:52 -06:00
Ian Philips
2610f32521 Correct my username 2022-07-15 07:35:17 -06:00
Ian Philips
47579e8509 Fix network spam with modified deps array 2022-07-15 07:28:04 -06:00
Ian Philips
9c49f2e2d7 Revert "Revert "Order groups by most recent chat activity (#650)""
This reverts commit 17c9beca28.
2022-07-15 06:52:08 -06:00
James Grugett
36851ae9f9 Exclude more mobile options from private instances 2022-07-15 00:45:50 -05:00
James Grugett
64c83c4ef0 Don't show portfolio no history message 2022-07-14 23:56:30 -05:00
James Grugett
590c63e911 Small fixes for limit order table 2022-07-14 21:27:00 -05:00
James Grugett
17c9beca28 Revert "Order groups by most recent chat activity (#650)"
This reverts commit 6e1aa4b0f4.
2022-07-14 20:51:38 -05:00
ingawei
2f02e4d3e0
minor tweaks of manalink form (#647)
* minor tweaks of manalink form, adding M$ in front of amount and changing expire time to dropdown instead of calendar selection
* made minimum for uses and amount 1, it seems otherwise it does not generate a link at all
2022-07-14 19:43:06 -05:00
Ian Philips
44d993a588 Bold group for old chat notif 2022-07-14 17:03:08 -06:00
James Grugett
a9018d77c7 If a limit bet doesn't match any orders, don't update the contract, don't redeem shares. Perf win! 2022-07-14 18:01:35 -05:00
Ian Philips
6e1aa4b0f4
Order groups by most recent chat activity (#650)
* Order groups by most recent chat activity

* Use group chat slug constant

* Match source slug and isSeenOnHref

* Listen for group member changes
2022-07-14 16:46:45 -06:00
James Grugett
be64bf71a7 Limit the amount of bets and comments sent to the client through getStaticProps 2022-07-14 14:57:17 -05:00
Ian Philips
d9279e42cc Don't collapse/expand notifs with ctrl/cmd click 2022-07-14 11:56:40 -06:00
Ian Philips
6a28643215 Notifications ux 2022-07-14 11:48:04 -06:00
Ian Philips
27a544205f Optimistically join groups 2022-07-14 11:09:28 -06:00
James Grugett
8daf1b2ba8 Return undefined instead of null for useUserById(undefined) 2022-07-14 12:03:29 -05:00
Sinclair Chen
a93e64c830
fix: let useUserById accept undefined userId (#648) 2022-07-14 12:02:46 -05:00
James Grugett
0c328bc398 Move getStorage() into init.ts after initializeApp() is called. 2022-07-14 11:44:52 -05:00
Ian Philips
deaa595f07 Exclude contract creator in both places 2022-07-14 09:32:50 -06:00
Ian Philips
4eba3c8124 Try new way of calculating rankings for large groups 2022-07-14 09:09:12 -06:00
Ian Philips
eb6b1b9f89 Rename on-delete-group 2022-07-14 08:02:54 -06:00
Ian Philips
709ce5377a Remove extra key assignment 2022-07-14 07:57:33 -06:00
Ian Philips
ee01328553 Remove group slugs from contracts on delete group 2022-07-14 07:53:41 -06:00
Sinclair Chen
5ebd4498a0
Remove deprecated useUserById implementation (#571)
* Remove duplicate useUserById implementation

* fix bug: firebase doesn't accept empty paths
2022-07-13 17:43:20 -07:00
Sinclair Chen
095af10d4f replace raw checkbox w/ Checkbox component
also run prettier
2022-07-13 16:50:08 -07:00
James Grugett
f4b7b9efd0 Only show probabilty update with arrow if probability changes 2022-07-13 18:39:32 -05:00
James Grugett
67b3450924 Use quick vs limit bet in mobile dialog 2022-07-13 18:28:33 -05:00
James Grugett
9240cd3d1c Bet panel: Quick vs Limit pill buttons. Also, pill buttons for Yes vs No. 2022-07-13 18:23:36 -05:00
Sinclair Chen
98192ee580 simplify rich text link styles 2022-07-13 16:14:44 -07:00
Sinclair Chen
664e55a40b
Add typing, pasting links (#646) 2022-07-13 15:56:15 -07:00
Ian Philips
45fb3803c1 Limit member search to 100 2022-07-13 16:24:35 -06:00
mantikoros
e1b6619e9c embeds: don't show bet button after resolution 2022-07-13 17:22:50 -05:00
Ian Philips
7a49549389 Ignore rankings/members for huge groups for now 2022-07-13 16:20:56 -06:00
Sinclair Chen
f08d6bda93
when adding package, don't put ^ before version (#645) 2022-07-13 15:14:06 -07:00
ingawei
a4e2cce4aa
initial commit for manalinks UI improvements (#642)
* manalinks UI improvements

* made manalink look more like card

* changed new link to pulsing indigo instead of green
2022-07-13 16:57:34 -05:00
Ian Philips
55c91dfcdd
Categories to groups (#641)
* start on script

* Revert "Remove category filters"

This reverts commit d6e808e1a3.

* Convert categories to official default groups

* Add new users to default groups

* Rework group cards

* Cleanup

* Add unique bettors to contract and sort by them

* Most bettors to most popular

* Unused vars

* Track unique bettor ids on contracts

* Add followed users' bets to personal markets

* Add new users to welcome, bugs, and updates groups

* Add users to fewer default cats
2022-07-13 15:11:22 -06:00
James Grugett
e868f0a15a Fix pagination component going one page too far + tweaks 2022-07-13 15:15:03 -05:00
James Grugett
9075a6f33a Add headers to limit orders table 2022-07-13 14:59:51 -05:00
Austin Chen
87b669e358 Add FYXX Foundation (h/t Holly Elmore) 2022-07-13 12:44:32 -07:00
Sinclair Chen
a92eda3af2 fix bug where descriptions not showing 2022-07-13 12:36:01 -07:00
Sinclair Chen
9a11f55762
Rich content (#620)
* Add TipTap editor and renderer components

* Change market description editor to rich text

* Type description as JSON, fix string-based logic

- Delete make-predictions.tsx
- Delete feed logic that showed descriptions

* wip Fix API validation

* fix type error

* fix extension import (backend)

In firebase, typescript compiles imports into common js imports
like `const StarterKit = require("@tiptap/starter-kit")`

Even though StarterKit is exported from the cjs file, it gets imported
as undefined. But it magically works if we import *

If you're reading this in the future, consider replacing StarterKit with
the entire list of extensions.

* Stop load on fail create market, improve warning

* Refactor editor as hook / fix infinite submit bug

Move state of editor back up to parent
We have to do this later anyways to allow parent to edit

* Add images - display, paste + uploading

* add uploading state of image

* Fix placeholder, misc styling

min height, quote

* Fix appending to description

* code review fixes: rename, refactor, chop carets

* Add hint & upload button on new lines

- bump to Tailwind 3.1 for arbitrary variants

* clean up, run prettier

* rename FileButton to FileUploadButton

* add image extension as functions dependency
2022-07-13 11:58:22 -07:00
mantikoros
83d8f18bd7 fix bet summary selling 2022-07-13 13:20:53 -05:00
James Grugett
50eee33a6e Redeem shares of makers after matching with limit bets 2022-07-13 12:51:19 -05:00
James Grugett
f1eea66588 Show all limit orders in a tab 2022-07-13 12:15:00 -05:00
mantikoros
737d803903 bet row: default to YES 2022-07-13 11:20:29 -05:00
Ian Philips
18abad38b6 Unused var 2022-07-13 09:13:34 -06:00
Ian Philips
cc1431da60 Disable enter submit on mobile on group chat 2022-07-13 09:12:43 -06:00
Ian Philips
490eabf977 Revert "Revert "Disable enter to submit on mobile group chat""
This reverts commit e3f7f0efda.
2022-07-13 09:08:32 -06:00
Ian Philips
e3f7f0efda Revert "Disable enter to submit on mobile group chat"
This reverts commit b3f4c2f009.
2022-07-13 08:44:27 -06:00
Ian Philips
b3f4c2f009 Disable enter to submit on mobile group chat 2022-07-13 08:34:14 -06:00
Ian Philips
9e90f849a8 Show group scrollbars only when needed 2022-07-13 07:57:51 -06:00
Ian Philips
96a378f25f Handle free response resolution 2022-07-13 07:41:58 -06:00
mantikoros
1f2bdf40d0 bet row: fix labels 2022-07-13 00:07:12 -05:00
Austin Chen
10c510fc6b Feature Wild Animal Initiative 2022-07-12 18:27:22 -07:00
mantikoros
68343701ca answer bet panel: scroll up on ios 2022-07-12 17:47:48 -05:00
mantikoros
5c166b9dd5 bet row: 'higher' 'lower' labels 2022-07-12 17:47:28 -05:00
mantikoros
38aad40569
Simplify bet buttons (#644)
* mono-button bet row

* "bet yes" => "yes"

* prettier
2022-07-12 17:34:10 -05:00
mantikoros
dd9fdc381f track limit orders 2022-07-12 16:55:00 -05:00
mantikoros
24896e44b4 "limit bet" => "limit order" 2022-07-12 16:46:03 -05:00
Marshall Polaris
5fd42df1ed
Don't run share redemption after adding liquidity (#631) 2022-07-12 12:36:31 -07:00
Marshall Polaris
43b30e6d04
Don't "warm up" resolveMarket anymore (#638) 2022-07-12 12:36:10 -07:00
James Grugett
0882f1c0d6 Remove top Pagepadding on small screens 2022-07-11 19:07:37 -05:00
James Grugett
b8d7c2ee17 Size group chat window & nav bar list of groups precisely. Update Page margin/padding. 2022-07-11 18:40:25 -05:00
James Grugett
24fac1fc0b Fix erronous 0 prob shown in table 2022-07-11 15:53:13 -05:00
Ian Philips
ed9a2c0d35 Set min height for group chat 2022-07-11 14:52:16 -06:00
James Grugett
90a75985dd In market bets tab, show limit orders' total order amount 2022-07-11 11:46:09 -05:00
mantikoros
61300e93a4 more validation for creating numeric markets 2022-07-11 11:38:51 -05:00
Ian Philips
7b60cc63ce Fix annoying create description scrolling on firefox 2022-07-11 09:56:10 -06:00
James Grugett
9b252b93ab Fix fee calculation in bet panel tooltip 2022-07-11 10:54:37 -05:00
James Grugett
dd6f5e5ef4 Show better limit order stats in bets table 2022-07-11 10:49:36 -05:00
Ian Philips
52d688885d Group income notifs by source title 2022-07-11 08:11:52 -06:00
Ian Philips
86c256cbf7 Unused var 2022-07-11 08:01:26 -06:00
Ian Philips
a2a08b90ff Show numeric resolution contract value 2022-07-11 07:51:48 -06:00
James Grugett
1e68267e8e Use relative import 2022-07-10 23:09:46 -05:00
James Grugett
098f20ccad Fix limit bet filter to exclude cancelled and filled bets 2022-07-10 22:28:29 -05:00
James Grugett
89d48d6c34 Use hook to fetch user bets 2022-07-10 22:28:04 -05:00
James Grugett
99fcfa6be7 Add portfolio filter for limit bets. 2022-07-10 22:15:07 -05:00
James Grugett
9586e81e95 Show limit bets in bets table 2022-07-10 22:07:42 -05:00
James Grugett
fd7384a099 Hide referrals button on user page 2022-07-10 19:59:23 -05:00
James Grugett
67edc7b639 UserPage: Load user with getStatic props 2022-07-10 19:42:34 -05:00
James Grugett
5e1ed17cdf Load contracts at UserPage top level instead of in BetsList 2022-07-10 19:19:35 -05:00
James Grugett
f294189e20 Refactor notifications to use Pagination component 2022-07-10 18:50:59 -05:00
James Grugett
162e73912e Paginate bets list 2022-07-10 18:41:33 -05:00
James Grugett
5c6a143614 Change portfolio graph option labels 2022-07-10 18:26:06 -05:00
James Grugett
78ceac0659 Don't load user bets twice 👀 2022-07-10 18:22:21 -05:00
Marshall Polaris
4700ceb14c
Refactor some backend-related stuff (#639)
* web/lib/firebase/api-call -> common/api, web/lib/firebase/api

* Reuse `APIError` type in server code

* Reuse `getFunctionUrl` in server code
2022-07-10 15:03:15 -07:00
Marshall Polaris
6462d4a2ed
Migrate createUser function to v2 (#633) 2022-07-10 14:02:32 -07:00
Marshall Polaris
eb9b14d6d5
Migrate unsubscribe function to v2 (#637)
* Migrate unsubscribe function to v2

* Move Stripe import because I forgot to do it before
2022-07-10 13:46:00 -07:00
James Grugett
83c5f9b323 Fix unused var 2022-07-10 14:55:10 -05:00
James Grugett
f2df32e710 PseudoNumeric markets store resolveValue in resolved notification and render it 2022-07-10 14:52:31 -05:00
James Grugett
900fc75506 Add sourceContractId to bet_fill notification 2022-07-10 13:45:32 -05:00
James Grugett
4de22acb3e Tweak check for matching with pool 2022-07-10 13:24:54 -05:00
James Grugett
80ae551ca9
🧾 Limit orders! (#495)
* Simple limit order UI

* Update bet schema

* Restrict bet panel / bet row to only CPMMBinaryContracts (all binary DPM are resolved)

* Limit orders partway implemented

* Update follow leaderboard copy

* Change cpmm code to take some state instead of whole contract

* Write more of matching algorithm

* Fill in more of placebet

* Use client side contract search for emulator

* More correct matching

* Merge branch 'main' into limit-orders

* Some cleanup

* Listen for unfilled bets in bet panel. Calculate how the probability moves based on open limit orders.

* Simpler switching between bet & limit bet.

* Render your open bets (unfilled limit orders)

* Cancel bet endpoint.

* Fix build error

* Rename open bets to limit bets. Tweak payout calculation

* Limit probability selector to 1-99

* Deduct user balance only on each fill. Store orderAmount of bet. Timestamp of fills.

* Use floating equal to check if have shares

* Add limit order switcher to mobile bet dialog

* Support limit orders on numeric markets

* Allow CORS exception for Vercel deployments

* Remove console.logs

* Update user balance by new bet amount

* Tweak vercel cors

* Try another regexp for vercel cors

* Test another vercel regex

* Slight notifications refactor

* Fix docs edit link (#624)

* Fix docs edit link

* Update github links

* Small groups UX changes

* Groups UX on mobile

* Leaderboards => Rankings on groups

* Unused vars

* create: remove automatic setting of log scale

* Use react-query to cache notifications (#625)

* Use react-query to cache notifications

* Fix imports

* Cleanup

* Limit unseen notifs query

* Catch the bounced query

* Don't use interval

* Unused var

* Avoid flash of page nav

* Give notification question priority & 2 lines

* Right justify timestamps

* Rewording

* Margin

* Simplify error msg

* Be explicit about limit for unseen notifs

* Pass limit > 0

* Remove category filters

* Remove category selector references

* Track notification clicks

* Analyze tab usage

* Bold more on new group chats

* Add API route for listing a bets by user (#567)

* Add API route for getting a user's bets

* Refactor bets API to use /bets

* Update /markets to use zod validation

* Update docs

* Clone missing indexes from firestore

* Minor notif spacing adjustments

* Enable tipping on group chats w/ notif (#629)

* Tweak cors regex for vercel

* Your limit bets

* Implement selling shares

* Merge branch 'main' into limit-orders

* Fix lint

* Move binary search to util file

* Add note that there might be closed form

* Add tooltip to explain limit probability

* Tweak

* Cancel your limit orders if you run out of money

* Don't show amount error in probability input

* Require limit prob to be >= .1% and <= 99.9%

* Fix focus input bug

* Simplify mobile betting dialog

* Move mobile limit bets list into bet dialog.

* Small fixes to existing sell shares client

* Lint

* Refactor useSaveShares to actually read from localStorage, use less bug-prone interface.

* Fix NaN error

* Remove TODO

* Simple bet fill notification

* Tweak wording

* Sort limit bets by limit prob

* Padding on limit bets

* Match header size

Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: ahalekelly <ahalekelly@gmail.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: Ben Congdon <ben@congdon.dev>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-07-10 13:05:44 -05:00
mantikoros
fc06b03af8 fix getCpmmLiquidityPoolWeights 2022-07-09 22:39:26 -04:00
mantikoros
d063e209dd Revert "expand search bar when typing on mobile"
This reverts commit 43b1096313.
2022-07-09 22:04:50 -04:00
Marshall Polaris
480b3e7c54
Make referral stuff not busted (#632) 2022-07-09 14:38:23 -07:00
mantikoros
43b1096313 expand search bar when typing on mobile 2022-07-09 17:27:39 -04:00
Marshall Polaris
67a05c2f1b
Migrate transact function to v2 (#635) 2022-07-09 13:54:15 -07:00
Marshall Polaris
581a42f288
Migrate stripeWebhook and createCheckoutSession to v2 (#636) 2022-07-09 13:43:18 -07:00
mantikoros
e7e686d579 return creator liquidity after resolution 2022-07-09 13:53:50 -04:00
Marshall Polaris
c1ca1471a1
Migrate createAnswer function to v2 (#634)
* Migrate createAnswer function to v2

* Remove unhelpful toString on APIError
2022-07-09 00:26:56 -07:00
Marshall Polaris
fdde73710e
Migrate claimManalink function to v2 (#628)
* Implement helpful `toString` on client `APIError`

* Migrate claimManalink function to v2
2022-07-08 15:28:04 -07:00
Marshall Polaris
d9f42caa6a
Migrate addLiquidity and withdrawLiquidity functions to v2 (#627) 2022-07-08 15:08:17 -07:00
Marshall Polaris
ed0544212d
Migrate changeUserInfo function to v2 (#626) 2022-07-08 15:00:03 -07:00
mantikoros
93b293ca0e remove quick betting for FR markets 2022-07-08 12:50:46 -04:00
mantikoros
50c5f8b6eb reenable fees on share sales; rename getCpmmFees() 2022-07-08 12:34:16 -04:00
Ian Philips
b1b016f9e0
Enable tipping on group chats w/ notif (#629) 2022-07-07 17:23:13 -06:00
Ian Philips
d6136a9937 Minor notif spacing adjustments 2022-07-07 17:17:10 -06:00
Austin Chen
53ddb1243b Clone missing indexes from firestore 2022-07-07 15:41:44 -07:00
Ben Congdon
c3bc25a4b9
Add API route for listing a bets by user (#567)
* Add API route for getting a user's bets

* Refactor bets API to use /bets

* Update /markets to use zod validation

* Update docs
2022-07-07 15:36:02 -07:00
Ian Philips
999c1cd8e3 Bold more on new group chats 2022-07-07 15:52:28 -06:00
Ian Philips
e456b9a855 Analyze tab usage 2022-07-07 15:24:13 -06:00
Ian Philips
3eee4a4103 Track notification clicks 2022-07-07 15:06:29 -06:00
Ian Philips
3ff8b26312 Remove category selector references 2022-07-07 14:55:28 -06:00
Ian Philips
d6e808e1a3 Remove category filters 2022-07-07 14:45:26 -06:00
Ian Philips
cfbb78af48
Use react-query to cache notifications (#625)
* Use react-query to cache notifications

* Fix imports

* Cleanup

* Limit unseen notifs query

* Catch the bounced query

* Don't use interval

* Unused var

* Avoid flash of page nav

* Give notification question priority & 2 lines

* Right justify timestamps

* Rewording

* Margin

* Simplify error msg

* Be explicit about limit for unseen notifs

* Pass limit > 0
2022-07-07 14:41:50 -06:00
mantikoros
a22b29ad6d create: remove automatic setting of log scale 2022-07-07 12:36:34 -04:00
Ian Philips
7f8617832f Unused vars 2022-07-07 07:05:12 -06:00
Ian Philips
b8748fd49a Leaderboards => Rankings on groups 2022-07-07 06:54:00 -06:00
Ian Philips
93b2900015 Groups UX on mobile 2022-07-07 06:53:14 -06:00
Ian Philips
a23c744c3e Small groups UX changes 2022-07-06 17:24:53 -06:00
ahalekelly
2591655269
Fix docs edit link (#624)
* Fix docs edit link

* Update github links
2022-07-06 15:41:13 -06:00
Ian Philips
e969540c72 Slight notifications refactor 2022-07-06 15:06:41 -06:00
Ian Philips
54b4f97a84 Move timestamp to same line 2022-07-06 13:45:31 -06:00
Ian Philips
de20ee9fb9
Show tip notifications (#623)
* Show tip notifications

* Optimizing notifications for mobile

* Unused vars

* Move income reason logic to income notif

* Remove unnecessary icons

* Unused vars
2022-07-06 13:30:51 -06:00
Austin Chen
2d1e76eae8 When duplicating, add the original link in description 2022-07-06 10:39:19 -07:00
Ian Philips
434b8b9dbe Just show first names to save space 2022-07-06 07:51:32 -06:00
Ian Philips
83a02c4b20 Small notifications ux improvements 2022-07-06 07:45:47 -06:00
Ian Philips
a6143c1abb Always group income 2022-07-06 07:27:21 -06:00
Austin Chen
029021b351 Remove Categories from /create 2022-07-05 17:20:37 -07:00
596 changed files with 49815 additions and 18968 deletions

43
.github/workflows/format.yml vendored Normal file
View File

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

43
.github/workflows/lint.yml vendored Normal file
View File

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

View File

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

View File

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

1
common/.yarnrc Normal file
View File

@ -0,0 +1 @@
save-prefix ""

View File

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

View File

@ -5,16 +5,22 @@ import {
CPMMBinaryContract,
DPMBinaryContract,
FreeResponseContract,
MultipleChoiceContract,
NumericContract,
} from './contract'
import { User } from './user'
import { LiquidityProvision } from './liquidity-provision'
import { noFees } from './fees'
import { ENV_CONFIG } from './envs/constants'
export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100
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,
@ -51,7 +57,7 @@ export function getAnteBets(
const { createdTime } = contract
const yesBet: Bet = {
const yesBet: NormalizedBet = {
id: yesAnteId,
userId: creator.id,
contractId: contract.id,
@ -65,7 +71,7 @@ export function getAnteBets(
fees: noFees,
}
const noBet: Bet = {
const noBet: NormalizedBet = {
id: noAnteId,
userId: creator.id,
contractId: contract.id,
@ -93,7 +99,7 @@ export function getFreeAnswerAnte(
const { createdTime } = contract
const anteBet: Bet = {
const anteBet: NormalizedBet = {
id: anteBetId,
userId: anteBettorId,
contractId: contract.id,
@ -110,6 +116,50 @@ export function getFreeAnswerAnte(
return anteBet
}
export function getMultipleChoiceAntes(
creator: User,
contract: MultipleChoiceContract,
answers: string[],
betDocIds: string[]
) {
const { totalBets, totalShares } = contract
const amount = totalBets['0']
const shares = totalShares['0']
const p = 1 / answers.length
const { createdTime } = contract
const bets: NormalizedBet[] = answers.map((answer, i) => ({
id: betDocIds[i],
userId: creator.id,
contractId: contract.id,
amount,
shares,
outcome: i.toString(),
probBefore: p,
probAfter: p,
createdTime,
isAnte: true,
fees: noFees,
}))
const { username, name, avatarUrl } = creator
const answerObjects: Answer[] = answers.map((answer, i) => ({
id: i.toString(),
number: i,
contractId: contract.id,
createdTime,
userId: creator.id,
username,
name,
avatarUrl,
text: answer,
}))
return { bets, answerObjects }
}
export function getNumericAnte(
anteBettorId: string,
contract: NumericContract,
@ -129,7 +179,7 @@ export function getNumericAnte(
range(0, bucketCount).map((_, i) => [i, betAnte])
)
const anteBet: NumericBet = {
const anteBet: NormalizedBet<NumericBet> = {
id: newBetId,
userId: anteBettorId,
contractId: contract.id,

24
common/api.ts Normal file
View File

@ -0,0 +1,24 @@
import { ENV_CONFIG } from './envs/constants'
export class APIError extends Error {
code: number
details?: unknown
constructor(code: number, message: string, details?: unknown) {
super(message)
this.code = code
this.name = 'APIError'
this.details = details
}
}
export function getFunctionUrl(name: string) {
if (process.env.NEXT_PUBLIC_FUNCTIONS_URL) {
return `${process.env.NEXT_PUBLIC_FUNCTIONS_URL}/${name}`
} else if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
const { projectId, region } = ENV_CONFIG.firebaseConfig
return `http://localhost:5001/${projectId}/${region}/${name}`
} else {
const { cloudRunId, cloudRunRegion } = ENV_CONFIG
return `https://${name}-${cloudRunId}-${cloudRunRegion}.a.run.app`
}
}

123
common/badge.ts Normal file
View File

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

View File

@ -3,7 +3,14 @@ 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
amount: number // bet size; negative if SELL bet
loanAmount?: number
@ -13,21 +20,22 @@ export type Bet = {
probBefore: number
probAfter: number
sale?: {
amount: number // amount user makes from sale
betId: string // id of bet being sold
// TODO: add sale time?
}
fees: Fees
isSold?: boolean // true if this BUY bet has been sold
isAnte?: boolean
isLiquidityProvision?: boolean
isRedemption?: boolean
challengeSlug?: string
createdTime: number
}
// Props for bets in DPM contract below.
// A bet is either a BUY or a SELL that sells all of a previous buy.
isSold?: boolean // true if this BUY bet has been sold
// This field marks a SELL bet.
sale?: {
amount: number // amount user makes from sale
betId: string // id of BUY bet being sold
}
} & Partial<LimitProps>
export type NumericBet = Bet & {
value: number
@ -35,4 +43,27 @@ export type NumericBet = Bet & {
allBetAmounts: { [outcome: string]: number }
}
export const MAX_LOAN_PER_CONTRACT = 20
// Binary market limit order.
export type LimitBet = Bet & LimitProps
type LimitProps = {
orderAmount: number // Amount of limit order.
limitProb: number // [0, 1]. Bet to this probability.
isFilled: boolean // Whether all of the bet amount has been filled.
isCancelled: boolean // Whether to prevent any further fills.
// A record of each transaction that partially (or fully) fills the orderAmount.
// I.e. A limit order could be filled by partially matching with several bets.
// Non-limit orders can also be filled by matching with multiple limit orders.
fills: fill[]
}
export type fill = {
// The id the bet matched against, or null if the bet was matched by the pool.
matchedBetId: string | null
amount: number
shares: number
timestamp: number
// If the fill is a sale, it means the matching bet has shares of the same outcome.
// I.e. -fill.shares === matchedBet.shares
isSale?: boolean
}

View File

@ -1,9 +1,15 @@
import { sum, groupBy, mapValues, sumBy, partition } from 'lodash'
import { groupBy, mapValues, sumBy } from 'lodash'
import { LimitBet } from './bet'
import { CPMMContract } from './contract'
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, noFees, PLATFORM_FEE } from './fees'
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees'
import { LiquidityProvision } from './liquidity-provision'
import { addObjects } from './util/object'
import { computeFills } from './new-bet'
import { binarySearch } from './util/algos'
export type CpmmState = {
pool: { [outcome: string]: number }
p: number
}
export function getCpmmProbability(
pool: { [outcome: string]: number },
@ -14,11 +20,11 @@ export function getCpmmProbability(
}
export function getCpmmProbabilityAfterBetBeforeFees(
contract: CPMMContract,
state: CpmmState,
outcome: string,
bet: number
) {
const { pool, p } = contract
const { pool, p } = state
const shares = calculateCpmmShares(pool, p, bet, outcome)
const { YES: y, NO: n } = pool
@ -31,12 +37,12 @@ export function getCpmmProbabilityAfterBetBeforeFees(
}
export function getCpmmOutcomeProbabilityAfterBet(
contract: CPMMContract,
state: CpmmState,
outcome: string,
bet: number
) {
const { newPool } = calculateCpmmPurchase(contract, bet, outcome)
const p = getCpmmProbability(newPool, contract.p)
const { newPool } = calculateCpmmPurchase(state, bet, outcome)
const p = getCpmmProbability(newPool, state.p)
return outcome === 'NO' ? 1 - p : p
}
@ -58,12 +64,8 @@ function calculateCpmmShares(
: n + bet - (k * (bet + y) ** -p) ** (1 / (1 - p))
}
export function getCpmmLiquidityFee(
contract: CPMMContract,
bet: number,
outcome: string
) {
const prob = getCpmmProbabilityAfterBetBeforeFees(contract, outcome, bet)
export function getCpmmFees(state: CpmmState, bet: number, outcome: string) {
const prob = getCpmmProbabilityAfterBetBeforeFees(state, outcome, bet)
const betP = outcome === 'YES' ? 1 - prob : prob
const liquidityFee = LIQUIDITY_FEE * betP * bet
@ -78,25 +80,23 @@ export function getCpmmLiquidityFee(
}
export function calculateCpmmSharesAfterFee(
contract: CPMMContract,
state: CpmmState,
bet: number,
outcome: string
) {
const { pool, p } = contract
const { remainingBet } = getCpmmLiquidityFee(contract, bet, outcome)
const { pool, p } = state
const { remainingBet } = getCpmmFees(state, bet, outcome)
return calculateCpmmShares(pool, p, remainingBet, outcome)
}
export function calculateCpmmPurchase(
contract: CPMMContract,
state: CpmmState,
bet: number,
outcome: string
) {
const { pool, p } = contract
const { remainingBet, fees } = getCpmmLiquidityFee(contract, bet, outcome)
// const remainingBet = bet
// const fees = noFees
const { pool, p } = state
const { remainingBet, fees } = getCpmmFees(state, bet, outcome)
const shares = calculateCpmmShares(pool, p, remainingBet, outcome)
const { YES: y, NO: n } = pool
@ -115,119 +115,125 @@ export function calculateCpmmPurchase(
return { shares, newPool, newP, fees }
}
function computeK(y: number, n: number, p: number) {
return y ** p * n ** (1 - p)
}
function sellSharesK(
y: number,
n: number,
p: number,
s: number,
outcome: 'YES' | 'NO',
b: number
) {
return outcome === 'YES'
? computeK(y - b + s, n - b, p)
: computeK(y - b, n - b + s, p)
}
function calculateCpmmShareValue(
contract: CPMMContract,
shares: number,
// Note: there might be a closed form solution for this.
// If so, feel free to switch out this implementation.
export function calculateCpmmAmountToProb(
state: CpmmState,
prob: number,
outcome: 'YES' | 'NO'
) {
const { pool, p } = contract
if (prob <= 0 || prob >= 1 || isNaN(prob)) return Infinity
if (outcome === 'NO') prob = 1 - prob
// Find bet amount that preserves k after selling shares.
const k = computeK(pool.YES, pool.NO, p)
const otherPool = outcome === 'YES' ? pool.NO : pool.YES
// First, find an upper bound that leads to a more extreme probability than prob.
let maxGuess = 10
let newProb = 0
do {
maxGuess *= 10
newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, maxGuess)
} while (newProb < prob)
// Constrain the max sale value to the lessor of 1. shares and 2. the other pool.
// This is because 1. the max value per share is M$ 1,
// and 2. The other pool cannot go negative and the sale value is subtracted from it.
// (Without this, there are multiple solutions for the same k.)
let highAmount = Math.min(shares, otherPool)
let lowAmount = 0
let mid = 0
let kGuess = 0
while (true) {
mid = lowAmount + (highAmount - lowAmount) / 2
// Then, binary search for the amount that gets closest to prob.
const amount = binarySearch(0, maxGuess, (amount) => {
const newProb = getCpmmOutcomeProbabilityAfterBet(state, outcome, amount)
return newProb - prob
})
// Break once we've reached max precision.
if (mid === lowAmount || mid === highAmount) break
return amount
}
kGuess = sellSharesK(pool.YES, pool.NO, p, shares, outcome, mid)
if (kGuess < k) {
highAmount = mid
} else {
lowAmount = mid
}
}
return mid
function calculateAmountToBuyShares(
state: CpmmState,
shares: number,
outcome: 'YES' | 'NO',
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) {
// Search for amount between bounds (0, shares).
// Min share price is M$0, and max is M$1 each.
return binarySearch(0, shares, (amount) => {
const { takers } = computeFills(
outcome,
amount,
state,
undefined,
unfilledBets,
balanceByUserId
)
const totalShares = sumBy(takers, (taker) => taker.shares)
return totalShares - shares
})
}
export function calculateCpmmSale(
contract: CPMMContract,
state: CpmmState,
shares: number,
outcome: string
outcome: 'YES' | 'NO',
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) {
if (Math.round(shares) < 0) {
throw new Error('Cannot sell non-positive shares')
}
const saleValue = calculateCpmmShareValue(
contract,
const oppositeOutcome = outcome === 'YES' ? 'NO' : 'YES'
const buyAmount = calculateAmountToBuyShares(
state,
shares,
outcome as 'YES' | 'NO'
oppositeOutcome,
unfilledBets,
balanceByUserId
)
const fees = noFees
const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills(
oppositeOutcome,
buyAmount,
state,
undefined,
unfilledBets,
balanceByUserId
)
// const { fees, remainingBet: saleValue } = getCpmmLiquidityFee(
// contract,
// rawSaleValue,
// outcome === 'YES' ? 'NO' : 'YES'
// )
// Transform buys of opposite outcome into sells.
const saleTakers = takers.map((taker) => ({
...taker,
// You bought opposite shares, which combine with existing shares, removing them.
shares: -taker.shares,
// Opposite shares combine with shares you are selling for M$ of shares.
// You paid taker.amount for the opposite shares.
// Take the negative because this is money you gain.
amount: -(taker.shares - taker.amount),
isSale: true,
}))
const { pool } = contract
const { YES: y, NO: n } = pool
const saleValue = -sumBy(saleTakers, (taker) => taker.amount)
const { liquidityFee: fee } = fees
const [newY, newN] =
outcome === 'YES'
? [y + shares - saleValue + fee, n - saleValue + fee]
: [y - saleValue + fee, n + shares - saleValue + fee]
if (newY < 0 || newN < 0) {
console.log('calculateCpmmSale', {
newY,
newN,
y,
n,
shares,
saleValue,
fee,
outcome,
})
throw new Error('Cannot sell more than in pool')
return {
saleValue,
cpmmState,
fees: totalFees,
makers,
takers: saleTakers,
ordersToCancel,
}
const postBetPool = { YES: newY, NO: newN }
const { newPool, newP } = addCpmmLiquidity(postBetPool, contract.p, fee)
return { saleValue, newPool, newP, fees }
}
export function getCpmmProbabilityAfterSale(
contract: CPMMContract,
state: CpmmState,
shares: number,
outcome: 'YES' | 'NO'
outcome: 'YES' | 'NO',
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) {
const { newPool } = calculateCpmmSale(contract, shares, outcome)
return getCpmmProbability(newPool, contract.p)
const { cpmmState } = calculateCpmmSale(
state,
shares,
outcome,
unfilledBets,
balanceByUserId
)
return getCpmmProbability(cpmmState.pool, cpmmState.p)
}
export function getCpmmLiquidity(
@ -260,46 +266,23 @@ export function addCpmmLiquidity(
return { newPool, liquidity, newP }
}
const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => {
const oldLiquidity = getCpmmLiquidity(l.pool, p)
export function getCpmmLiquidityPoolWeights(liquidities: LiquidityProvision[]) {
const userAmounts = groupBy(liquidities, (w) => w.userId)
const totalAmount = sumBy(liquidities, (w) => w.amount)
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(
contract: CPMMContract,
liquidities: LiquidityProvision[]
) {
const [antes, nonAntes] = partition(liquidities, (l) => !!l.isAnte)
const calcLiqudity = calculateLiquidityDelta(contract.p)
const liquidityShares = nonAntes.map(calcLiqudity)
const shareSum = sum(liquidityShares) + sum(antes.map(calcLiqudity))
const weights = liquidityShares.map((s, i) => ({
weight: s / shareSum,
providerId: nonAntes[i].userId,
}))
const userWeights = groupBy(weights, (w) => w.providerId)
const totalUserWeights = mapValues(userWeights, (userWeight) =>
sumBy(userWeight, (w) => w.weight)
return mapValues(
userAmounts,
(amounts) => sumBy(amounts, (w) => w.amount) / totalAmount
)
return totalUserWeights
}
export function getUserLiquidityShares(
userId: string,
contract: CPMMContract,
state: CpmmState,
liquidities: LiquidityProvision[]
) {
const weights = getCpmmLiquidityPoolWeights(contract, liquidities)
const weights = getCpmmLiquidityPoolWeights(liquidities)
const userWeight = weights[userId] ?? 0
return mapValues(contract.pool, (shares) => userWeight * shares)
return mapValues(state.pool, (shares) => userWeight * shares)
}

View File

@ -2,7 +2,7 @@ import { cloneDeep, range, sum, sumBy, sortBy, mapValues } from 'lodash'
import { Bet, NumericBet } from './bet'
import { DPMContract, DPMBinaryContract, NumericContract } from './contract'
import { DPM_FEES } from './fees'
import { normpdf } from '../common/util/math'
import { normpdf } from './util/math'
import { addObjects } from './util/object'
export function getDpmProbability(totalShares: { [outcome: string]: number }) {

315
common/calculate-metrics.ts Normal file
View File

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

View File

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

View File

@ -1,5 +1,7 @@
import { difference } from 'lodash'
export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default'
export const CATEGORIES = {
politics: 'Politics',
technology: 'Technology',
@ -24,9 +26,18 @@ export const TO_CATEGORY = Object.fromEntries(
export const CATEGORY_LIST = Object.keys(CATEGORIES)
export const EXCLUDED_CATEGORIES: category[] = ['fun', 'manifold', 'personal']
export const EXCLUDED_CATEGORIES: category[] = [
'fun',
'manifold',
'personal',
'covid',
'gaming',
'crypto',
]
export const DEFAULT_CATEGORIES = difference(
CATEGORY_LIST,
EXCLUDED_CATEGORIES
)
export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES)
export const DEFAULT_CATEGORY_GROUPS = DEFAULT_CATEGORIES.map((c) => ({
slug: c.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX,
name: CATEGORIES[c as category],
}))

65
common/challenge.ts Normal file
View File

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

View File

@ -169,7 +169,7 @@ export const charities: Charity[] = [
{
name: "Founder's Pledge Climate Change Fund",
website: 'https://founderspledge.com/funds/climate-change-fund',
photo: 'https://i.imgur.com/ZAhzHu4.png',
photo: 'https://i.imgur.com/9turaJW.png',
preview:
'The Climate Change Fund aims to sustainably reach net-zero emissions globally, while still allowing growth to free millions from energy poverty.',
description: `The Climate Change Fund aims to sustainably reach net-zero emissions globally.
@ -183,7 +183,7 @@ export const charities: Charity[] = [
{
name: "Founder's Pledge Patient Philanthropy Fund",
website: 'https://founderspledge.com/funds/patient-philanthropy-fund',
photo: 'https://i.imgur.com/ZAhzHu4.png',
photo: 'https://i.imgur.com/LLR6CI6.png',
preview:
'The Patient Philanthropy Project aims to safeguard and benefit the long-term future of humanity',
description: `The Patient Philanthropy Project focuses on how we can collectively grow our resources to support the long-term flourishing of humanity. It addresses a crucial gap: as a society, we spend much too little on safeguarding and benefiting future generations. In fact, we spend more money on ice cream each year than we do on preventing our own extinction. However, people in the future - who do not have a voice in their future survival or environment - matter. Lots of them may yet come into existence and we have the ability to positively affect their lives now, if only by making sure we avoid major catastrophes that could destroy our common future.
@ -300,10 +300,29 @@ Future plans: We expect to focus on similar theoretical problems in alignment un
name: 'Wild Animal Initiative',
website: 'https://www.wildanimalinitiative.org/',
ein: '82-2281466',
tags: ['Featured'] as CharityTag[],
photo: 'https://i.imgur.com/bOVUnDm.png',
preview: 'We want to make life better for wild animals.',
description:
'Wild Animal Initiative (WAI) currently operates in the U.S., where they work to strengthen the animal advocacy movement through creating an academic field dedicated to wild animal welfare. They compile literature reviews, write theoretical and opinion articles, and publish research results on their website and/or in peer-reviewed journals. WAI focuses on identifying and sharing possible research avenues and connecting with more established fields. They also work with researchers from various academic and non-academic institutions to identify potential collaborators, and they recently launched a grant assistance program.',
preview:
'Our mission is to understand and improve the lives of wild animals.',
description: `Although the natural world is a source of great beauty and happiness, vast numbers of animals routinely face serious challenges such as disease, hunger, or natural disasters. There is no “one-size-fits-all” solution to these threats. However, even as we recognize that improving the welfare of free-ranging wild animals is difficult, we believe that humans have a responsibility to help whenever we can.
Our staff explores how humans can beneficially coexist with animals through the lens of wild animal welfare.
We respect wild animals as individuals with their own needs and preferences, rather than seeing them as mere parts of ecosystems. But this approach demands a richer understanding of wild animals lives.
We want to take a proactive approach to managing the welfare benefits, threats, and uncertainties that are inherent to complex natural and urban environments. Yet, to take action safely, we must conduct research to understand the impacts of our actions. The transdisciplinary perspective of wild animal welfare draws upon ethics, ecology, and animal welfare science to gather the knowledge we need, facilitating evidence-based improvements to wild animals quality of life.
Without sufficient public interest or research activity, solutions to the problems wild animals face will go undiscovered.
Wild Animal Initiative currently focuses on helping scientists, grantors, and decision-makers investigate important and understudied questions about wild animal welfare. Our work catalyzes research and applied projects that will open the door to a clearer picture of wild animals needs and how to enhance their well-being. Ultimately, we envision a world in which people actively choose to help wild animals and have the knowledge they need to do so responsibly.`,
},
{
name: 'FYXX Foundation',
website: 'https://www.fyxxfoundation.org/',
photo: 'https://i.imgur.com/ROmWO7m.png',
preview:
'FYXX Foundation: wildlife population management, without killing.',
description: `The future of our planet depends on the innovations of today, and the health of our wildlife are the first indication of our successful stewardship, which we believe can be improved by safe population management utilizing fertility control instead of poison and culling.`,
},
{
name: 'New Incentives',
@ -532,6 +551,53 @@ With an emphasis on approval voting, we bring better elections to people across
The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that dont represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`,
},
{
name: 'Founders Pledge Global Health and Development Fund',
website: 'https://founderspledge.com/funds/global-health-and-development',
photo: 'https://i.imgur.com/EXbxH7T.png',
preview:
'Tackling the vast global inequalities in health, wealth and opportunity',
description: `Nearly half the world lives on less than $2.50 a day, yet giving by the worlds richest often overlooks the worlds poorest and most vulnerable. Despite the average American household being richer than 90% of the rest of the world, only 6% of US charitable giving goes to charities which work internationally.
This Fund is focused on helping those who need it most, wherever that help can make the biggest difference. By building a mixed portfolio of direct and indirect interventions, such as policy work, we aim to:
Improve the lives of the world's most vulnerable people.
Reduce the number of easily preventable deaths worldwide.
Work towards sustainable, systemic change.`,
},
{
name: 'YIMBY Law',
website: 'https://www.yimbylaw.org/',
photo: 'https://i.imgur.com/zlzp21Z.png',
preview:
'YIMBY Law works to make housing in California more accessible and affordable, by enforcing state housing laws.',
description: `
YIMBY Law works to make housing in California more accessible and affordable. Our method is to enforce state housing laws, and some examples are outlined below. We send letters to cities considering zoning or general plan compliant housing developments informing them of their duties under state law, and sue them when they do not comply.
If you would like to support our work, you can do so by getting involved or by donating.`,
},
{
name: 'CaRLA',
website: 'https://carlaef.org/',
photo: 'https://i.imgur.com/IsNVTOY.png',
preview:
'The California Renters Legal Advocacy and Education Funds core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.',
description: `
The California Renters Legal Advocacy and Education Funds core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.
CaRLA uses legal advocacy and education to ensure all cities comply with their own zoning and state housing laws and do their part to help solve the states housing shortage.
In addition to housing impact litigation, we provide free legal aid, education and workshops, counseling and advocacy to advocates, homeowners, small developers, and city and state government officials.`,
},
{
name: 'Mriya',
website: 'https://mriya-ua.org/',
photo:
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2Fdefault%2Fci2h3hStFM.47?alt=media&token=0d2cdc3d-e4d8-4f5e-8f23-4a586b6ff637',
preview: 'Donate supplies to soldiers in Ukraine',
description:
'Donate supplies to soldiers in Ukraine, including tourniquets and plate carriers.',
},
].map((charity) => {
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
return {

View File

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

168
common/contract-details.ts Normal file
View File

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

View File

@ -1,14 +1,23 @@
import { Answer } from './answer'
import { Fees } from './fees'
import { JSONContent } from '@tiptap/core'
import { GroupLink } from 'common/group'
export type AnyMechanism = DPM | CPMM
export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
export type AnyOutcomeType =
| Binary
| MultipleChoice
| PseudoNumeric
| FreeResponse
| Numeric
export type AnyContractType =
| (CPMM & Binary)
| (CPMM & PseudoNumeric)
| (DPM & Binary)
| (DPM & FreeResponse)
| (DPM & Numeric)
| (DPM & MultipleChoice)
export type Contract<T extends AnyContractType = AnyContractType> = {
id: string
@ -20,10 +29,10 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
creatorAvatarUrl?: string
question: string
description: string // More info about what the contract is about
description: string | JSONContent // More info about what the contract is about
tags: string[]
lowercaseTags: string[]
visibility: 'public' | 'unlisted'
visibility: visibility
createdTime: number // Milliseconds since epoch
lastUpdatedTime?: number // Updated on new bet or comment
@ -41,14 +50,30 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
volume: number
volume24Hours: number
volume7Days: number
elasticity: number
collectedFees: Fees
groupSlugs?: string[]
groupLinks?: GroupLink[]
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
export type PseudoNumericContract = Contract & PseudoNumeric
export type BinaryContract = Contract & Binary
export type PseudoNumericContract = Contract & PseudoNumeric
export type NumericContract = Contract & Numeric
export type FreeResponseContract = Contract & FreeResponse
export type MultipleChoiceContract = Contract & MultipleChoice
export type DPMContract = Contract & DPM
export type CPMMContract = Contract & CPMM
export type DPMBinaryContract = BinaryContract & DPM
@ -67,7 +92,14 @@ export type CPMM = {
mechanism: 'cpmm-1'
pool: { [outcome: string]: number }
p: number // probability constant in y^p * n^(1-p) = k
totalLiquidity: number // in M$
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
}
}
export type Binary = {
@ -96,6 +128,13 @@ export type FreeResponse = {
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
}
export type MultipleChoice = {
outcomeType: 'MULTIPLE_CHOICE'
answers: Answer[]
resolution?: string | 'MKT' | 'CANCEL'
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
}
export type Numeric = {
outcomeType: 'NUMERIC'
bucketCount: number
@ -108,10 +147,19 @@ export type Numeric = {
export type outcomeType = AnyOutcomeType['outcomeType']
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'PSEUDO_NUMERIC', 'NUMERIC'] as const
export const OUTCOME_TYPES = [
'BINARY',
'MULTIPLE_CHOICE',
'FREE_RESPONSE',
'PSEUDO_NUMERIC',
'NUMERIC',
] as const
export const MAX_QUESTION_LENGTH = 480
export const MAX_DESCRIPTION_LENGTH = 10000
export const MAX_QUESTION_LENGTH = 240
export const MAX_DESCRIPTION_LENGTH = 16000
export const MAX_TAG_LENGTH = 60
export const CPMM_MIN_POOL_QTY = 0.01
export type visibility = 'public' | 'unlisted'
export const VISIBILITIES = ['public', 'unlisted'] as const

20
common/economy.ts Normal file
View File

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

View File

@ -21,18 +21,38 @@ export function isWhitelisted(email?: string) {
}
// TODO: Before open sourcing, we should turn these into env vars
export function isAdmin(email: string) {
export function isAdmin(email?: string) {
if (!email) {
return false
}
return ENV_CONFIG.adminEmails.includes(email)
}
export function isManifoldId(userId: string) {
return userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2'
}
export const DOMAIN = ENV_CONFIG.domain
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) + '$'
)
// Vercel deployments, used for testing.
export const CORS_ORIGIN_VERCEL = new RegExp(
'^https?://[a-zA-Z0-9\\-]+' + escapeRegExp('mantic.vercel.app') + '$'
)
// Any localhost server on any port
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
export function firestoreConsolePath(contractId: string) {
return `https://console.firebase.google.com/project/${PROJECT_ID}/firestore/data/~2Fcontracts~2F${contractId}`
}

View File

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

View File

@ -2,6 +2,8 @@ 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
@ -15,13 +17,31 @@ 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[]
// Currency controls
fixedAnte?: number
startingBalance?: number
economy?: Economy
}
export type Economy = {
FIXED_ANTE?: number
STARTING_BALANCE?: number
SUS_STARTING_BALANCE?: number
REFERRAL_AMOUNT?: number
UNIQUE_BETTOR_BONUS_AMOUNT?: number
BETTING_STREAK_BONUS_AMOUNT?: number
BETTING_STREAK_BONUS_MAX?: number
BETTING_STREAK_RESET_HOUR?: number
FREE_MARKETS_PER_USER_MAX?: number
COMMENT_BOUNTY_AMOUNT?: number
}
type FirebaseConfig = {
@ -38,6 +58,7 @@ type FirebaseConfig = {
export const PROD_CONFIG: EnvConfig = {
domain: 'manifold.markets',
amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15',
sprigEnvironmentId: 'sQcrq9TDqkib',
firebaseConfig: {
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
@ -49,6 +70,7 @@ export const PROD_CONFIG: EnvConfig = {
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
measurementId: 'G-SSFK1Q138D',
},
twitchBotEndpoint: 'https://twitch-bot.manifold.markets',
cloudRunId: 'nggbo3neva',
cloudRunRegion: 'uc',
adminEmails: [
@ -57,10 +79,17 @@ export const PROD_CONFIG: EnvConfig = {
'taowell@gmail.com', // Stephen
'abc.sinclair@gmail.com', // Sinclair
'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: [

View File

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

View File

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

3
common/globalConfig.ts Normal file
View File

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

View File

@ -6,10 +6,37 @@ export type Group = {
creatorId: string // User id
createdTime: number
mostRecentActivityTime: number
memberIds: string[] // User ids
anyoneCanJoin: boolean
contractIds: string[]
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' }[]
}
export const MAX_GROUP_NAME_LENGTH = 75
export const MAX_ABOUT_LENGTH = 140
export const MAX_ID_LENGTH = 60
export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome']
export const GROUP_CHAT_SLUG = 'chat'
export type GroupLink = {
slug: string
name: string
groupId: string
createdTime: number
userId?: string
}
export type GroupContractDoc = { contractId: string; createdTime: number }

9
common/like.ts Normal file
View File

@ -0,0 +1,9 @@
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

138
common/loans.ts Normal file
View File

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

View File

@ -1,6 +1,6 @@
import { sumBy } from 'lodash'
import { sortBy, sum, sumBy } from 'lodash'
import { Bet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
import { Bet, fill, LimitBet, NumericBet } from './bet'
import {
calculateDpmShares,
getDpmProbability,
@ -8,19 +8,32 @@ import {
getNumericBets,
calculateNumericDpmShares,
} from './calculate-dpm'
import { calculateCpmmPurchase, getCpmmProbability } from './calculate-cpmm'
import {
calculateCpmmAmountToProb,
calculateCpmmPurchase,
CpmmState,
getCpmmProbability,
} from './calculate-cpmm'
import {
CPMMBinaryContract,
DPMBinaryContract,
FreeResponseContract,
DPMContract,
NumericContract,
PseudoNumericContract,
} from './contract'
import { noFees } from './fees'
import { addObjects } from './util/object'
import { addObjects, removeUndefinedProps } from './util/object'
import { NUMERIC_FIXED_VAR } from './numeric-constants'
import {
floatingEqual,
floatingGreaterEqual,
floatingLesserEqual,
} from './util/math'
export type CandidateBet<T extends Bet = Bet> = Omit<T, 'id' | 'userId'>
export type CandidateBet<T extends Bet = Bet> = Omit<
T,
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
>
export type BetInfo = {
newBet: CandidateBet
newPool?: { [outcome: string]: number }
@ -30,45 +43,261 @@ export type BetInfo = {
newP?: number
}
export const getNewBinaryCpmmBetInfo = (
outcome: 'YES' | 'NO',
const computeFill = (
amount: number,
contract: CPMMBinaryContract | PseudoNumericContract,
loanAmount: number
outcome: 'YES' | 'NO',
limitProb: number | undefined,
cpmmState: CpmmState,
matchedBet: LimitBet | undefined
) => {
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
contract,
amount,
outcome
)
const prob = getCpmmProbability(cpmmState.pool, cpmmState.p)
const { pool, p, totalLiquidity } = contract
const probBefore = getCpmmProbability(pool, p)
const probAfter = getCpmmProbability(newPool, newP)
const newBet: CandidateBet = {
contractId: contract.id,
amount,
shares,
outcome,
fees,
loanAmount,
probBefore,
probAfter,
createdTime: Date.now(),
if (
limitProb !== undefined &&
(outcome === 'YES'
? floatingGreaterEqual(prob, limitProb) &&
(matchedBet?.limitProb ?? 1) > limitProb
: floatingLesserEqual(prob, limitProb) &&
(matchedBet?.limitProb ?? 0) < limitProb)
) {
// No fill.
return undefined
}
const { liquidityFee } = fees
const newTotalLiquidity = (totalLiquidity ?? 0) + liquidityFee
const timestamp = Date.now()
return { newBet, newPool, newP, newTotalLiquidity }
if (
!matchedBet ||
(outcome === 'YES'
? !floatingGreaterEqual(prob, matchedBet.limitProb)
: !floatingLesserEqual(prob, matchedBet.limitProb))
) {
// Fill from pool.
const limit = !matchedBet
? limitProb
: outcome === 'YES'
? Math.min(matchedBet.limitProb, limitProb ?? 1)
: Math.max(matchedBet.limitProb, limitProb ?? 0)
const buyAmount =
limit === undefined
? amount
: Math.min(amount, calculateCpmmAmountToProb(cpmmState, limit, outcome))
const { shares, newPool, newP, fees } = calculateCpmmPurchase(
cpmmState,
buyAmount,
outcome
)
const newState = { pool: newPool, p: newP }
return {
maker: {
matchedBetId: null,
shares,
amount: buyAmount,
state: newState,
fees,
timestamp,
},
taker: {
matchedBetId: null,
shares,
amount: buyAmount,
timestamp,
},
}
}
// Fill from matchedBet.
const matchRemaining = matchedBet.orderAmount - matchedBet.amount
const shares = Math.min(
amount /
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
matchRemaining /
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb)
)
const maker = {
bet: matchedBet,
matchedBetId: 'taker',
amount:
shares *
(outcome === 'YES' ? 1 - matchedBet.limitProb : matchedBet.limitProb),
shares,
timestamp,
}
const taker = {
matchedBetId: matchedBet.id,
amount:
shares *
(outcome === 'YES' ? matchedBet.limitProb : 1 - matchedBet.limitProb),
shares,
timestamp,
}
return { maker, taker }
}
export const computeFills = (
outcome: 'YES' | 'NO',
betAmount: number,
state: CpmmState,
limitProb: number | undefined,
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => {
if (isNaN(betAmount)) {
throw new Error('Invalid bet amount: ${betAmount}')
}
if (isNaN(limitProb ?? 0)) {
throw new Error('Invalid limitProb: ${limitProb}')
}
const sortedBets = sortBy(
unfilledBets.filter((bet) => bet.outcome !== outcome),
(bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb),
(bet) => bet.createdTime
)
const takers: fill[] = []
const makers: {
bet: LimitBet
amount: number
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) {
const matchedBet: LimitBet | undefined = sortedBets[i]
const fill = computeFill(amount, outcome, limitProb, cpmmState, matchedBet)
if (!fill) break
const { taker, maker } = fill
if (maker.matchedBetId === null) {
// Matched against pool.
cpmmState = maker.state
totalFees = addObjects(totalFees, maker.fees)
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)
}
amount -= taker.amount
if (floatingEqual(amount, 0)) break
}
return { takers, makers, totalFees, cpmmState, ordersToCancel }
}
export const getBinaryCpmmBetInfo = (
outcome: 'YES' | 'NO',
betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number | undefined,
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => {
const { pool, p } = contract
const { takers, makers, cpmmState, totalFees, ordersToCancel } = computeFills(
outcome,
betAmount,
{ pool, p },
limitProb,
unfilledBets,
balanceByUserId
)
const probBefore = getCpmmProbability(contract.pool, contract.p)
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
const takerAmount = sumBy(takers, 'amount')
const takerShares = sumBy(takers, 'shares')
const isFilled = floatingEqual(betAmount, takerAmount)
const newBet: CandidateBet = removeUndefinedProps({
orderAmount: betAmount,
amount: takerAmount,
shares: takerShares,
limitProb,
isFilled,
isCancelled: false,
fills: takers,
contractId: contract.id,
outcome,
probBefore,
probAfter,
loanAmount: 0,
createdTime: Date.now(),
fees: totalFees,
})
const { liquidityFee } = totalFees
const newTotalLiquidity = (contract.totalLiquidity ?? 0) + liquidityFee
return {
newBet,
newPool: cpmmState.pool,
newP: cpmmState.p,
newTotalLiquidity,
makers,
ordersToCancel,
}
}
export const getBinaryBetStats = (
outcome: 'YES' | 'NO',
betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number,
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => {
const { newBet } = getBinaryCpmmBetInfo(
outcome,
betAmount ?? 0,
contract,
limitProb,
unfilledBets,
balanceByUserId
)
const remainingMatched =
((newBet.orderAmount ?? 0) - newBet.amount) /
(outcome === 'YES' ? limitProb : 1 - limitProb)
const currentPayout = newBet.shares + remainingMatched
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
const totalFees = sum(Object.values(newBet.fees))
return { currentPayout, currentReturn, totalFees, newBet }
}
export const getNewBinaryDpmBetInfo = (
outcome: 'YES' | 'NO',
amount: number,
contract: DPMBinaryContract,
loanAmount: number
contract: DPMBinaryContract
) => {
const { YES: yesPool, NO: noPool } = contract.pool
@ -99,7 +328,7 @@ export const getNewBinaryDpmBetInfo = (
const newBet: CandidateBet = {
contractId: contract.id,
amount,
loanAmount,
loanAmount: 0,
shares,
outcome,
probBefore,
@ -114,8 +343,7 @@ export const getNewBinaryDpmBetInfo = (
export const getNewMultiBetInfo = (
outcome: string,
amount: number,
contract: FreeResponseContract,
loanAmount: number
contract: DPMContract
) => {
const { pool, totalShares, totalBets } = contract
@ -136,7 +364,7 @@ export const getNewMultiBetInfo = (
const newBet: CandidateBet = {
contractId: contract.id,
amount,
loanAmount,
loanAmount: 0,
shares,
outcome,
probBefore,
@ -190,13 +418,3 @@ export const getNumericBetsInfo = (
return { newBet, newPool, newTotalShares, newTotalBets }
}
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
const prevLoanAmount = sumBy(openBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = Math.min(
newBetAmount,
MAX_LOAN_PER_CONTRACT - prevLoanAmount
)
return loanAmount
}

View File

@ -5,13 +5,15 @@ import {
CPMM,
DPM,
FreeResponse,
MultipleChoice,
Numeric,
outcomeType,
PseudoNumeric,
visibility,
} from './contract'
import { User } from './user'
import { parseTags } from './util/parse'
import { removeUndefinedProps } from './util/object'
import { JSONContent } from '@tiptap/core'
export function getNewContract(
id: string,
@ -19,7 +21,7 @@ export function getNewContract(
creator: User,
question: string,
outcomeType: outcomeType,
description: string,
description: JSONContent,
initialProb: number,
ante: number,
closeTime: number,
@ -29,13 +31,12 @@ export function getNewContract(
bucketCount: number,
min: number,
max: number,
isLogScale: boolean
) {
const tags = parseTags(
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
)
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
isLogScale: boolean,
// for multiple choice
answers: string[],
visibility: visibility
) {
const propsByOutcomeType =
outcomeType === 'BINARY'
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
@ -43,6 +44,8 @@ export function getNewContract(
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
: outcomeType === 'NUMERIC'
? getNumericProps(ante, bucketCount, min, max)
: outcomeType === 'MULTIPLE_CHOICE'
? getMultipleChoiceProps(ante, answers)
: getFreeAnswerProps(ante)
const contract: Contract = removeUndefinedProps({
@ -56,10 +59,11 @@ export function getNewContract(
creatorAvatarUrl: creator.avatarUrl,
question: question.trim(),
description: description.trim(),
tags,
lowercaseTags,
visibility: 'public',
description,
tags: [],
lowercaseTags: [],
visibility,
unlistedById: visibility === 'unlisted' ? creator.id : undefined,
isResolved: false,
createdTime: Date.now(),
closeTime,
@ -67,6 +71,7 @@ export function getNewContract(
volume: 0,
volume24Hours: 0,
volume7Days: 0,
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
collectedFees: {
creatorFee: 0,
@ -107,9 +112,12 @@ 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
@ -146,6 +154,26 @@ const getFreeAnswerProps = (ante: number) => {
return system
}
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
const numAnswers = answers.length
const betAnte = ante / numAnswers
const betShares = Math.sqrt(ante ** 2 / numAnswers)
const defaultValues = (x: any) =>
Object.fromEntries(range(0, numAnswers).map((k) => [k, x]))
const system: DPM & MultipleChoice = {
mechanism: 'dpm-2',
outcomeType: 'MULTIPLE_CHOICE',
pool: defaultValues(betAnte),
totalShares: defaultValues(betShares),
totalBets: defaultValues(betAnte),
answers: [],
}
return system
}
const getNumericProps = (
ante: number,
bucketCount: number,

View File

@ -1,8 +1,10 @@
import { notification_preference } from './user-notification-preferences'
export type Notification = {
id: string
userId: string
reasonText?: string
reason?: notification_reason_types
reason?: notification_reason_types | notification_preference
createdTime: number
viewTime?: number
isSeen: boolean
@ -15,6 +17,7 @@ export type Notification = {
sourceUserUsername?: string
sourceUserAvatarUrl?: string
sourceText?: string
data?: { [key: string]: any }
sourceContractTitle?: string
sourceContractCreatorUsername?: string
@ -25,6 +28,7 @@ export type Notification = {
isSeenOnHref?: string
}
export type notification_source_types =
| 'contract'
| 'comment'
@ -37,6 +41,12 @@ export type notification_source_types =
| 'group'
| 'user'
| 'bonus'
| 'challenge'
| 'betting_streak_bonus'
| 'loan'
| 'like'
| 'tip_and_like'
| 'badge'
export type notification_source_update_types =
| 'created'
@ -45,19 +55,216 @@ 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'
| 'you_follow_user'
| 'added_you_to_group'
| 'contract_from_followed_user'
| '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
}

View File

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

View File

@ -8,6 +8,13 @@
},
"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",
"lodash": "4.17.21"
},
"devDependencies": {

View File

@ -2,14 +2,17 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
import { Bet, NumericBet } from './bet'
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
import { DPMContract, FreeResponseContract } from './contract'
import {
DPMContract,
FreeResponseContract,
MultipleChoiceContract,
} from './contract'
import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
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)
@ -54,17 +57,6 @@ 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,
@ -106,17 +98,6 @@ 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,
@ -159,17 +140,6 @@ 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,
@ -180,7 +150,7 @@ export const getDpmMktPayouts = (
export const getPayoutsMultiOutcome = (
resolutions: { [outcome: string]: number },
contract: FreeResponseContract,
contract: FreeResponseContract | MultipleChoiceContract,
bets: Bet[]
) => {
const poolTotal = sum(Object.values(contract.pool))
@ -198,7 +168,7 @@ export const getPayoutsMultiOutcome = (
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
const profit = winnings - amount
const payout = amount + (1 - DPM_FEES) * Math.max(0, profit)
const payout = amount + (1 - DPM_FEES) * profit
return { userId, profit, payout }
})
@ -212,16 +182,6 @@ 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,

View File

@ -1,5 +1,3 @@
import { sum } from 'lodash'
import { Bet } from './bet'
import { getProbability } from './calculate'
import { getCpmmLiquidityPoolWeights } from './calculate-cpmm'
@ -43,18 +41,6 @@ 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,
@ -69,10 +55,11 @@ export const getLiquidityPoolPayouts = (
outcome: string,
liquidities: LiquidityProvision[]
) => {
const { pool } = contract
const finalPool = pool[outcome]
const { pool, subsidyPool } = contract
const finalPool = pool[outcome] + (subsidyPool ?? 0)
if (finalPool < 1e-3) return []
const weights = getCpmmLiquidityPoolWeights(contract, liquidities)
const weights = getCpmmLiquidityPoolWeights(liquidities)
return Object.entries(weights).map(([providerId, weight]) => ({
userId: providerId,
@ -98,18 +85,6 @@ 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 }
@ -120,10 +95,11 @@ export const getLiquidityPoolProbPayouts = (
p: number,
liquidities: LiquidityProvision[]
) => {
const { pool } = contract
const finalPool = p * pool.YES + (1 - p) * pool.NO
const { pool, subsidyPool } = contract
const finalPool = p * pool.YES + (1 - p) * pool.NO + (subsidyPool ?? 0)
if (finalPool < 1e-3) return []
const weights = getCpmmLiquidityPoolWeights(contract, liquidities)
const weights = getCpmmLiquidityPoolWeights(liquidities)
return Object.entries(weights).map(([providerId, weight]) => ({
userId: providerId,

View File

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

29
common/post.ts Normal file
View File

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

View File

@ -16,8 +16,8 @@ export const getMappedValue =
const { min, max, isLogScale } = contract
if (isLogScale) {
const logValue = p * Math.log10(max - min)
return 10 ** logValue + min
const logValue = p * Math.log10(max - min + 1)
return 10 ** logValue + min - 1
}
return p * (max - min) + min
@ -37,8 +37,11 @@ export const getPseudoProbability = (
max: number,
isLogScale = false
) => {
if (value < min) return 0
if (value > max) return 1
if (isLogScale) {
return Math.log10(value - min) / Math.log10(max - min)
return Math.log10(value - min + 1) / Math.log10(max - min + 1)
}
return (value - min) / (max - min)

View File

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

View File

@ -13,8 +13,12 @@ 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 loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
const loanPayment = Math.min(loanAmount, shares)
const loanPayment = loanAmount * soldFrac
const netAmount = shares - loanPayment
return { shares, loanPayment, netAmount }
}

View File

@ -1,8 +1,9 @@
import { groupBy, sumBy, mapValues, partition } from 'lodash'
import { groupBy, sumBy, mapValues, keyBy, sortBy } from 'lodash'
import { Bet } from './bet'
import { getContractBetMetrics, resolvedPayout } from './calculate'
import { Contract } from './contract'
import { getPayouts } from './payouts'
import { ContractComment } from './comment'
export function scoreCreators(contracts: Contract[]) {
const creatorScore = mapValues(
@ -30,46 +31,11 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
}
export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
const { resolution } = contract
const resolutionProb =
contract.outcomeType == 'BINARY'
? contract.resolutionProbability
: undefined
const [closedBets, openBets] = partition(
bets,
(bet) => bet.isSold || bet.sale
const betsByUser = groupBy(bets, (bet) => bet.userId)
return mapValues(
betsByUser,
(bets) => getContractBetMetrics(contract, bets).profit
)
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(
@ -81,3 +47,47 @@ 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,
}
}

View File

@ -1,4 +1,4 @@
import { Bet } from './bet'
import { Bet, LimitBet } from './bet'
import {
calculateDpmShareValue,
deductDpmFees,
@ -7,12 +7,16 @@ import {
import { calculateCpmmSale, getCpmmProbability } from './calculate-cpmm'
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'>
export type CandidateBet<T extends Bet> = Omit<
T,
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
>
export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
const { pool, totalShares, totalBets } = contract
const { id: betId, amount, shares, outcome } = bet
const { id: betId, amount, shares, outcome, loanAmount } = bet
const adjShareValue = calculateDpmShareValue(contract, bet)
@ -63,6 +67,7 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
betId,
},
fees,
loanAmount: -(loanAmount ?? 0),
}
return {
@ -78,19 +83,25 @@ export const getCpmmSellBetInfo = (
shares: number,
outcome: 'YES' | 'NO',
contract: CPMMContract,
prevLoanAmount: number
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number },
loanPaid: number
) => {
const { pool, p } = contract
const { saleValue, newPool, newP, fees } = calculateCpmmSale(
const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale(
contract,
shares,
outcome
outcome,
unfilledBets,
balanceByUserId,
)
const loanPaid = Math.min(prevLoanAmount, saleValue)
const probBefore = getCpmmProbability(pool, p)
const probAfter = getCpmmProbability(newPool, p)
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
const takerAmount = sumBy(takers, 'amount')
const takerShares = sumBy(takers, 'shares')
console.log(
'SELL M$',
@ -104,20 +115,27 @@ export const getCpmmSellBetInfo = (
const newBet: CandidateBet<Bet> = {
contractId: contract.id,
amount: -saleValue,
shares: -shares,
amount: takerAmount,
shares: takerShares,
outcome,
probBefore,
probAfter,
createdTime: Date.now(),
loanAmount: -loanPaid,
fees,
fills: takers,
isFilled: true,
isCancelled: false,
orderAmount: takerAmount,
}
return {
newBet,
newPool,
newP,
newPool: cpmmState.pool,
newP: cpmmState.p,
fees,
makers,
takers,
ordersToCancel
}
}

View File

@ -1,20 +1,22 @@
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[]
weeklyActivationRate: number[]
topTenthActions: {
daily: number[]
weekly: number[]
monthly: number[]
}
dailyActivationRate: number[]
dailyActivationRateWeeklyAvg: number[]
manaBet: {
daily: number[]
weekly: number[]

View File

@ -1,6 +1,14 @@
// 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 | Bonus
type AnyTxnType =
| Donation
| Tip
| Manalink
| Referral
| UniqueBettorBonus
| BettingStreakBonus
| CancelUniqueBettorBonus
| CommentBountyRefund
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
export type Txn<T extends AnyTxnType = AnyTxnType> = {
@ -16,7 +24,16 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
amount: number
token: 'M$' // | 'USD' | MarketOutcome
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
category:
| 'CHARITY'
| 'MANALINK'
| 'TIP'
| 'REFERRAL'
| 'UNIQUE_BETTOR_BONUS'
| 'BETTING_STREAK_BONUS'
| 'CANCEL_UNIQUE_BETTOR_BONUS'
| 'COMMENT_BOUNTY'
| 'REFUND_COMMENT_BOUNTY'
// Any extra data
data?: { [key: string]: any }
@ -36,8 +53,9 @@ type Tip = {
toType: 'USER'
category: 'TIP'
data: {
contractId: string
commentId: string
contractId?: string
groupId?: string
}
}
@ -53,13 +71,70 @@ type Referral = {
category: 'REFERRAL'
}
type Bonus = {
type UniqueBettorBonus = {
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
}
}
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

View File

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

View File

@ -1,4 +1,6 @@
import { notification_preferences } from './user-notification-preferences'
import { ENV_CONFIG } from './envs/constants'
import { MarketCreatorBadge, ProvenCorrectBadge, StreakerBadge } from './badge'
export type User = {
id: string
@ -10,7 +12,6 @@ export type User = {
// For their user page
bio?: string
bannerUrl?: string
website?: string
twitterHandle?: string
discordHandle?: string
@ -32,36 +33,58 @@ export type User = {
allTime: number
}
fractionResolvedCorrectly: number
nextLoanCached: number
followerCountCached: number
followedCategories?: string[]
homeSections?: string[]
referredByUserId?: string
referredByContractId?: string
referredByGroupId?: string
lastPingTime?: number
shouldShowWelcome?: boolean
lastBetTime?: number
currentBettingStreak?: number
hasSeenContractFollowModal?: boolean
freeMarketsCreated?: number
isBannedFromPosting?: boolean
achievements: {
provenCorrect?: {
badges: ProvenCorrectBadge[]
}
marketCreator?: {
badges: MarketCreatorBadge[]
}
streaker?: {
badges: StreakerBadge[]
}
}
}
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
// for sus users, i.e. multiple sign ups for same person
export const SUS_STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 10
export const REFERRAL_AMOUNT = 500
export type PrivateUser = {
id: string // same as User.id
username: string // denormalized from User
email?: string
unsubscribedFromResolutionEmails?: boolean
unsubscribedFromCommentEmails?: boolean
unsubscribedFromAnswerEmails?: boolean
unsubscribedFromGenericEmails?: boolean
weeklyTrendingEmailSent?: boolean
weeklyPortfolioUpdateEmailSent?: boolean
manaBonusEmailSent?: boolean
initialDeviceToken?: string
initialIpAddress?: string
apiKey?: string
notificationPreferences?: notification_subscribe_types
lastTimeCheckedBonuses?: number
notificationPreferences: notification_preferences
twitchInfo?: {
twitchName: string
controlToken: string
botEnabled?: boolean
needsRelinking?: boolean
}
}
export type notification_subscribe_types = 'all' | 'less' | 'none'
export type PortfolioMetrics = {
investmentValue: number
balance: number
@ -69,3 +92,16 @@ export type PortfolioMetrics = {
timestamp: number
userId: string
}
export const MANIFOLD_USER_USERNAME = 'ManifoldMarkets'
export const MANIFOLD_USER_NAME = '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'

22
common/util/algos.ts Normal file
View File

@ -0,0 +1,22 @@
export function binarySearch(
min: number,
max: number,
comparator: (x: number) => number
) {
let mid = 0
while (true) {
mid = min + (max - min) / 2
// Break once we've reached max precision.
if (mid === min || mid === max) break
const comparison = comparator(mid)
if (comparison === 0) break
else if (comparison > 0) {
max = mid
} else {
min = mid
}
}
return mid
}

View File

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

24
common/util/color.ts Normal file
View File

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

View File

@ -8,7 +8,14 @@ const formatter = new Intl.NumberFormat('en-US', {
})
export function formatMoney(amount: number) {
const newAmount = Math.round(amount) === 0 ? 0 : Math.floor(amount) // handle -0 case
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)
)
return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
}
@ -33,18 +40,34 @@ export function formatPercent(zeroToOne: number) {
return (zeroToOne * 100).toFixed(decimalPlaces) + '%'
}
const showPrecision = (x: number, sigfigs: number) =>
// convert back to number for weird formatting reason
`${Number(x.toPrecision(sigfigs))}`
// Eg 1234567.89 => 1.23M; 5678 => 5.68K
export function formatLargeNumber(num: number, sigfigs = 2): string {
const absNum = Math.abs(num)
if (absNum < 1000) {
return '' + Number(num.toPrecision(sigfigs))
}
if (absNum < 1) return showPrecision(num, sigfigs)
if (absNum < 100) return showPrecision(num, 2)
if (absNum < 1000) return showPrecision(num, 3)
if (absNum < 10000) return showPrecision(num, 4)
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
const suffixIdx = Math.floor(Math.log10(absNum) / 3)
const suffixStr = suffix[suffixIdx]
const numStr = (num / Math.pow(10, 3 * suffixIdx)).toPrecision(sigfigs)
return `${Number(numStr)}${suffixStr}`
const i = Math.floor(Math.log10(absNum) / 3)
const numStr = showPrecision(num / Math.pow(10, 3 * i), sigfigs)
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) {

View File

@ -34,3 +34,17 @@ export function median(xs: number[]) {
export function average(xs: number[]) {
return sum(xs) / xs.length
}
const EPSILON = 0.00000001
export function floatingEqual(a: number, b: number, epsilon = EPSILON) {
return Math.abs(a - b) < epsilon
}
export function floatingGreaterEqual(a: number, b: number, epsilon = EPSILON) {
return a + epsilon >= b
}
export function floatingLesserEqual(a: number, b: number, epsilon = EPSILON) {
return a - epsilon <= b
}

View File

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

View File

@ -1,29 +1,115 @@
import { MAX_TAG_LENGTH } from '../contract'
import { generateText, JSONContent, Node } from '@tiptap/core'
import { generateJSON } from '@tiptap/html'
// Tiptap starter extensions
import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold'
import { BulletList } from '@tiptap/extension-bullet-list'
import { Code } from '@tiptap/extension-code'
import { CodeBlock } from '@tiptap/extension-code-block'
import { Document } from '@tiptap/extension-document'
import { HardBreak } from '@tiptap/extension-hard-break'
import { Heading } from '@tiptap/extension-heading'
import { History } from '@tiptap/extension-history'
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
import { Italic } from '@tiptap/extension-italic'
import { ListItem } from '@tiptap/extension-list-item'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Strike } from '@tiptap/extension-strike'
import { Text } from '@tiptap/extension-text'
// other tiptap extensions
import { Image } from '@tiptap/extension-image'
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'
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)
}
/** 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
}
// TODO: fuzzy matching
export const wordIn = (word: string, corpus: string) =>
corpus.toLocaleLowerCase().includes(word.toLocaleLowerCase())
const checkAgainstQuery = (query: string, corpus: string) =>
query.split(' ').every((word) => wordIn(word, corpus))
export const searchInAny = (query: string, ...fields: string[]) =>
fields.some((field) => checkAgainstQuery(query, field))
/** @return user ids of all \@mentions */
export function parseMentions(data: JSONContent): string[] {
const mentions = data.content?.flatMap(parseMentions) ?? [] //dfs
if (data.type === 'mention' && data.attrs) {
mentions.push(data.attrs.id as string)
}
uniqueTags.reverse()
return uniqueTags
return uniq(mentions)
}
export function parseWordsAsTags(text: string) {
const taggedText = text
.split(/\s+/)
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
.join(' ')
return parseTags(taggedText)
// TODO: this is a hack to get around the fact that tiptap doesn't have a
// way to add a node view without bundling in tsx
function skippableComponent(name: string): Node<any, any> {
return Node.create({
name,
group: 'block',
content: 'inline*',
parseHTML() {
return [
{
tag: 'grid-cards-component',
},
]
},
})
}
const stringParseExts = [
// StarterKit extensions
Blockquote,
Bold,
BulletList,
Code,
CodeBlock,
Document,
HardBreak,
Heading,
History,
HorizontalRule,
Italic,
ListItem,
OrderedList,
Paragraph,
Strike,
Text,
// other extensions
Link,
Image.extend({ renderText: () => '[image]' }),
Mention, // user @mention
Mention.extend({ name: 'contract-mention' }), // market %mention
Iframe.extend({
renderText: ({ node }) =>
'[embed]' + node.attrs.src ? `(${node.attrs.src})` : '',
}),
skippableComponent('gridCardsComponent'),
skippableComponent('staticReactEmbedComponent'),
TiptapTweet.extend({ renderText: () => '[tweet]' }),
TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }),
]
export function richTextToString(text?: JSONContent) {
if (!text) return ''
return generateText(text, stringParseExts)
}
export function htmlToRichText(html: string) {
return generateJSON(html, stringParseExts)
}

View File

@ -46,3 +46,10 @@ 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)
}

View File

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

View File

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

View File

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

View File

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

43
dev.sh Executable file
View File

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

View File

@ -34,6 +34,54 @@ response was a 4xx or 5xx.)
## Endpoints
### `GET /v0/user/[username]`
Gets a user by their username. Remember that usernames may change.
Requires no authorization.
### `GET /v0/user/by-id/[id]`
Gets a user by their unique ID. Many other API endpoints return this as the `userId`.
Requires no authorization.
### GET /v0/me
Returns the authenticated user.
### `GET /v0/groups`
Gets all groups, in no particular order.
Parameters:
- `availableToUserId`: Optional. if specified, only groups that the user can
join and groups they've already joined will be returned.
Requires no authorization.
### `GET /v0/group/[slug]`
Gets a group by its slug.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]`
Gets a group by its unique ID.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]/markets`
Gets a group's markets by its unique ID.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/markets`
Lists all markets, ordered by creation date descending.
@ -63,7 +111,6 @@ Requires no authorization.
"creatorAvatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c",
"closeTime":1653893940000,
"question":"Will I write a new blog post today?",
"description":"I'm supposed to, or else Beeminder charges me $90.\nTentative topic ideas:\n- \"Manifold funding, a history\"\n- \"Markets and bounties allow trades through time\"\n- \"equity vs money vs time\"\n\nClose date updated to 2022-05-29 11:59 pm",
"tags":[
"personal",
"commitments"
@ -101,7 +148,6 @@ Requires no authorization.
// Market attributes. All times are in milliseconds since epoch
closeTime?: number // Min of creator's chosen date, and resolutionTime
question: string
description: string
// A list of tags on each market. Any user can add tags to any market.
// This list also includes the predefined categories shown as filters on the home page.
@ -112,13 +158,16 @@ 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, or NUMERIC
outcomeType: string // BINARY, FREE_RESPONSE, MULTIPLE_CHOICE, NUMERIC, or PSEUDO_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
@ -128,6 +177,8 @@ Requires no authorization.
resolutionTime?: number
resolution?: string
resolutionProbability?: number // Used for BINARY markets resolved to MKT
lastUpdatedTime?: number
}
```
@ -360,7 +411,9 @@ Requires no authorization.
type FullMarket = LiteMarket & {
bets: Bet[]
comments: Comment[]
answers?: Answer[]
answers?: Answer[] // dpm-2 markets only
description: JSONContent // Rich text content. See https://tiptap.dev/guide/output#option-1-json
textDescription: string // string description without formatting, images, or embeds
}
type Bet = {
@ -469,6 +522,20 @@ Parameters:
answer. For numeric markets, this is a string representing the target bucket,
and an additional `value` parameter is required which is a number representing
the target value. (Bet on numeric markets at your own peril.)
- `limitProb`: Optional. A number between `0.001` and `0.999` inclusive representing
the limit probability for your bet (i.e. 0.1% to 99.9% — multiply by 100 for the
probability percentage).
The bet will execute immediately in the direction of `outcome`, but not beyond this
specified limit. If not all the bet is filled, the bet will remain as an open offer
that can later be matched against an opposite direction bet.
- For example, if the current market probability is `50%`:
- A `M$10` bet on `YES` with `limitProb=0.4` would not be filled until the market
probability moves down to `40%` and someone bets `M$15` of `NO` to match your
bet odds.
- A `M$100` bet on `YES` with `limitProb=0.6` would fill partially or completely
depending on current unfilled limit bets and the AMM's liquidity. Any remaining
portion of the bet not filled would remain to be matched against in the future.
- An unfilled limit order bet can be cancelled using the cancel API.
Example request:
@ -480,15 +547,20 @@ $ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application
"contractId":"{...}"}'
```
### `POST /v0/bet/cancel/[id]`
Cancel the limit order of a bet with the specified id. If the bet was unfilled, it will be cancelled so that no other bets will match with it. This is action irreversable.
### `POST /v0/market`
Creates a new market on behalf of the authorized user.
Parameters:
- `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, or `NUMERIC`.
- `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, `MULTIPLE_CHOICE`, or `PSEUDO_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).
- `closeTime`: Required. The time at which the market will close, represented as milliseconds since the epoch.
- `tags`: Optional. An array of string tags for the market.
@ -500,6 +572,12 @@ 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:
@ -513,6 +591,18 @@ $ 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.
@ -524,15 +614,18 @@ For binary markets:
- `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`.
- `probabilityInt`: Optional. The probability to use for `MKT` resolution.
For free response markets:
For free response or multiple choice 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.
- `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.
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:
@ -567,8 +660,128 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
]}'
```
### `POST /v0/market/[marketId]/sell`
Sells some quantity of shares in a binary market on behalf of the authorized user.
Parameters:
- `outcome`: Optional. One of `YES`, or `NO`. If you leave it off, and you only
own one kind of shares, you will sell that kind of shares.
- `shares`: Optional. The amount of shares to sell of the outcome given
above. If not provided, all the shares you own will be sold.
Example request:
```
$ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Key {...}' \
--data-raw '{"outcome": "YES", "shares": 10}'
```
### `POST /v0/comment`
Creates a comment in the specified market. Only supports top-level comments for now.
Parameters:
- `contractId`: Required. The ID of the market to comment on.
- `content`: The comment to post, formatted as [TipTap json](https://tiptap.dev/guide/output#option-1-json), OR
- `html`: The comment to post, formatted as an HTML string, OR
- `markdown`: The comment to post, formatted as a markdown string.
### `GET /v0/bets`
Gets a list of bets, ordered by creation date descending.
Parameters:
- `username`: Optional. If set, the response will include only bets created by this user.
- `market`: Optional. The slug of a market. If set, the response will only include bets on this market.
- `limit`: Optional. How many bets to return. The maximum and the default is 1000.
- `before`: Optional. The ID of the bet before which the list will start. For
example, if you ask for the most recent 10 bets, and then perform a second
query for 10 more bets with `before=[the id of the 10th bet]`, you will
get bets 11 through 20.
Requires no authorization.
- Example request
```
https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-i-be-able-to-place-a-limit-ord
```
- Response type: A `Bet[]`.
- <details><summary>Example response</summary><p>
```json
[
// Limit bet, partially filled.
{
"isFilled": false,
"amount": 15.596681605353808,
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
"contractId": "Tz5dA01GkK5QKiQfZeDL",
"probBefore": 0.5730753474948571,
"isCancelled": false,
"outcome": "YES",
"fees": { "creatorFee": 0, "liquidityFee": 0, "platformFee": 0 },
"shares": 31.193363210707616,
"limitProb": 0.5,
"id": "yXB8lVbs86TKkhWA1FVi",
"loanAmount": 0,
"orderAmount": 100,
"probAfter": 0.5730753474948571,
"createdTime": 1659482775970,
"fills": [
{
"timestamp": 1659483249648,
"matchedBetId": "MfrMd5HTiGASDXzqibr7",
"amount": 15.596681605353808,
"shares": 31.193363210707616
}
]
},
// Normal bet (no limitProb specified).
{
"shares": 17.350459904608414,
"probBefore": 0.5304358279113885,
"isFilled": true,
"probAfter": 0.5730753474948571,
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
"amount": 10,
"contractId": "Tz5dA01GkK5QKiQfZeDL",
"id": "1LPJHNz5oAX4K6YtJlP1",
"fees": {
"platformFee": 0,
"liquidityFee": 0,
"creatorFee": 0.4251333951457593
},
"isCancelled": false,
"loanAmount": 0,
"orderAmount": 10,
"fills": [
{
"amount": 10,
"matchedBetId": null,
"shares": 17.350459904608414,
"timestamp": 1659482757271
}
],
"createdTime": 1659482757271,
"outcome": "YES"
}
]
```
</p>
</details>
## 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
- 2022-02-28: Add `resolutionTime` to markets, change `closeTime` definition

View File

@ -8,15 +8,40 @@ 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
- [manifold-sdk](https://github.com/keriwarr/manifold-sdk) - TypeScript/JavaScript client for the Manifold API
## Bots
- [@manifold@botsin.space](https://botsin.space/@manifold) - Posts new Manifold markets to Mastodon
- [James' Bot](https://github.com/manifoldmarkets/market-maker) — Simple trading bot that makes markets
- [mana](https://github.com/AnnikaCodes/mana) - A Discord bot for Manifold by [@arae](https://manifold.markets/arae)
## Writeups
- [Information Markets, Decision Markets, Attention Markets, Action Markets](https://astralcodexten.substack.com/p/information-markets-decision-markets) by Scott Alexander
- [Mismatched Monetary Motivation in Manifold Markets](https://kevin.zielnicki.com/2022/02/17/manifold/) by Kevin Zielnicki
- [Introducing the Salem/CSPI Forecasting Tournament](https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting) by Richard Hanania
- [What I learned about running a betting market game night contest](https://shakeddown.wordpress.com/2022/08/04/what-i-learned-about-running-a-betting-market-game-night-contest/) by shakeddown
- [Free-riding on prediction markets](https://pedunculate.substack.com/p/free-riding-on-prediction-markets) by John Roxton
## Art
- Folded origami and doodles by [@hamnox](https://manifold.markets/hamnox) ![](https://i.imgur.com/nVGY4pL.png)
- Laser-cut Foldy by [@wasabipesto](https://manifold.markets/wasabipesto) ![](https://i.imgur.com/g9S6v3P.jpg)
## Alumni
_These projects are no longer active, but were really really cool!_
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government

View File

@ -15,6 +15,22 @@ 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**

View File

@ -4,11 +4,7 @@
### Do I have to pay real money in order to participate?
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.
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.
### Can M$ be sold for real money?

View File

@ -26,8 +26,7 @@ const config = {
docs: {
routeBasePath: '/',
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
editUrl: 'https://github.com/manifoldmarkets/manifold/tree/main/docs/docs',
editUrl: 'https://github.com/manifoldmarkets/manifold/tree/main/docs',
remarkPlugins: [math],
rehypePlugins: [katex],
},
@ -72,7 +71,7 @@ const config = {
label: 'Docs',
},
{
href: 'https://github.com/manifoldmarkets/docs',
href: 'https://github.com/manifoldmarkets/manifold/tree/main/docs/docs',
label: 'GitHub',
position: 'right',
},
@ -116,7 +115,7 @@ const config = {
},
{
label: 'GitHub',
href: 'https://github.com/manifoldmarkets/docs',
href: 'https://github.com/manifoldmarkets/manifold/',
},
],
},

View File

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

View File

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

View File

@ -22,6 +22,20 @@
}
]
},
{
"collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "isFilled",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP",
@ -36,6 +50,84 @@
}
]
},
{
"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",
@ -78,6 +170,42 @@
}
]
},
{
"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",
@ -124,6 +252,46 @@
}
]
},
{
"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",
@ -306,24 +474,6 @@
}
]
},
{
"collectionGroup": "txns",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "toId",
"order": "ASCENDING"
},
{
"fieldPath": "toType",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "manalinks",
"queryScope": "COLLECTION",
@ -338,6 +488,34 @@
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isSeen",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "portfolioHistory",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "timestamp",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "portfolioHistory",
"queryScope": "COLLECTION",
@ -351,6 +529,24 @@
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "txns",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "toId",
"order": "ASCENDING"
},
{
"fieldPath": "toType",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
}
],
"fieldOverrides": [
@ -424,6 +620,28 @@
}
]
},
{
"collectionGroup": "bets",
"fieldPath": "id",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "bets",
"fieldPath": "userId",
@ -446,6 +664,28 @@
}
]
},
{
"collectionGroup": "comments",
"fieldPath": "contractId",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "comments",
"fieldPath": "createdTime",

View File

@ -6,50 +6,89 @@ service cloud.firestore {
match /databases/{database}/documents {
function isAdmin() {
return request.auth.uid == 'igi2zGXsfxYPgB0DJTXVJVmwCOr2' // Austin
|| request.auth.uid == '5LZ4LgYuySdL1huCWe7bti02ghx2' // James
|| request.auth.uid == 'tlmGNz9kjXc2EteizMORes4qvWl2' // Stephen
|| request.auth.uid == 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // Manifold
return request.auth.token.email in [
'akrolsmir@gmail.com',
'jahooma@gmail.com',
'taowell@gmail.com',
'abc.sinclair@gmail.com',
'manticmarkets@gmail.com',
'iansphilips@gmail.com',
'd4vidchee@gmail.com',
'federicoruizcassarino@gmail.com',
'ingawei@gmail.com'
]
}
match /stats/stats {
allow read;
}
match /globalConfig/globalConfig {
allow read;
allow update: if isAdmin()
allow create: if isAdmin()
}
match /users/{userId} {
allow read;
allow update: if resource.data.id == request.auth.uid
allow update: if userId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']);
allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['referredByUserId'])
// only one referral allowed per user
&& !("referredByUserId" in resource.data)
// user can't refer themselves
&& !(resource.data.id == request.resource.data.referredByUserId);
// quid pro quos enabled (only once though so nbd) - bc I can't make this work:
// && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id);
.hasOnly(['bio', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome', 'hasSeenContractFollowModal', 'homeSections']);
// User referral rules
allow update: if userId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['referredByUserId', 'referredByContractId', 'referredByGroupId'])
// only one referral allowed per user
&& !("referredByUserId" in resource.data)
// user can't refer themselves
&& !(userId == request.resource.data.referredByUserId);
// quid pro quos enabled (only once though so nbd) - bc I can't make this work:
// && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id);
}
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {
allow read;
}
match /{somePath=**}/contract-metrics/{contractId} {
allow read;
}
match /{somePath=**}/challenges/{challengeId}{
allow read;
}
match /contracts/{contractId}/follows/{userId} {
allow read;
allow create, delete: if userId == request.auth.uid;
}
match /contracts/{contractId}/challenges/{challengeId}{
allow read;
allow create: if request.auth.uid == request.resource.data.creatorId;
// allow update if there have been no claims yet and if the challenge is still open
allow update: if request.auth.uid == resource.data.creatorId;
}
match /users/{userId}/follows/{followUserId} {
allow read;
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;
}
match /private-users/{userId} {
allow read: if resource.data.id == request.auth.uid || isAdmin();
allow update: if (resource.data.id == request.auth.uid || isAdmin())
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', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences' ]);
.hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']);
}
match /private-users/{userId}/views/{viewId} {
@ -71,9 +110,9 @@ service cloud.firestore {
match /contracts/{contractId} {
allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['tags', 'lowercaseTags']);
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks', 'flaggedByUsernames']);
allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'closeTime'])
.hasOnly(['description', 'closeTime', 'question', 'visibility', 'unlistedById'])
&& resource.data.creatorId == request.auth.uid;
allow update: if isAdmin();
match /comments/{commentId} {
@ -134,25 +173,52 @@ service cloud.firestore {
.hasOnly(['isSeen', 'viewTime']);
}
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=**}/groupMembers/{memberId} {
allow read;
}
function isMember() {
return request.auth.uid in get(/databases/$(database)/documents/groups/$(groupId)).data.memberIds;
}
match /comments/{commentId} {
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) && isMember();
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) ;
}
}
}
}
}

3
functions/.env.dev Normal file
View File

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

View File

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

View File

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

1
functions/.yarnrc Normal file
View File

@ -0,0 +1 @@
save-prefix ""

View File

@ -20,13 +20,14 @@ 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
### For local development
#### (Installing) 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.`):
1. `$ brew install java`
2. `$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk`
2. `$ gcloud auth login` to authenticate the CLI tools to Google Cloud
3. `$ gcloud config set project <project-id>` to choose the project (`$ gcloud projects list` to see options)
4. `$ mkdir firestore_export` to create a folder to store the exported database
@ -34,10 +35,10 @@ Adapted from https://firebase.google.com/docs/functions/get-started
## Developing locally
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
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/)!)
## Firestore Commands
@ -53,7 +54,10 @@ Adapted from https://firebase.google.com/docs/functions/get-started
## Deploying
0. `$ firebase use prod` to switch to prod
0. After merging, you need to manually deploy to backend:
1. `git checkout main`
1. `git pull origin main`
1. `$ firebase use prod` to switch to prod
1. `$ firebase deploy --only functions` to push your changes live!
(Future TODO: auto-deploy functions on Git push)
@ -61,5 +65,6 @@ 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.test_secret="THE-API-KEY"`
- Set a secret: `$ firebase functions:secrets:set STRIPE_APIKEY`
- Then, enter the secret in the prompt.
- Read a secret: `$ firebase functions:secrets:access STRIPE_APIKEY`

View File

@ -5,17 +5,19 @@
"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 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.prod dist && cp .env.dev dist",
"compile": "tsc -b",
"watch": "tsc -w",
"shell": "yarn build && firebase functions:shell",
"start": "yarn shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log",
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export",
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
"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:backup-local": "firebase emulators:export --force ./firestore_export",
"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: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: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"
@ -24,18 +26,33 @@
"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",
"cors": "2.8.5",
"dayjs": "1.11.4",
"express": "4.18.1",
"firebase-admin": "10.0.0",
"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",
"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",
"firebase-functions-test": "0.3.3"
"@types/node-fetch": "2.6.2",
"firebase-functions-test": "0.3.3",
"puppeteer": "18.0.5"
},
"private": true
}

View File

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

View File

@ -1,105 +0,0 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { Contract } from '../../common/contract'
import { User } from '../../common/user'
import { removeUndefinedProps } from '../../common/util/object'
import { redeemShares } from './redeem-shares'
import { getNewLiquidityProvision } from '../../common/add-liquidity'
export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall(
async (
data: {
amount: number
contractId: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const { amount, contractId } = data
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' }
// run as transaction to prevent race conditions
return await firestore
.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${userId}`)
const userSnap = await transaction.get(userDoc)
if (!userSnap.exists)
return { status: 'error', message: 'User not found' }
const user = userSnap.data() as User
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists)
return { status: 'error', message: 'Invalid contract' }
const contract = contractSnap.data() as Contract
if (
contract.mechanism !== 'cpmm-1' ||
(contract.outcomeType !== 'BINARY' &&
contract.outcomeType !== 'PSEUDO_NUMERIC')
)
return { status: 'error', message: 'Invalid contract' }
const { closeTime } = contract
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
if (user.balance < amount)
return { status: 'error', message: 'Insufficient balance' }
const newLiquidityProvisionDoc = firestore
.collection(`contracts/${contractId}/liquidity`)
.doc()
const { newLiquidityProvision, newPool, newP, newTotalLiquidity } =
getNewLiquidityProvision(
user,
amount,
contract,
newLiquidityProvisionDoc.id
)
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
if (!isFinite(newBalance)) {
throw new Error('Invalid user balance for ' + user.username)
}
transaction.update(userDoc, {
balance: newBalance,
totalDeposits: newTotalDeposits,
})
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
return { status: 'success', newLiquidityProvision }
})
.then(async (result) => {
await redeemShares(userId, contractId)
return result
})
}
)
const firestore = admin.firestore()

View File

@ -0,0 +1,78 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Contract, CPMMContract } from '../../common/contract'
import { User } from '../../common/user'
import { getNewLiquidityProvision } from '../../common/add-liquidity'
import { APIError, newEndpoint, validate } from './api'
const bodySchema = z.object({
contractId: z.string(),
amount: z.number().gt(0),
})
export const addsubsidy = newEndpoint({}, async (req, auth) => {
const { amount, contractId } = validate(bodySchema, req.body)
if (!isFinite(amount) || amount < 1) throw new APIError(400, 'Invalid amount')
// run as transaction to prevent race conditions
return await firestore.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${auth.uid}`)
const userSnap = await transaction.get(userDoc)
if (!userSnap.exists) throw new APIError(400, 'User not found')
const user = userSnap.data() as User
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
const contract = contractSnap.data() as Contract
if (
contract.mechanism !== 'cpmm-1' ||
(contract.outcomeType !== 'BINARY' &&
contract.outcomeType !== 'PSEUDO_NUMERIC')
)
throw new APIError(400, 'Invalid contract')
const { closeTime } = contract
if (closeTime && Date.now() > closeTime)
throw new APIError(400, 'Trading is closed')
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
const newLiquidityProvisionDoc = firestore
.collection(`contracts/${contractId}/liquidity`)
.doc()
const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } =
getNewLiquidityProvision(
user.id,
amount,
contract,
newLiquidityProvisionDoc.id
)
transaction.update(contractDoc, {
subsidyPool: newSubsidyPool,
totalLiquidity: newTotalLiquidity,
} as Partial<CPMMContract>)
const newBalance = user.balance - amount
const newTotalDeposits = user.totalDeposits - amount
if (!isFinite(newBalance)) {
throw new APIError(500, 'Invalid user balance for ' + user.username)
}
transaction.update(userDoc, {
balance: newBalance,
totalDeposits: newTotalDeposits,
})
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
return newLiquidityProvision
})
})
const firestore = admin.firestore()

View File

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

View File

@ -1,17 +1,20 @@
import * as admin from 'firebase-admin'
import { logger } from 'firebase-functions/v2'
import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https'
import { Request, RequestHandler, Response } from 'express'
import { error } from 'firebase-functions/logger'
import { HttpsOptions } from 'firebase-functions/v2/https'
import { log } from './utils'
import { z } from 'zod'
import { APIError } from '../../common/api'
import { PrivateUser } from '../../common/user'
import {
CORS_ORIGIN_MANIFOLD,
CORS_ORIGIN_LOCALHOST,
CORS_ORIGIN_VERCEL,
} from '../../common/envs/constants'
export { APIError } from '../../common/api'
type Output = Record<string, unknown>
type AuthedUser = {
export type AuthedUser = {
uid: string
creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser })
}
@ -20,24 +23,8 @@ type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
type KeyCredentials = { kind: 'key'; data: string }
type Credentials = JwtCredentials | KeyCredentials
export class APIError {
code: number
msg: string
details: unknown
constructor(code: number, msg: string, details?: unknown) {
this.code = code
this.msg = msg
this.details = details
}
}
const auth = admin.auth()
const firestore = admin.firestore()
const privateUsers = firestore.collection(
'private-users'
) as admin.firestore.CollectionReference<PrivateUser>
export const parseCredentials = async (req: Request): Promise<Credentials> => {
const auth = admin.auth()
const authHeader = req.get('Authorization')
if (!authHeader) {
throw new APIError(403, 'Missing Authorization header.')
@ -54,7 +41,7 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
return { kind: 'jwt', data: await auth.verifyIdToken(payload) }
} catch (err) {
// This is somewhat suspicious, so get it into the firebase console
logger.error('Error verifying Firebase JWT: ', err)
error('Error verifying Firebase JWT: ', err)
throw new APIError(403, 'Error validating token.')
}
case 'Key':
@ -65,6 +52,8 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
}
export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
const firestore = admin.firestore()
const privateUsers = firestore.collection('private-users')
switch (creds.kind) {
case 'jwt': {
if (typeof creds.data.user_id !== 'string') {
@ -78,7 +67,7 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
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:
@ -86,12 +75,30 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
}
}
export const writeResponseError = (e: unknown, res: Response) => {
if (e instanceof APIError) {
const output: { [k: string]: unknown } = { message: e.message }
if (e.details != null) {
output.details = e.details
}
res.status(e.code).json(output)
} else {
error(e)
res.status(500).json({ message: 'An unknown error occurred.' })
}
}
export const zTimestamp = () => {
return z.preprocess((arg) => {
return typeof arg == 'number' ? new Date(arg) : undefined
}, z.date())
}
export type EndpointDefinition = {
opts: EndpointOptions & { method: string }
handler: RequestHandler
}
export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
const result = schema.safeParse(val)
if (!result.success) {
@ -108,42 +115,55 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
}
}
interface EndpointOptions extends HttpsOptions {
methods?: string[]
export interface EndpointOptions extends HttpsOptions {
method?: string
}
const DEFAULT_OPTS = {
methods: ['POST'],
method: 'POST',
minInstances: 1,
concurrency: 100,
memory: '2GiB',
cpu: 1,
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
cors: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_VERCEL, CORS_ORIGIN_LOCALHOST],
}
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
const opts = Object.assign(endpointOpts, DEFAULT_OPTS)
return onRequest(opts, async (req, res) => {
log('Request processing started.')
try {
if (!opts.methods.includes(req.method)) {
const allowed = opts.methods.join(', ')
throw new APIError(405, `This endpoint supports only ${allowed}.`)
}
const authedUser = await lookupUser(await parseCredentials(req))
log('User credentials processed.')
res.status(200).json(await fn(req, authedUser))
} catch (e) {
if (e instanceof APIError) {
const output: { [k: string]: unknown } = { message: e.msg }
if (e.details != null) {
output.details = e.details
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(e.code).json(output)
} else {
logger.error(e)
res.status(500).json({ message: 'An unknown error occurred.' })
const authedUser = await lookupUser(await parseCredentials(req))
res.status(200).json(await fn(req, authedUser))
} catch (e) {
writeResponseError(e, res)
}
}
})
},
} 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
}

View File

@ -0,0 +1,33 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { APIError, newEndpoint, validate } from './api'
import { LimitBet } from '../../common/bet'
const bodySchema = z.object({
betId: z.string(),
})
export const cancelbet = newEndpoint({}, async (req, auth) => {
const { betId } = validate(bodySchema, req.body)
return await firestore.runTransaction(async (trans) => {
const snap = await trans.get(
firestore.collectionGroup('bets').where('id', '==', betId)
)
const betDoc = snap.docs[0]
if (!betDoc?.exists) throw new APIError(400, 'Bet not found.')
const bet = betDoc.data() as LimitBet
if (bet.userId !== auth.uid)
throw new APIError(400, 'Not authorized to cancel bet.')
if (bet.limitProb === undefined)
throw new APIError(400, 'Not a limit order: Cannot cancel.')
if (bet.isCancelled) throw new APIError(400, 'Bet already cancelled.')
trans.update(betDoc.ref, { isCancelled: true })
return { ...bet, isCancelled: true }
})
})
const firestore = admin.firestore()

View File

@ -1,7 +1,8 @@
import * as functions from 'firebase-functions'
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'
@ -11,37 +12,23 @@ import {
} from '../../common/util/clean-username'
import { removeUndefinedProps } from '../../common/util/object'
import { Answer } from '../../common/answer'
import { APIError, newEndpoint, validate } from './api'
export const changeUserInfo = functions
.runWith({ minInstances: 1 })
.https.onCall(
async (
data: {
username?: string
name?: string
avatarUrl?: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const bodySchema = z.object({
username: z.string().optional(),
name: z.string().optional(),
avatarUrl: z.string().optional(),
})
const user = await getUser(userId)
if (!user) return { status: 'error', message: 'User not found' }
export const changeuserinfo = newEndpoint({}, async (req, auth) => {
const { username, name, avatarUrl } = validate(bodySchema, req.body)
const { username, name, avatarUrl } = data
const user = await getUser(auth.uid)
if (!user) throw new APIError(400, 'User not found')
return await changeUser(user, { username, name, avatarUrl })
.then(() => {
console.log('succesfully changed', user.username, 'to', data)
return { status: 'success' }
})
.catch((e) => {
console.log('Error', e.message)
return { status: 'error', message: e.message }
})
}
)
await changeUser(user, { username, name, avatarUrl })
return { message: 'Successfully changed user info.' }
})
export const changeUser = async (
user: User,
@ -51,18 +38,68 @@ 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)
if (!update.username) {
throw new Error('Invalid username')
throw new APIError(400, 'Invalid username')
}
const sameNameUser = await transaction.get(
firestore.collection('users').where('username', '==', update.username)
)
if (!sameNameUser.empty) {
throw new Error('Username already exists')
throw new APIError(400, 'Username already exists')
}
}
@ -72,49 +109,7 @@ 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)
await transaction.update(userRef, userUpdate)
await Promise.all(
commentSnap.docs.map((d) => transaction.update(d.ref, commentUpdate))
)
await Promise.all(
answerSnap.docs.map((d) => transaction.update(d.ref, answerUpdate))
)
await contracts.docs.map((d) => transaction.update(d.ref, contractUpdate))
transaction.update(userRef, userUpdate)
})
}

View File

@ -1,102 +1,107 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { User } from 'common/user'
import { Manalink } from 'common/manalink'
import { runTxn, TxnData } from './transact'
import { APIError, newEndpoint, validate } from './api'
export const claimManalink = functions
.runWith({ minInstances: 1 })
.https.onCall(async (slug: string, context) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const bodySchema = z.object({
slug: z.string(),
})
// Run as transaction to prevent race conditions.
return await firestore.runTransaction(async (transaction) => {
// Look up the manalink
const manalinkDoc = firestore.doc(`manalinks/${slug}`)
const manalinkSnap = await transaction.get(manalinkDoc)
if (!manalinkSnap.exists) {
return { status: 'error', message: 'Manalink not found' }
}
const manalink = manalinkSnap.data() as Manalink
export const claimmanalink = newEndpoint({}, async (req, auth) => {
const { slug } = validate(bodySchema, req.body)
const { amount, fromId, claimedUserIds } = manalink
// Run as transaction to prevent race conditions.
return await firestore.runTransaction(async (transaction) => {
// Look up the manalink
const manalinkDoc = firestore.doc(`manalinks/${slug}`)
const manalinkSnap = await transaction.get(manalinkDoc)
if (!manalinkSnap.exists) {
throw new APIError(400, 'Manalink not found')
}
const manalink = manalinkSnap.data() as Manalink
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' }
const { amount, fromId, claimedUserIds } = manalink
const fromDoc = firestore.doc(`users/${fromId}`)
const fromSnap = await transaction.get(fromDoc)
if (!fromSnap.exists) {
return { status: 'error', message: `User ${fromId} not found` }
}
const fromUser = fromSnap.data() as User
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
throw new APIError(500, 'Invalid amount')
// Only permit one redemption per user per link
if (claimedUserIds.includes(userId)) {
return {
status: 'error',
message: `${fromUser.name} already redeemed manalink ${slug}`,
}
}
if (auth.uid === fromId)
throw new APIError(400, `You can't claim your own manalink`)
// Disallow expired or maxed out links
if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) {
return {
status: 'error',
message: `Manalink ${slug} expired on ${new Date(
manalink.expiresTime
).toLocaleString()}`,
}
}
if (
manalink.maxUses != null &&
manalink.maxUses <= manalink.claims.length
) {
return {
status: 'error',
message: `Manalink ${slug} has reached its max uses of ${manalink.maxUses}`,
}
}
const fromDoc = firestore.doc(`users/${fromId}`)
const fromSnap = await transaction.get(fromDoc)
if (!fromSnap.exists) {
throw new APIError(500, `User ${fromId} not found`)
}
const fromUser = fromSnap.data() as User
if (fromUser.balance < amount) {
return {
status: 'error',
message: `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} `,
}
}
// Only permit one redemption per user per link
if (claimedUserIds.includes(auth.uid)) {
throw new APIError(400, `You already redeemed manalink ${slug}`)
}
// Actually execute the txn
const data: TxnData = {
fromId,
fromType: 'USER',
toId: userId,
toType: 'USER',
amount,
token: 'M$',
category: 'MANALINK',
description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${userId}`,
}
const result = await runTxn(transaction, data)
const txnId = result.txn?.id
if (!txnId) {
return { status: 'error', message: result.message }
}
// Disallow expired or maxed out links
if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) {
throw new APIError(
400,
`Manalink ${slug} expired on ${new Date(
manalink.expiresTime
).toLocaleString()}`
)
}
if (
manalink.maxUses != null &&
manalink.maxUses <= manalink.claims.length
) {
throw new APIError(
400,
`Manalink ${slug} has reached its max uses of ${manalink.maxUses}`
)
}
// Update the manalink object with this info
const claim = {
toId: userId,
txnId,
claimedTime: Date.now(),
}
transaction.update(manalinkDoc, {
claimedUserIds: [...claimedUserIds, userId],
claims: [...manalink.claims, claim],
})
if (fromUser.balance < amount) {
throw new APIError(
400,
`Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} `
)
}
return { status: 'success', message: 'Manalink claimed' }
// Actually execute the txn
const data: TxnData = {
fromId,
fromType: 'USER',
toId: auth.uid,
toType: 'USER',
amount,
token: 'M$',
category: 'MANALINK',
description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${auth.uid}`,
}
const result = await runTxn(transaction, data)
const txnId = result.txn?.id
if (!txnId) {
throw new APIError(
500,
result.message ?? 'An error occurred posting the transaction.'
)
}
// Update the manalink object with this info
const claim = {
toId: auth.uid,
txnId,
claimedTime: Date.now(),
}
transaction.update(manalinkDoc, {
claimedUserIds: [...claimedUserIds, auth.uid],
claims: [...manalink.claims, claim],
})
return { message: 'Manalink claimed' }
})
})
const firestore = admin.firestore()

View File

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

View File

@ -1,128 +1,105 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { z } from 'zod'
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 { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails'
import { getValues } from './utils'
import { APIError, newEndpoint, validate } from './api'
import { addUserToContractFollowers } from './follow-market'
export const createAnswer = functions
.runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] })
.https.onCall(
async (
data: {
contractId: string
amount: number
text: string
},
context
) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const bodySchema = z.object({
contractId: z.string().max(MAX_ANSWER_LENGTH),
amount: z.number().gt(0),
text: z.string(),
})
const { contractId, amount, text } = data
const opts = { secrets: ['MAILGUN_KEY'] }
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' }
export const createanswer = newEndpoint(opts, async (req, auth) => {
const { contractId, amount, text } = validate(bodySchema, req.body)
if (!text || typeof text !== 'string' || text.length > MAX_ANSWER_LENGTH)
return { status: 'error', message: 'Invalid text' }
if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
// Run as transaction to prevent race conditions.
const result = await firestore.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${userId}`)
const userSnap = await transaction.get(userDoc)
if (!userSnap.exists)
return { status: 'error', message: 'User not found' }
const user = userSnap.data() as User
// Run as transaction to prevent race conditions.
const answer = await firestore.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${auth.uid}`)
const userSnap = await transaction.get(userDoc)
if (!userSnap.exists) throw new APIError(400, 'User not found')
const user = userSnap.data() as User
if (user.balance < amount)
return { status: 'error', message: 'Insufficient balance' }
if (user.balance < amount) throw new APIError(400, 'Insufficient balance')
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists)
return { status: 'error', message: 'Invalid contract' }
const contract = contractSnap.data() as Contract
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await transaction.get(contractDoc)
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
const contract = contractSnap.data() as Contract
if (contract.outcomeType !== 'FREE_RESPONSE')
return {
status: 'error',
message: 'Requires a free response contract',
}
if (contract.outcomeType !== 'FREE_RESPONSE')
throw new APIError(400, 'Requires a free response contract')
const { closeTime, volume } = contract
if (closeTime && Date.now() > closeTime)
return { status: 'error', message: 'Trading is closed' }
const { closeTime, volume } = contract
if (closeTime && Date.now() > closeTime)
throw new APIError(400, 'Trading is closed')
const [lastAnswer] = await getValues<Answer>(
firestore
.collection(`contracts/${contractId}/answers`)
.orderBy('number', 'desc')
.limit(1)
)
const [lastAnswer] = await getValues<Answer>(
firestore
.collection(`contracts/${contractId}/answers`)
.orderBy('number', 'desc')
.limit(1)
)
if (!lastAnswer)
return { status: 'error', message: 'Could not fetch last answer' }
if (!lastAnswer) throw new APIError(500, 'Could not fetch last answer')
const number = lastAnswer.number + 1
const id = `${number}`
const number = lastAnswer.number + 1
const id = `${number}`
const newAnswerDoc = firestore
.collection(`contracts/${contractId}/answers`)
.doc(id)
const newAnswerDoc = firestore
.collection(`contracts/${contractId}/answers`)
.doc(id)
const answerId = newAnswerDoc.id
const { username, name, avatarUrl } = user
const answerId = newAnswerDoc.id
const { username, name, avatarUrl } = user
const answer: Answer = {
id,
number,
contractId,
createdTime: Date.now(),
userId: user.id,
username,
name,
avatarUrl,
text,
}
transaction.create(newAnswerDoc, answer)
const loanAmount = 0
const { newBet, newPool, newTotalShares, newTotalBets } =
getNewMultiBetInfo(answerId, amount, contract, loanAmount)
const newBalance = user.balance - amount
const betDoc = firestore
.collection(`contracts/${contractId}/bets`)
.doc()
transaction.create(betDoc, {
id: betDoc.id,
userId: user.id,
...newBet,
})
transaction.update(userDoc, { balance: newBalance })
transaction.update(contractDoc, {
pool: newPool,
totalShares: newTotalShares,
totalBets: newTotalBets,
answers: [...(contract.answers ?? []), answer],
volume: volume + amount,
})
return { status: 'success', answerId, betId: betDoc.id, answer }
})
const { answer } = result
const contract = await getContract(contractId)
if (answer && contract) await sendNewAnswerEmail(answer, contract)
return result
const answer: Answer = {
id,
number,
contractId,
createdTime: Date.now(),
userId: user.id,
username,
name,
avatarUrl,
text,
}
)
transaction.create(newAnswerDoc, answer)
const { newBet, newPool, newTotalShares, newTotalBets } =
getNewMultiBetInfo(answerId, amount, contract)
const newBalance = user.balance - amount
const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
transaction.create(betDoc, {
id: betDoc.id,
userId: user.id,
...newBet,
})
transaction.update(userDoc, { balance: newBalance })
transaction.update(contractDoc, {
pool: newPool,
totalShares: newTotalShares,
totalBets: newTotalBets,
answers: [...(contract.answers ?? []), answer],
volume: volume + amount,
})
return answer
})
await addUserToContractFollowers(contractId, auth.uid)
return answer
})
const firestore = admin.firestore()

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import {
MAX_GROUP_NAME_LENGTH,
MAX_ID_LENGTH,
} from '../../common/group'
import { APIError, newEndpoint, validate } from '../../functions/src/api'
import { APIError, newEndpoint, validate } from './api'
import { z } from 'zod'
const bodySchema = z.object({
@ -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
@ -57,17 +58,29 @@ 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,
memberIds,
totalContracts: 0,
totalMembers: memberIds.length,
postIds: [],
pinnedItems: [],
}
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 }
})
const getSlug = async (name: string) => {
export const getSlug = async (name: string) => {
const proposedSlug = slugify(name)
const preexistingGroup = await getGroupFromSlug(proposedSlug)
@ -75,9 +88,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)

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,100 +1,97 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { z } from 'zod'
import {
PrivateUser,
STARTING_BALANCE,
SUS_STARTING_BALANCE,
User,
} from '../../common/user'
import { getUser, getUserByUsername } from './utils'
import { PrivateUser, User } from '../../common/user'
import { getUser, getUserByUsername, getValues } from './utils'
import { randomString } from '../../common/util/random'
import {
cleanDisplayName,
cleanUsername,
} from '../../common/util/clean-username'
import { sendWelcomeEmail } from './emails'
import { isWhitelisted } from '../../common/envs/constants'
import { DEFAULT_CATEGORIES } from '../../common/categories'
import {
CATEGORIES_GROUP_SLUG_POSTFIX,
DEFAULT_CATEGORIES,
} from '../../common/categories'
import { track } from './analytics'
import { APIError, newEndpoint, validate } from './api'
import { Group } from '../../common/group'
import { SUS_STARTING_BALANCE, STARTING_BALANCE } from '../../common/economy'
import { getDefaultNotificationPreferences } from '../../common/user-notification-preferences'
export const createUser = functions
.runWith({ minInstances: 1, secrets: ['MAILGUN_KEY'] })
.https.onCall(async (data: { deviceToken?: string }, context) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const bodySchema = z.object({
deviceToken: z.string().optional(),
})
const preexistingUser = await getUser(userId)
if (preexistingUser)
return {
status: 'error',
message: 'User already created',
user: preexistingUser,
}
const opts = { secrets: ['MAILGUN_KEY'] }
const fbUser = await admin.auth().getUser(userId)
export const createuser = newEndpoint(opts, async (req, auth) => {
const { deviceToken } = validate(bodySchema, req.body)
const preexistingUser = await getUser(auth.uid)
if (preexistingUser)
throw new APIError(400, 'User already exists', { user: preexistingUser })
const email = fbUser.email
if (!isWhitelisted(email)) {
return { status: 'error', message: `${email} is not whitelisted` }
}
const emailName = email?.replace(/@.*$/, '')
const fbUser = await admin.auth().getUser(auth.uid)
const rawName = fbUser.displayName || emailName || 'User' + randomString(4)
const name = cleanDisplayName(rawName)
let username = cleanUsername(name)
const email = fbUser.email
if (!isWhitelisted(email)) {
throw new APIError(400, `${email} is not whitelisted`)
}
const emailName = email?.replace(/@.*$/, '')
const sameNameUser = await getUserByUsername(username)
if (sameNameUser) {
username += randomString(4)
}
const rawName = fbUser.displayName || emailName || 'User' + randomString(4)
const name = cleanDisplayName(rawName)
let username = cleanUsername(name)
const avatarUrl = fbUser.photoURL
const sameNameUser = await getUserByUsername(username)
if (sameNameUser) {
username += randomString(4)
}
const { deviceToken } = data
const deviceUsedBefore =
!deviceToken || (await isPrivateUserWithDeviceToken(deviceToken))
const avatarUrl = fbUser.photoURL
const deviceUsedBefore =
!deviceToken || (await isPrivateUserWithDeviceToken(deviceToken))
const ipAddress = context.rawRequest.ip
const ipCount = ipAddress ? await numberUsersWithIp(ipAddress) : 0
const balance = deviceUsedBefore ? SUS_STARTING_BALANCE : STARTING_BALANCE
const balance =
deviceUsedBefore || ipCount > 2 ? SUS_STARTING_BALANCE : STARTING_BALANCE
const user: User = {
id: auth.uid,
name,
username,
avatarUrl,
balance,
totalDeposits: balance,
createdTime: Date.now(),
profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
nextLoanCached: 0,
followerCountCached: 0,
followedCategories: DEFAULT_CATEGORIES,
shouldShowWelcome: true,
fractionResolvedCorrectly: 1,
achievements: {},
}
const user: User = {
id: userId,
name,
username,
avatarUrl,
balance,
totalDeposits: balance,
createdTime: Date.now(),
profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
followerCountCached: 0,
followedCategories: DEFAULT_CATEGORIES,
}
await firestore.collection('users').doc(auth.uid).create(user)
console.log('created user', username, 'firebase id:', auth.uid)
await firestore.collection('users').doc(userId).create(user)
console.log('created user', username, 'firebase id:', userId)
const privateUser: PrivateUser = {
id: auth.uid,
username,
email,
initialIpAddress: req.ip,
initialDeviceToken: deviceToken,
notificationPreferences: getDefaultNotificationPreferences(auth.uid),
}
const privateUser: PrivateUser = {
id: userId,
username,
email,
initialIpAddress: ipAddress,
initialDeviceToken: deviceToken,
}
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
await addUserToDefaultGroups(user)
await firestore.collection('private-users').doc(userId).create(privateUser)
await track(auth.uid, 'create user', { username }, { ip: req.ip })
await sendWelcomeEmail(user, privateUser)
await track(userId, 'create user', { username }, { ip: ipAddress })
return { status: 'success', user }
})
return { user, privateUser }
})
const firestore = admin.firestore()
@ -107,7 +104,7 @@ const isPrivateUserWithDeviceToken = async (deviceToken: string) => {
return !snap.empty
}
const numberUsersWithIp = async (ipAddress: string) => {
export const numberUsersWithIp = async (ipAddress: string) => {
const snap = await firestore
.collection('private-users')
.where('initialIpAddress', '==', ipAddress)
@ -115,3 +112,16 @@ const numberUsersWithIp = async (ipAddress: string) => {
return snap.docs.length
}
const addUserToDefaultGroups = async (user: User) => {
for (const category of Object.values(DEFAULT_CATEGORIES)) {
const slug = category.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX
const groups = await getValues<Group>(
firestore.collection('groups').where('slug', '==', slug)
)
await firestore
.collection(`groups/${groups[0].id}/groupMembers`)
.doc(user.id)
.set({ userId: user.id, createdTime: Date.now() })
}
}

View File

@ -0,0 +1,69 @@
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()
})
}

View File

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

View File

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

View File

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

View File

@ -1,84 +1,91 @@
<!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>
">
<style type="text/css">
img {
max-width: 100%;
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market answer</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
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;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
.content {
padding: 0 !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;
}
.content-wrap {
padding: 10px !important;
}
</style>
</head>
<body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
.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;
@ -89,43 +96,29 @@
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;
@ -134,12 +127,8 @@
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;
@ -147,14 +136,8 @@
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;
@ -162,20 +145,14 @@
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;
@ -183,35 +160,23 @@
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;
@ -220,29 +185,21 @@
margin: 0;
padding: 0 0 0px 0;
text-align: left;
"
valign="top"
>
<img
src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td>
</tr>
<tr
style="
" 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: 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;
@ -251,13 +208,8 @@
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;
@ -266,19 +218,15 @@
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;
@ -286,37 +234,26 @@
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;
@ -324,40 +261,29 @@
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;
@ -375,38 +301,29 @@
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;
@ -415,28 +332,20 @@
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;
@ -446,14 +355,9 @@
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;
@ -461,39 +365,26 @@
color: #999;
text-decoration: underline;
margin: 0;
"
>our Discord</a
>! Or,
<a
href="{{unsubscribeUrl}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
">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>
</td>
<td
style="
</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>
" valign="top"></td>
</tr>
</table>
</body>
</html>

View File

@ -1,84 +1,91 @@
<!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>
">
<style type="text/css">
img {
max-width: 100%;
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market closed</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
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;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
.content {
padding: 0 !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;
}
.content-wrap {
padding: 10px !important;
}
</style>
</head>
<body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
.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;
@ -89,43 +96,29 @@
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;
@ -134,12 +127,8 @@
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;
@ -147,14 +136,8 @@
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;
@ -162,20 +145,14 @@
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;
@ -183,35 +160,23 @@
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;
@ -220,30 +185,22 @@
margin: 0;
padding: 0 0 40px 0;
text-align: left;
"
valign="top"
>
<img
src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td>
</tr>
<tr
style="
" 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="
">
<td class="content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
@ -252,24 +209,18 @@
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;
@ -277,12 +228,8 @@
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;
@ -295,24 +242,18 @@
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;
@ -320,12 +261,8 @@
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;
@ -335,25 +272,19 @@
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;
@ -362,13 +293,8 @@
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;
@ -376,19 +302,15 @@
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;
@ -396,116 +318,90 @@
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;
"
/>
Resolve your market to earn {{creatorFee}} as the
creator commission.
<br
style="
" />
Please resolve your market.
<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;
@ -523,38 +419,29 @@
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;
@ -563,28 +450,20 @@
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;
@ -594,14 +473,9 @@
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;
@ -609,39 +483,22 @@
color: #999;
text-decoration: underline;
margin: 0;
"
>our Discord</a
>! Or,
<a
href="https://us-central1-mantic-markets.cloudfunctions.net/unsubscribe?id={{userId}}&type=market-resolve"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
">our Discord</a>!
</td>
</tr>
</table>
</div>
</td>
<td
style="
</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>
" valign="top"></td>
</tr>
</table>
</body>
</html>

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