Compare commits
1106 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c762869b83 | ||
|
aaa09f49c0 | ||
|
4d214c01b4 | ||
|
0a70652667 | ||
|
b4162a0896 | ||
|
2ecece02c3 | ||
|
4359ad0530 | ||
|
3f8988bf27 | ||
|
41a46aad9b | ||
|
4097082c75 | ||
|
58cd0e57bd | ||
|
3a63503161 | ||
|
abd06f272b | ||
|
ab1c3020da | ||
|
c044460a91 | ||
|
7a6725ee77 | ||
|
823d1ddd4c | ||
|
7d490e0de1 | ||
|
96d2255cb1 | ||
|
4a139c5cc2 | ||
|
47eb8abed0 | ||
|
903fcc83b3 | ||
|
0615bb2d4b | ||
|
3508c94634 | ||
|
29c0dfe3fe | ||
|
18a3b66164 | ||
|
9e4f41253f | ||
|
546b0231e7 | ||
|
8bb44593f3 | ||
|
2c2bc61788 | ||
|
34c9dbb3e7 | ||
|
d6525bae9f | ||
|
da32a756a8 | ||
|
fa476c78dd | ||
|
e7ba7e715f | ||
|
9bf82c6082 | ||
|
3e1876f0dc | ||
|
5ba4a9dce7 | ||
|
4e5b78f4ee | ||
|
bc6fab399e | ||
|
c2d112e516 | ||
|
3cbe8ad8bb | ||
|
6226291e02 | ||
|
fa4dba4da3 | ||
|
9eff69be75 | ||
|
789bec2a4f | ||
|
18042cd4d1 | ||
|
04a126707b | ||
|
7a412fdb0d | ||
|
e2dc4c6b8f | ||
|
204d302d87 | ||
|
ae39c1175b | ||
|
c44f223064 | ||
|
aa717a767d | ||
|
d9f57b7daa | ||
|
93ceaa52c4 | ||
|
de76557326 | ||
|
da1fcb646f | ||
|
1d618ba337 | ||
|
2cda3a4d4f | ||
|
e44fc8ae13 | ||
|
e6a90e18e4 | ||
|
cee8caa3e8 | ||
|
b49264ddfa | ||
|
12ed569ff6 | ||
|
00acc262a0 | ||
|
fd7d4eb5e2 | ||
|
8ae1166c49 | ||
|
84e2b63c49 | ||
|
f19ef83ac2 | ||
|
0c11f3b450 | ||
|
de9ffa2b52 | ||
|
decb3213f6 | ||
|
ff6278b147 | ||
|
3fc53112b9 | ||
|
59cdc9f776 | ||
|
f587e0256d | ||
|
1c209f68f6 | ||
|
b4e7d88ed8 | ||
|
b2cd6bbe03 | ||
|
a6d5d5ad15 | ||
|
beeca57d4e | ||
|
fb8bd1acfb | ||
|
4215821f35 | ||
|
a71c3d6a4a | ||
|
cdcce421a8 | ||
|
8beff6eb1f | ||
|
f714918b88 | ||
|
946d74489f | ||
|
220d0841bd | ||
|
9d44190b9a | ||
|
3cdd790ae9 | ||
|
6c1ac89cbe | ||
|
0d8a84ef06 | ||
|
d528566ffa | ||
|
b0f8369d9c | ||
|
721c18cf6c | ||
|
43b06ae6fa | ||
|
bfdb5ae595 | ||
|
274f7fa849 | ||
|
d507c4092e | ||
|
e970a908c6 | ||
|
4fd0e5caad | ||
|
70b2b14f80 | ||
|
0ec15ff2f8 | ||
|
8bb9885aee | ||
|
c46c384d1d | ||
|
4f5c93be96 | ||
|
f03e5d7af0 | ||
|
fb0a09664e | ||
|
17d0fb7da6 | ||
|
867cdf2496 | ||
|
f26ba1c4a2 | ||
|
cdc64c6475 | ||
|
5d561acdf8 | ||
|
84f79ffe7c | ||
|
f6fd703005 | ||
|
b8ef272784 | ||
|
a4699b79ed | ||
|
66071e16fa | ||
|
b3136ebcac | ||
|
a143a96919 | ||
|
dea65a4ba0 | ||
|
a310963952 | ||
|
8d06e4b4d2 | ||
|
dc51e2cf46 | ||
|
4831c25ce0 | ||
|
60f2552139 | ||
|
4b8d381da5 | ||
|
565177b76f | ||
|
8bd21c6693 | ||
|
310a41d63e | ||
|
e1636d0f13 | ||
|
d00ea65279 | ||
|
60bb5379cb | ||
|
f3dedfb27a | ||
|
efa2e44937 | ||
|
84bc490ed3 | ||
|
443397b7dc | ||
|
b57ff68654 | ||
|
f0b35993c9 | ||
|
8f56ccad22 | ||
|
9e289146af | ||
|
4285198f09 | ||
|
f533d9bfcb | ||
|
71b0c71729 | ||
|
25333317b0 | ||
|
42a7d04b4d | ||
|
b1d386ca5a | ||
|
0dc8753a92 | ||
|
454f2d1417 | ||
|
d846b9fb30 | ||
|
77e0631ea4 | ||
|
badd67c278 | ||
|
80622dc7ee | ||
|
9d12fa3af0 | ||
|
d9c8925ea0 | ||
|
adb809f973 | ||
|
a63405ca7c | ||
|
7ca0fb72fc | ||
|
ac37f94cf7 | ||
|
bc5af50b0c | ||
|
4162cca3ff | ||
|
91da39370f | ||
|
2f2c586d5d | ||
|
853e3e4896 | ||
|
edbd0feb37 | ||
|
59de979949 | ||
|
b8d65acc3f | ||
|
26f04fb04a | ||
|
e127f9646a | ||
|
25ef17498a | ||
|
68075db3da | ||
|
e1f24f24a9 | ||
|
cd8245fbee | ||
|
f1e400765a | ||
|
94624c5387 | ||
|
7ce09ae39d | ||
|
935bdd12a7 | ||
|
5d7721e041 | ||
|
a149777c0e | ||
|
81fb2456bd | ||
|
f8ec306ee9 | ||
|
a53fb49ec3 | ||
|
7863a4232d | ||
|
a3b841423f | ||
|
b8911cafe8 | ||
|
60aa294131 | ||
|
0818a94307 | ||
|
a3acd3fa3c | ||
|
1ef1af8234 | ||
|
189da4a0cf | ||
|
10f0bbc63d | ||
|
2d56525d65 | ||
|
f1f8082600 | ||
|
ec006f25c4 | ||
|
b40a114168 | ||
|
4bbadeb27c | ||
|
2596d54831 | ||
|
0df5497ffb | ||
|
27dabc193c | ||
|
6ec1b38a21 | ||
|
f35eb42d7b | ||
|
18f8ad433d | ||
|
37e8f2ff5a | ||
|
328aa1457d | ||
|
b9ba3e75fa | ||
|
70bfec2742 | ||
|
26281556f7 | ||
|
730abf584a | ||
|
34d09316e0 | ||
|
6f41ab8efd | ||
|
f1207e87ec | ||
|
4e22b8e332 | ||
|
07de8cc86a | ||
|
f07a022d63 | ||
|
d42ec42b0e | ||
|
6fa4e17a58 | ||
|
af3a3a3934 | ||
|
9e3477970d | ||
|
3390c34d0a | ||
|
419219c703 | ||
|
8aaca848b2 | ||
|
e4d7d0a232 | ||
|
e9050973e1 | ||
|
83d9a1f3e2 | ||
|
49e97ddac1 | ||
|
a9d5dd7fc8 | ||
|
ddb186dd98 | ||
|
d2273087cf | ||
|
6a0b577aeb | ||
|
ca6197c7bb | ||
|
ed6ea011c2 | ||
|
83d33792aa | ||
|
583c5b225e | ||
|
9f256aa7a8 | ||
|
7a271fce29 | ||
|
d8ef363f06 | ||
|
8043fa515a | ||
|
f551e6c469 | ||
|
3f0b665753 | ||
|
40b07329bd | ||
|
7b9aeea0bd | ||
|
935ff7b97a | ||
|
c115b5cca7 | ||
|
d6bb27f97c | ||
|
bbce3e873e | ||
|
26f5e506b7 | ||
|
5adaa7253f | ||
|
a55d85d4b6 | ||
|
f085df96e3 | ||
|
1d2af2900b | ||
|
a48cec63fc | ||
|
e6374c4994 | ||
|
6ac467764d | ||
|
79af4b2be0 | ||
|
094bcaea17 | ||
|
c6e5e04e65 | ||
|
ee4d3947b8 | ||
|
45b281fac5 | ||
|
31c6cb7739 | ||
|
23ca3ff56a | ||
|
c3ffac34a1 | ||
|
375a4e089f | ||
|
efd83eaad4 | ||
|
8d70dc4800 | ||
|
a1dcf8d168 | ||
|
84aaeece9f | ||
|
27d765a4a1 | ||
|
5214f27be3 | ||
|
d0d223f7ad | ||
|
0c9226de41 | ||
|
ce48016f80 | ||
|
1515d8cab2 | ||
|
28cad9caf8 | ||
|
9a950dc080 | ||
|
42cc07e4a6 | ||
|
a5490c903f | ||
|
71975f307c | ||
|
ae4136348d | ||
|
67de983aac | ||
|
59b128dbe7 | ||
|
074a1fdde2 | ||
|
7c34805eeb | ||
|
77a5f8b9dd | ||
|
5ae9049295 | ||
|
d5d1284306 | ||
|
adb8bc476f | ||
|
f92f098f82 | ||
|
370edec890 | ||
|
f5a3abf0bc | ||
|
27e6534d94 | ||
|
1caf75d3b5 | ||
|
051c2905e1 | ||
|
1f7b9174b3 | ||
|
06571a3657 | ||
|
3fb43c16c4 | ||
|
603201a00f | ||
|
b517817ee3 | ||
|
80693620f0 | ||
|
f1ae54355d | ||
|
503038d2a2 | ||
|
bf8dca25b2 | ||
|
a82f447965 | ||
|
1f8c72b4c9 | ||
|
40c51c3d59 | ||
|
86ceea831b | ||
|
efb9ef7602 | ||
|
8c1131ebab | ||
|
2c223160ed | ||
|
11bd658c68 | ||
|
39638a3888 | ||
|
234820ecd4 | ||
|
4d996c2476 | ||
|
9ecf10496c | ||
|
42b27fcedd | ||
|
7bf59bcdd0 | ||
|
043b18da0e | ||
|
64951e691e | ||
|
9a90cc3835 | ||
|
10e361bcac | ||
|
a7f6cb7cfa | ||
|
359a768e14 | ||
|
42aea03415 | ||
|
0fb263efa4 | ||
|
747977556b | ||
|
37e8cfbbed | ||
|
701d0a06cd | ||
|
0ffd6c129a | ||
|
758dbfe398 | ||
|
33dfce3e16 | ||
|
af66d94c84 | ||
|
290a34bc64 | ||
|
4c2f9011d0 | ||
|
57b592b5aa | ||
|
fd31b7eaca | ||
|
1d645e5ff8 | ||
|
0b0b84a6ad | ||
|
2baae33a77 | ||
|
fac87f8e0c | ||
|
670c6faea8 | ||
|
09e4864b32 | ||
|
a445d9b7fa | ||
|
cb613705e9 | ||
|
aeeb47bdbe | ||
|
0844e5620a | ||
|
2d6fe308b8 | ||
|
759685258a | ||
|
b53e4acea6 | ||
|
2f1221f094 | ||
|
2f3ae5192e | ||
|
b0b1d72ba6 | ||
|
dc0b6dc6a6 | ||
|
89e26d077e | ||
|
38b7c898f6 | ||
|
1fc2f15dae | ||
|
3d146dd57d | ||
|
a219680701 | ||
|
1e2df99054 | ||
|
37beb584ef | ||
|
9815e7301f | ||
|
ac97e62f2e | ||
|
17d1b8575c | ||
|
a25acbe1db | ||
|
b2f81c1149 | ||
|
9d81e3b6d1 | ||
|
ab883ea777 | ||
|
3677de58c3 | ||
|
31de3636fd | ||
|
a90b765670 | ||
|
55f854115c | ||
|
138f34fc66 | ||
|
c16e5189f7 | ||
|
1bc1debbe8 | ||
|
608ee7b865 | ||
|
95c47aba1a | ||
|
f892c92e26 | ||
|
7e91133229 | ||
|
523689b525 | ||
|
b83e5db563 | ||
|
13b3613460 | ||
|
715bae57e0 | ||
|
5b5a919ed7 | ||
|
2625ab1549 | ||
|
262183e0e6 | ||
|
b7df1a7043 | ||
|
8929b2e6ba | ||
|
9fc1e855ff | ||
|
1755fb15d4 | ||
|
1e6b72059e | ||
|
2d1fd07834 | ||
|
ec1a9fab77 | ||
|
2cc08ba9e7 | ||
|
35aa6c0429 | ||
|
cd7ddae133 | ||
|
46fab105d9 | ||
|
4cc985634a | ||
|
15cd8b1f94 | ||
|
8862425120 | ||
|
be010da9f5 | ||
|
7f7e7acd61 | ||
|
1f2c7271b7 | ||
|
83de206e9e | ||
|
d55cedb36c | ||
|
eb762d9b9e | ||
|
dba938032f | ||
|
7c8e977d60 | ||
|
e0e6838711 | ||
|
513cf7b290 | ||
|
89c3ea559c | ||
|
9238b20242 | ||
|
925a9e850f | ||
|
8f88af4e2a | ||
|
5b54e7d468 | ||
|
f52127237e | ||
|
95f2604479 | ||
|
a5b943965c | ||
|
c16adb9ec9 | ||
|
e0d9b4d335 | ||
|
9dc0d1696e | ||
|
a7abdbb1db | ||
|
13dad9a10c | ||
|
14c008234a | ||
|
b87e29d7c0 | ||
|
3ed29877ce | ||
|
80d4bffc95 | ||
|
b21daa1248 | ||
|
419c7ab636 | ||
|
e2047210b7 | ||
|
5e34b5a911 | ||
|
723d9dbece | ||
|
7ba19c274b | ||
|
a12ed78813 | ||
|
aa93ec060d | ||
|
fd90bc353b | ||
|
e17a59ae23 | ||
|
2fe9fe593d | ||
|
d612192109 | ||
|
13cffcdaf1 | ||
|
1b9811ce28 | ||
|
3ed3b6fb42 | ||
|
f7bf42d2e0 | ||
|
df316fc4da | ||
|
2ef025a151 | ||
|
90eaf83775 | ||
|
94ffac287e | ||
|
a10e4c115e | ||
|
cc3b44891b | ||
|
d9292f7a95 | ||
|
bf92c4fb06 | ||
|
68120ec2b2 | ||
|
be2c60d3f3 | ||
|
c1c3a360fd | ||
|
ae4d49d960 | ||
|
21c7130d3b | ||
|
d990bc2f07 | ||
|
e2a8df6c3a | ||
|
96dc060a0a | ||
|
d04304bdac | ||
|
2891a47d8c | ||
|
490734db00 | ||
|
77ddc456a2 | ||
|
1a5dcdedcc | ||
|
0ab82a7bd4 | ||
|
deb8397ee9 | ||
|
57190e7876 | ||
|
5a10132e2b | ||
|
ebcecd4fe9 | ||
|
61a9224a7d | ||
|
47c97c36db | ||
|
5483955590 | ||
|
91f89ccb3d | ||
|
08202c3ede | ||
|
70bc5b2c4a | ||
|
c6d034545a | ||
|
eaaa46294a | ||
|
2240db9baa | ||
|
a1c3d0a2dd | ||
|
7704de6904 | ||
|
721448f408 | ||
|
6ee8d90bdb | ||
|
6fe0a22a48 | ||
|
b9fffcfa30 | ||
|
0c0e7b5582 | ||
|
06db5515f6 | ||
|
a5e293c010 | ||
|
4412d0195c | ||
|
c15285aa64 | ||
|
9ff2b62740 | ||
|
e9ab234d61 | ||
|
7988fdde60 | ||
|
b875ac563d | ||
|
b4a59cfb21 | ||
|
d922900bda | ||
|
24766740c5 | ||
|
73fad2e34b | ||
|
a10605e74c | ||
|
c7f29af2ee | ||
|
ea1579975c | ||
|
6e2aa622ab | ||
|
54778ec1b1 | ||
|
8870f0d356 | ||
|
be4def49a2 | ||
|
589bf9651d | ||
|
60c79141aa | ||
|
faaf502114 | ||
|
30ce80d0c9 | ||
|
8145b128ad | ||
|
a2d9e8e3d2 | ||
|
106dc232b8 | ||
|
379e736e51 | ||
|
8920241c39 | ||
|
ac952f1164 | ||
|
6d7fbd69c7 | ||
|
a4399aaee9 | ||
|
6c3338f5d7 | ||
|
272ba921a0 | ||
|
fdd7dcc0ab | ||
|
5ab86c8362 | ||
|
c6a60a6678 | ||
|
62f20694bf | ||
|
c338dce3ce | ||
|
44deaf7b0a | ||
|
4dc3eada1f | ||
|
d0973de2b4 | ||
|
b4244ea75d | ||
|
a2b01e28c9 | ||
|
935c550733 | ||
|
c101337c38 | ||
|
f3ff6d99c8 | ||
|
de8e4df04c | ||
|
1a82ce193d | ||
|
fb27fac524 | ||
|
6f5d69ec9c | ||
|
55a68d4fec | ||
|
24cf42284f | ||
|
6aa45a2d12 | ||
|
5d65bb5bb1 | ||
|
bfe00595e7 | ||
|
b93af31d2f | ||
|
a9e5020904 | ||
|
58dcbaaf6e | ||
|
e37b805b49 | ||
|
8ebf829169 | ||
|
56b4889b94 | ||
|
17453e5618 | ||
|
ae6437442b | ||
|
373cfc5d10 | ||
|
540915eb65 | ||
|
f111d6e24f | ||
|
676bcc159d | ||
|
1da4373335 | ||
|
c9e782faa7 | ||
|
39119a3419 | ||
|
65166f2fcb | ||
|
eb021f30f5 | ||
|
4aea3b96d7 | ||
|
987274ad2d | ||
|
2166169608 | ||
|
d8e9e7812a | ||
|
3bddda37d2 | ||
|
42f66b11f4 | ||
|
436646cc47 | ||
|
a14e7d3947 | ||
|
47cc313aef | ||
|
44f9a1faa2 | ||
|
f71791bdd5 | ||
|
350ab35856 | ||
|
37cff04e39 | ||
|
e7ed893b78 | ||
|
8f30ef38d9 | ||
|
1fbadf8181 | ||
|
438c12da57 | ||
|
191ec9535c | ||
|
b74fd57912 | ||
|
a54f060ccb | ||
|
fdde57e334 | ||
|
fde90be5a2 | ||
|
d2471e2a02 | ||
|
f35799c129 | ||
|
6a21067440 | ||
|
340b21c53e | ||
|
fc5807ebbe | ||
|
e0806cf0e0 | ||
|
94c448ee8b | ||
|
3e9f046a29 | ||
|
9340d827d9 | ||
|
612066d96c | ||
|
015e86afcb | ||
|
7c710ba598 | ||
|
5b8fc12163 | ||
|
ab3ed3fbf1 | ||
|
22d5c74818 | ||
|
1321139e7f | ||
|
70ef9e1836 | ||
|
e4cfd92bb2 | ||
|
25ee793208 | ||
|
f7164ddd7d | ||
|
dd2b09830e | ||
|
52ecd79736 | ||
|
c316d49957 | ||
|
68f2277def | ||
|
a2d912bb5a | ||
|
c183315d52 | ||
|
6a5873f8d4 | ||
|
456aed467c | ||
|
256fd89fd2 | ||
|
833ec518b4 | ||
|
1321b95eb1 | ||
|
ca4a2bc7db | ||
|
430ad1acb0 | ||
|
1ce989f3d6 | ||
|
5a1cc4c19d | ||
|
e0634cea6d | ||
|
ebbb8905e2 | ||
|
140628692f | ||
|
e9fcf5a352 | ||
|
3362b2f953 | ||
|
61c672ce4c | ||
|
7628713c4b | ||
|
b903183fff | ||
|
1476f669d3 | ||
|
8c6a40bab7 | ||
|
69c2570ff9 | ||
|
b3e6dce31e | ||
|
be91d5d5e0 | ||
|
e9f136a653 | ||
|
4c10c8499b | ||
|
718218c717 | ||
|
772eeb5c93 | ||
|
ada9fac343 | ||
|
733d206517 | ||
|
4a5c6a42f6 | ||
|
e5428ce525 | ||
|
176acf959f | ||
|
8aaaf5e9e0 | ||
|
ccf02bdba8 | ||
|
9aa56dd193 | ||
|
3efd968058 | ||
|
68b0539fc1 | ||
|
7aaacf4d50 | ||
|
050bd14e46 | ||
|
7ba2eab65e | ||
|
edbae16c8e | ||
|
d6b0a1edc0 | ||
|
a2d61a1daa | ||
|
7144e57c93 | ||
|
1ebb505752 | ||
|
273b815e54 | ||
|
e7d8cfe7e0 | ||
|
be851b8382 | ||
|
58ef43a8ec | ||
|
f6feacfbc9 | ||
|
74335f2b01 | ||
|
df3d7b591d | ||
|
c9d323c83f | ||
|
34bad35cb8 | ||
|
c423326270 | ||
|
4398fa9bda | ||
|
2c922cbae6 | ||
|
55b895146b | ||
|
8b1776fe3b | ||
|
de8c27c970 | ||
|
483838c1b2 | ||
|
cb143117e5 | ||
|
22d2248951 | ||
|
2351403674 | ||
|
018eb8fbfc | ||
|
f49cb9b399 | ||
|
d66a81bc6b | ||
|
8e41b39936 | ||
|
0e5b1a7742 | ||
|
3d3caa7a42 | ||
|
a3da8a7c3c | ||
|
2a96ee98f4 | ||
|
5c6fe08bdb | ||
|
747d5d7c7c | ||
|
3a814a5b5d | ||
|
e35c0b3b52 | ||
|
0af1ff112b | ||
|
4456a771fd | ||
|
86422f90ea | ||
|
7d9908dbd0 | ||
|
ff81b859d1 | ||
|
3cb36a36ec | ||
|
4f19220778 | ||
|
5c6328ffc2 | ||
|
28f0c6b1f8 | ||
|
a6ed8c9228 | ||
|
c1287a4a25 | ||
|
f8d346a404 | ||
|
93033b5b24 | ||
|
18815caed4 | ||
|
9ee7173305 | ||
|
b39e0f304f | ||
|
e17234ecce | ||
|
33bcc1a65e | ||
|
e61591622e | ||
|
11ba65ec4a | ||
|
26f83ac4f6 | ||
|
cca870ced5 | ||
|
fdf123b875 | ||
|
a737ae9f46 | ||
|
43660387fa | ||
|
7729bdd2dc | ||
|
1ae0f0e273 | ||
|
18466afc78 | ||
|
4c801f76b4 | ||
|
6a69f44f07 | ||
|
aa5876fe0d | ||
|
e639cb654e | ||
|
1408908959 | ||
|
cd1d8ecd8a | ||
|
0dbb42aa69 | ||
|
2ebb83418c | ||
|
eac56b1f4f | ||
|
987ebccdfd | ||
|
cf74a195b2 | ||
|
677b20a7ba | ||
|
e3e80a5fd0 | ||
|
8aeb544f7e | ||
|
b9ae919fda | ||
|
f25460a647 | ||
|
28f3694e8f | ||
|
caa3fc06e6 | ||
|
1e645f911a | ||
|
adf2086141 | ||
|
d9bb7d1926 | ||
|
5547b30364 | ||
|
3932a3dbd4 | ||
|
bff4eff719 | ||
|
54c227cf6c | ||
|
edbebb7e67 | ||
|
004671f032 | ||
|
45a965476e | ||
|
bcee49878b | ||
|
35de4c485a | ||
|
4439447a6d | ||
|
e6c6f64077 | ||
|
0acdec787d | ||
|
ce52f21ce9 | ||
|
b3343c210a | ||
|
b4e0e9ebc0 | ||
|
28af2063c3 | ||
|
cce14cbe1f | ||
|
87060488f5 | ||
|
ad18987e65 | ||
|
a40bdc28be | ||
|
082125bd2f | ||
|
21870d7edb | ||
|
85be84071a | ||
|
a9627bb2b6 | ||
|
537962a7dc | ||
|
f7d027ccc9 | ||
|
8759064ccb | ||
|
c16e7c6cfd | ||
|
668f30dd55 | ||
|
45e54789b7 | ||
|
c59de1be2e | ||
|
a038ef91eb | ||
|
74af54f3c0 | ||
|
7c44abdcd7 | ||
|
5af92a7d81 | ||
|
2ee067c072 | ||
|
39d7f1055b | ||
|
a3b18e5bea | ||
|
59f3936dad | ||
|
450b140f5f | ||
|
f21711f3dc | ||
|
cd8bb72f94 | ||
|
837a4d8949 | ||
|
8952b100ad | ||
|
2d724bf2c8 | ||
|
374c25ffb3 | ||
|
96cf1a5f7f | ||
|
ae40999700 | ||
|
30d73d6362 | ||
|
97e0a78806 | ||
|
d812776357 | ||
|
9a49c0b8fe | ||
|
70eec63533 | ||
|
6ef2beed8f | ||
|
a15230e7ab | ||
|
a21466d877 | ||
|
89b30fc50d | ||
|
9060abde8e | ||
|
085b9aeb2a | ||
|
c0383bcf26 | ||
|
0938368e30 | ||
|
272658e5dc | ||
|
861fb7abbd | ||
|
2d88675f42 | ||
|
bfa88c3406 | ||
|
784c081663 | ||
|
8318621d51 | ||
|
e924061c54 | ||
|
25a0276bf7 | ||
|
c74d972caf | ||
|
57b74a5d09 | ||
|
9577955d2d | ||
|
cf508fd8b6 | ||
|
2f53cef36f | ||
|
af68fa6c42 | ||
|
231d3e65c4 | ||
|
00de66cd79 | ||
|
b6449ad296 | ||
|
d1e1937195 | ||
|
245627a347 | ||
|
a429a98a29 | ||
|
b1bb6fab5b | ||
|
21b9d0efab | ||
|
4c429cd519 | ||
|
0cb20d89ed | ||
|
8029ee49a4 | ||
|
4406e53121 | ||
|
dca7205a47 | ||
|
04e8bb248b | ||
|
51fe44f877 | ||
|
00ba3b0c48 | ||
|
7508d86c73 | ||
|
8d853815d6 | ||
|
1208694d2d | ||
|
96be4e8992 | ||
|
7310cf3d4a | ||
|
8922b370cc | ||
|
fecf976ab9 | ||
|
0823414360 | ||
|
c6eac97b64 | ||
|
6706fe7350 | ||
|
a7c8b8aec4 | ||
|
5dec6b4a22 | ||
|
a8d7e91a02 | ||
|
fec4e19c1d | ||
|
0568322c82 | ||
|
42548cea2a | ||
|
879d6fb2dd | ||
|
2a17bcb8b2 | ||
|
7c1e663b26 | ||
|
2c3cd34444 | ||
|
e0ebdc644d | ||
|
ee76f4188b | ||
|
58e671e640 | ||
|
bc1ec414de | ||
|
5514eeff2d | ||
|
7a9b159909 | ||
|
74b6df2e44 | ||
|
26aba26da5 | ||
|
7c8b33597a | ||
|
3660830ec1 | ||
|
83696cca21 | ||
|
d06b725f52 | ||
|
149204f6ca | ||
|
5a9d8e3f5d | ||
|
37d2be9384 | ||
|
5df594e46a | ||
|
91e5abe76a | ||
|
27b46f4306 | ||
|
d336383a93 | ||
|
a3569280a4 | ||
|
ccb6fd291e | ||
|
849402ed70 | ||
|
7dddff52b8 | ||
|
40f1c09002 | ||
|
ec90b041ee | ||
|
c202c5de68 | ||
|
aad5f6528b | ||
|
3e1e84ee5e | ||
|
f83b62cf50 | ||
|
d658a48b66 | ||
|
876abef040 | ||
|
74c9406191 | ||
|
a0402830c5 | ||
|
e1f19c52ab | ||
|
7debc4925e | ||
|
1e3a0ca3d9 | ||
|
c7452796f0 | ||
|
1369f3b967 | ||
|
1d948821ca | ||
|
0318f7a12b | ||
|
84432e5ac4 | ||
|
851cffd73e | ||
|
1d1b09c938 | ||
|
8f338a8d88 | ||
|
7ea6777d6b | ||
|
ecacce0796 | ||
|
71dfcc4dd9 | ||
|
6c64c9f1cd | ||
|
6facf3b7a7 | ||
|
62e72b2091 | ||
|
4dad954820 | ||
|
f0727a65fc | ||
|
c7be227865 | ||
|
cf58fc9fd4 | ||
|
996b4795ea | ||
|
7e00f29189 | ||
|
1ff453d64c | ||
|
e4c66e08f5 | ||
|
3fd07da1b0 | ||
|
eb070f0b07 | ||
|
c88621de19 | ||
|
2e96721a5c | ||
|
0a5fb4752a | ||
|
cae2154893 | ||
|
926929880a | ||
|
9c15d5b96c | ||
|
3d073da97e | ||
|
d63dd12056 | ||
|
133e7a9c3f | ||
|
98861ccc19 | ||
|
1e11491369 | ||
|
7c798a063c | ||
|
03e07037ea | ||
|
2acc1a8433 | ||
|
9dd23b4a08 | ||
|
e4f46c48f1 | ||
|
cb08a114ae | ||
|
e7f369e2b4 | ||
|
f31db2f9ed | ||
|
b21051ced5 | ||
|
ef77c7c9a3 | ||
|
36fa9078f5 | ||
|
a80d1f194c | ||
|
d7793841d1 | ||
|
4b513a894d | ||
|
eeed9eef10 | ||
|
305acbb18f | ||
|
5d8f5d41fc | ||
|
3e976eadac | ||
|
51ceb62871 | ||
|
a9ea335cd1 | ||
|
a040df2732 | ||
|
2e3c2d4dcb | ||
|
5ff847fba3 | ||
|
f641569bcc | ||
|
8e4dd407f6 | ||
|
b88f9a4fc1 | ||
|
86cf956894 | ||
|
e4d6bb35b5 | ||
|
902d9e140c | ||
|
9698895c22 | ||
|
a2da319e7c | ||
|
1dbef921b0 | ||
|
59aa76a474 | ||
|
99bff6b794 | ||
|
5735864fd1 | ||
|
8903b1ef95 | ||
|
3255806891 | ||
|
ba7d0f45db | ||
|
490115d890 | ||
|
803091db06 | ||
|
31db330319 | ||
|
b1ccee73fd | ||
|
74ce98913c | ||
|
26a2eb2391 | ||
|
6f2e2a3bbb | ||
|
539bfba70c | ||
|
e5777f02d8 | ||
|
1b029ce8dd | ||
|
4faab4fcdc | ||
|
0f49effade | ||
|
7773234138 | ||
|
91bb4dfab2 | ||
|
97b648a51e | ||
|
b785d4b047 | ||
|
90e1fdb586 | ||
|
dc89d5d4d0 | ||
|
b9f0da9d3b | ||
|
6e3b8fdd4d | ||
|
465e219bfc | ||
|
18f2550e4d | ||
|
93739e7990 | ||
|
51380febd4 | ||
|
cffd5dcd31 | ||
|
0caa5e24e8 | ||
|
25eca71846 | ||
|
535e50eeac | ||
|
bca34dad60 | ||
|
a8da5719fe | ||
|
7a38d67c5b | ||
|
7a22c7d76a | ||
|
8d1cebf4db | ||
|
b6e636cbc0 | ||
|
5bf135760e | ||
|
74a0479cbd | ||
|
52a89d0783 | ||
|
d553aae71e | ||
|
5365fa6175 | ||
|
d5ac560f0c | ||
|
de74b2987a | ||
|
d390b39e0a | ||
|
d6d1e8e86f | ||
|
1c323c5a7f | ||
|
3eb1b66e9a | ||
|
c72bf506c3 | ||
|
432ee387ec | ||
|
a5812a5a73 | ||
|
5dcaae7af6 | ||
|
480371cf9f | ||
|
f50b4775a1 | ||
|
78780a9219 | ||
|
7da4eb8fe9 | ||
|
bea94d58c5 | ||
|
1c73d21925 | ||
|
b476a7e3f8 | ||
|
baa27a3c85 | ||
|
20fd286756 | ||
|
552f9add70 | ||
|
3bea983662 | ||
|
6929076740 | ||
|
e1775681aa | ||
|
ec4d0f6b4a | ||
|
b9a667b126 | ||
|
571cf80e13 | ||
|
3313b55853 | ||
|
650aa68bcd | ||
|
7736f1e3c1 | ||
|
0cd61eb214 | ||
|
2530171721 | ||
|
009c85b61a | ||
|
7a0d64e72f | ||
|
40a22b31f3 | ||
|
8ea9a79760 | ||
|
e6db99e810 | ||
|
571d3e71b5 | ||
|
b7790a9678 | ||
|
88bf678ce3 | ||
|
8b7cd20b6f | ||
|
3158740ea3 | ||
|
258b2a318f | ||
|
aa3647e0f3 | ||
|
d18dd5b8fb | ||
|
645cfc65f4 | ||
|
97b38c156f | ||
|
c6dc852cd8 | ||
|
ef127ea335 | ||
|
43bbc9ec24 | ||
|
2439317408 | ||
|
f4ebb2b504 | ||
|
a9f846e8fc | ||
|
099764a931 | ||
|
09e8993cd4 | ||
|
dd6c5dc97a | ||
|
2fef413d88 | ||
|
474304d284 | ||
|
6791da0fc8 | ||
|
c850cfe97f | ||
|
51c843d765 | ||
|
03d98a7ad7 | ||
|
0cbc0010c1 | ||
|
fc8487dca0 | ||
|
b67a26ad61 | ||
|
39c312cf9f | ||
|
1196ec4375 | ||
|
634196d8f1 | ||
|
36bfbe8f42 | ||
|
a0f62ba172 | ||
|
ba5dabd613 | ||
|
00c9fa61c3 | ||
|
4f3202f90b | ||
|
98a0ed99c9 | ||
|
4d7df00a68 | ||
|
f51ad2224b | ||
|
0972de9025 | ||
|
f2764e9258 | ||
|
2537663a57 | ||
|
0cf9a90cfb | ||
|
4f6d478211 | ||
|
06ced7042d | ||
|
c37997bcb7 | ||
|
c2db558b85 | ||
|
097000c9da | ||
|
d216b298ba | ||
|
56e9b5fa2f | ||
|
c9c3a95d2a | ||
|
87561503c1 | ||
|
68a949de35 | ||
|
33edd3c0fb | ||
|
c3d09e5323 | ||
|
97fa5fa636 | ||
|
fb67010c0e | ||
|
5bf39a7a92 | ||
|
2c97be815b | ||
|
159723ed0c | ||
|
ce3d092497 | ||
|
99009f841b | ||
|
bf64f5b3a9 | ||
|
770a8d049c | ||
|
a00857cb45 | ||
|
59565416b6 | ||
|
8c2f3c56d3 | ||
|
814c4aa01d | ||
|
62728e52b7 | ||
|
63a5241b2e | ||
|
c58ed8bd2c | ||
|
c3eaf0351b | ||
|
59ca1f7640 | ||
|
d00fe7bcd2 | ||
|
186befd0ac | ||
|
ec7263da18 | ||
|
f2f77cb51e | ||
|
e5aef763cd | ||
|
aef14e49bb | ||
|
cd520e6cfe | ||
|
d56435b9cd | ||
|
4002c23bee | ||
|
997d68a574 |
43
.github/workflows/lint.yml
vendored
Normal file
43
.github/workflows/lint.yml
vendored
Normal 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 }}
|
17
.github/workflows/merge-main-into-main2.yml
vendored
Normal file
17
.github/workflows/merge-main-into-main2.yml
vendored
Normal 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 }}
|
|
@ -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,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 }
|
||||
}
|
||||
|
|
|
@ -11,13 +11,16 @@ import {
|
|||
import { User } from './user'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { noFees } from './fees'
|
||||
import { ENV_CONFIG } from './envs/constants'
|
||||
import { Answer } from './answer'
|
||||
|
||||
export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100
|
||||
|
||||
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,
|
||||
|
@ -54,7 +57,7 @@ export function getAnteBets(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const yesBet: Bet = {
|
||||
const yesBet: NormalizedBet = {
|
||||
id: yesAnteId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -68,7 +71,7 @@ export function getAnteBets(
|
|||
fees: noFees,
|
||||
}
|
||||
|
||||
const noBet: Bet = {
|
||||
const noBet: NormalizedBet = {
|
||||
id: noAnteId,
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -96,7 +99,7 @@ export function getFreeAnswerAnte(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const anteBet: Bet = {
|
||||
const anteBet: NormalizedBet = {
|
||||
id: anteBetId,
|
||||
userId: anteBettorId,
|
||||
contractId: contract.id,
|
||||
|
@ -126,7 +129,7 @@ export function getMultipleChoiceAntes(
|
|||
|
||||
const { createdTime } = contract
|
||||
|
||||
const bets: Bet[] = answers.map((answer, i) => ({
|
||||
const bets: NormalizedBet[] = answers.map((answer, i) => ({
|
||||
id: betDocIds[i],
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
|
@ -176,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,
|
||||
|
|
123
common/badge.ts
Normal file
123
common/badge.ts
Normal 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
|
||||
}
|
|
@ -3,6 +3,12 @@ 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
|
||||
|
||||
|
@ -14,19 +20,21 @@ 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
|
||||
|
||||
// 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 & {
|
||||
|
@ -59,5 +67,3 @@ export type fill = {
|
|||
// I.e. -fill.shares === matchedBet.shares
|
||||
isSale?: boolean
|
||||
}
|
||||
|
||||
export const MAX_LOAN_PER_CONTRACT = 20
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { sum, groupBy, mapValues, sumBy } from 'lodash'
|
||||
import { groupBy, mapValues, sumBy } from 'lodash'
|
||||
import { LimitBet } from './bet'
|
||||
|
||||
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { computeFills } from './new-bet'
|
||||
import { binarySearch } from './util/algos'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
export type CpmmState = {
|
||||
pool: { [outcome: string]: number }
|
||||
|
@ -147,7 +146,8 @@ function calculateAmountToBuyShares(
|
|||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[]
|
||||
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.
|
||||
|
@ -157,7 +157,8 @@ function calculateAmountToBuyShares(
|
|||
amount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
|
||||
const totalShares = sumBy(takers, (taker) => taker.shares)
|
||||
|
@ -169,7 +170,8 @@ export function calculateCpmmSale(
|
|||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[]
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) {
|
||||
if (Math.round(shares) < 0) {
|
||||
throw new Error('Cannot sell non-positive shares')
|
||||
|
@ -180,15 +182,17 @@ export function calculateCpmmSale(
|
|||
state,
|
||||
shares,
|
||||
oppositeOutcome,
|
||||
unfilledBets
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
|
||||
const { cpmmState, makers, takers, totalFees } = computeFills(
|
||||
const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills(
|
||||
oppositeOutcome,
|
||||
buyAmount,
|
||||
state,
|
||||
undefined,
|
||||
unfilledBets
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
|
||||
// Transform buys of opposite outcome into sells.
|
||||
|
@ -211,6 +215,7 @@ export function calculateCpmmSale(
|
|||
fees: totalFees,
|
||||
makers,
|
||||
takers: saleTakers,
|
||||
ordersToCancel,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,9 +223,16 @@ export function getCpmmProbabilityAfterSale(
|
|||
state: CpmmState,
|
||||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
unfilledBets: LimitBet[]
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) {
|
||||
const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets)
|
||||
const { cpmmState } = calculateCpmmSale(
|
||||
state,
|
||||
shares,
|
||||
outcome,
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
return getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
}
|
||||
|
||||
|
@ -254,48 +266,22 @@ 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(
|
||||
state: CpmmState,
|
||||
liquidities: LiquidityProvision[],
|
||||
excludeAntes: boolean
|
||||
) {
|
||||
const calcLiqudity = calculateLiquidityDelta(state.p)
|
||||
const liquidityShares = liquidities.map(calcLiqudity)
|
||||
const shareSum = sum(liquidityShares)
|
||||
|
||||
const weights = liquidityShares.map((shares, i) => ({
|
||||
weight: shares / shareSum,
|
||||
providerId: liquidities[i].userId,
|
||||
}))
|
||||
|
||||
const includedWeights = excludeAntes
|
||||
? weights.filter((_, i) => !liquidities[i].isAnte)
|
||||
: weights
|
||||
|
||||
const userWeights = groupBy(includedWeights, (w) => w.providerId)
|
||||
const totalUserWeights = mapValues(userWeights, (userWeight) =>
|
||||
sumBy(userWeight, (w) => w.weight)
|
||||
return mapValues(
|
||||
userAmounts,
|
||||
(amounts) => sumBy(amounts, (w) => w.amount) / totalAmount
|
||||
)
|
||||
return totalUserWeights
|
||||
}
|
||||
|
||||
export function getUserLiquidityShares(
|
||||
userId: string,
|
||||
state: CpmmState,
|
||||
liquidities: LiquidityProvision[],
|
||||
excludeAntes: boolean
|
||||
liquidities: LiquidityProvision[]
|
||||
) {
|
||||
const weights = getCpmmLiquidityPoolWeights(state, liquidities, excludeAntes)
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
const userWeight = weights[userId] ?? 0
|
||||
|
||||
return mapValues(state.pool, (shares) => userWeight * shares)
|
||||
|
|
315
common/calculate-metrics.ts
Normal file
315
common/calculate-metrics.ts
Normal 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,
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { maxBy } from 'lodash'
|
||||
import { maxBy, partition, sortBy, sum, sumBy } from 'lodash'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import {
|
||||
calculateCpmmSale,
|
||||
|
@ -78,7 +78,8 @@ export function calculateShares(
|
|||
export function calculateSaleAmount(
|
||||
contract: Contract,
|
||||
bet: Bet,
|
||||
unfilledBets: LimitBet[]
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1' &&
|
||||
(contract.outcomeType === 'BINARY' ||
|
||||
|
@ -87,7 +88,8 @@ export function calculateSaleAmount(
|
|||
contract,
|
||||
Math.abs(bet.shares),
|
||||
bet.outcome as 'YES' | 'NO',
|
||||
unfilledBets
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
).saleValue
|
||||
: calculateDpmSaleAmount(contract, bet)
|
||||
}
|
||||
|
@ -102,14 +104,16 @@ export function getProbabilityAfterSale(
|
|||
contract: Contract,
|
||||
outcome: string,
|
||||
shares: number,
|
||||
unfilledBets: LimitBet[]
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
? getCpmmProbabilityAfterSale(
|
||||
contract,
|
||||
shares,
|
||||
outcome as 'YES' | 'NO',
|
||||
unfilledBets
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
|
||||
}
|
||||
|
@ -133,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
|
||||
|
@ -162,7 +209,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
|
|||
saleValue -= amount
|
||||
}
|
||||
|
||||
currentInvested += amount
|
||||
loan += loanAmount ?? 0
|
||||
payout += resolution
|
||||
? calculatePayout(contract, bet, resolution)
|
||||
|
@ -170,18 +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 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,
|
||||
|
@ -192,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 },
|
||||
|
@ -214,3 +260,43 @@ export function getTopAnswer(
|
|||
)
|
||||
return top?.answer
|
||||
}
|
||||
|
||||
export function getLargestPosition(contract: Contract, userBets: Bet[]) {
|
||||
let yesFloorShares = 0,
|
||||
yesShares = 0,
|
||||
noShares = 0,
|
||||
noFloorShares = 0
|
||||
|
||||
if (userBets.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||
const answerCounts: { [outcome: string]: number } = {}
|
||||
for (const bet of userBets) {
|
||||
if (bet.outcome) {
|
||||
if (!answerCounts[bet.outcome]) {
|
||||
answerCounts[bet.outcome] = bet.amount
|
||||
} else {
|
||||
answerCounts[bet.outcome] += bet.amount
|
||||
}
|
||||
}
|
||||
}
|
||||
const majorityAnswer =
|
||||
maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? ''
|
||||
return {
|
||||
prob: undefined,
|
||||
shares: answerCounts[majorityAnswer] || 0,
|
||||
outcome: majorityAnswer,
|
||||
}
|
||||
}
|
||||
|
||||
const [yesBets, noBets] = partition(userBets, (bet) => bet.outcome === 'YES')
|
||||
yesShares = sumBy(yesBets, (bet) => bet.shares)
|
||||
noShares = sumBy(noBets, (bet) => bet.shares)
|
||||
yesFloorShares = Math.floor(yesShares)
|
||||
noFloorShares = Math.floor(noShares)
|
||||
|
||||
const shares = yesFloorShares || noFloorShares
|
||||
const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO'
|
||||
return { shares, outcome }
|
||||
}
|
||||
|
|
|
@ -565,6 +565,39 @@ 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 Fund’s 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 Fund’s 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 state’s 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 {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
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
|
||||
|
||||
|
@ -20,4 +18,39 @@ export type Comment = {
|
|||
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
168
common/contract-details.ts
Normal 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
|
||||
)
|
||||
}
|
|
@ -10,6 +10,7 @@ export type AnyOutcomeType =
|
|||
| PseudoNumeric
|
||||
| FreeResponse
|
||||
| Numeric
|
||||
|
||||
export type AnyContractType =
|
||||
| (CPMM & Binary)
|
||||
| (CPMM & PseudoNumeric)
|
||||
|
@ -31,7 +32,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
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
|
||||
|
@ -49,6 +50,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
volume: number
|
||||
volume24Hours: number
|
||||
volume7Days: number
|
||||
elasticity: number
|
||||
|
||||
collectedFees: Fees
|
||||
|
||||
|
@ -57,6 +59,14 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
uniqueBettorIds?: string[]
|
||||
uniqueBettorCount?: number
|
||||
popularityScore?: number
|
||||
dailyScore?: number
|
||||
followerCount?: number
|
||||
featuredOnHomeRank?: number
|
||||
likedByUserIds?: string[]
|
||||
likedByUserCount?: number
|
||||
flaggedByUsernames?: string[]
|
||||
openCommentBounties?: number
|
||||
unlistedById?: string
|
||||
} & T
|
||||
|
||||
export type BinaryContract = Contract & Binary
|
||||
|
@ -82,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 = {
|
||||
|
@ -138,8 +155,11 @@ export const OUTCOME_TYPES = [
|
|||
'NUMERIC',
|
||||
] as const
|
||||
|
||||
export const MAX_QUESTION_LENGTH = 480
|
||||
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
20
common/economy.ts
Normal 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
|
|
@ -21,7 +21,10 @@ 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)
|
||||
}
|
||||
|
||||
|
@ -34,6 +37,11 @@ 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) + '$'
|
||||
|
@ -44,3 +52,7 @@ export const CORS_ORIGIN_VERCEL = new RegExp(
|
|||
)
|
||||
// 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}`
|
||||
}
|
||||
|
|
|
@ -16,4 +16,6 @@ export const DEV_CONFIG: EnvConfig = {
|
|||
cloudRunId: 'w3txbmd3ba',
|
||||
cloudRunRegion: 'uc',
|
||||
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
|
||||
twitchBotEndpoint: 'https://dev-twitch-bot.manifold.markets',
|
||||
sprigEnvironmentId: 'Tu7kRZPm7daP',
|
||||
}
|
||||
|
|
|
@ -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,14 +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
|
||||
referralBonus?: 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 = {
|
||||
|
@ -39,6 +58,7 @@ type FirebaseConfig = {
|
|||
export const PROD_CONFIG: EnvConfig = {
|
||||
domain: 'manifold.markets',
|
||||
amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15',
|
||||
sprigEnvironmentId: 'sQcrq9TDqkib',
|
||||
|
||||
firebaseConfig: {
|
||||
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
|
||||
|
@ -50,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: [
|
||||
|
@ -58,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: [
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
3
common/globalConfig.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export type GlobalConfig = {
|
||||
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
|
||||
}
|
|
@ -6,14 +6,26 @@ 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
|
||||
mostRecentChatActivityTime?: number
|
||||
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
|
||||
|
@ -27,3 +39,4 @@ export type GroupLink = {
|
|||
createdTime: number
|
||||
userId?: string
|
||||
}
|
||||
export type GroupContractDoc = { contractId: string; createdTime: number }
|
||||
|
|
9
common/like.ts
Normal file
9
common/like.ts
Normal 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
138
common/loans.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { sortBy, sum, sumBy } from 'lodash'
|
||||
|
||||
import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet'
|
||||
import { Bet, fill, LimitBet, NumericBet } from './bet'
|
||||
import {
|
||||
calculateDpmShares,
|
||||
getDpmProbability,
|
||||
|
@ -17,8 +17,7 @@ import {
|
|||
import {
|
||||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
DPMContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
|
@ -31,7 +30,10 @@ import {
|
|||
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 }
|
||||
|
@ -141,7 +143,8 @@ export const computeFills = (
|
|||
betAmount: number,
|
||||
state: CpmmState,
|
||||
limitProb: number | undefined,
|
||||
unfilledBets: LimitBet[]
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) => {
|
||||
if (isNaN(betAmount)) {
|
||||
throw new Error('Invalid bet amount: ${betAmount}')
|
||||
|
@ -163,10 +166,12 @@ export const computeFills = (
|
|||
shares: number
|
||||
timestamp: number
|
||||
}[] = []
|
||||
const ordersToCancel: LimitBet[] = []
|
||||
|
||||
let amount = betAmount
|
||||
let cpmmState = { pool: state.pool, p: state.p }
|
||||
let totalFees = noFees
|
||||
const currentBalanceByUserId = { ...balanceByUserId }
|
||||
|
||||
let i = 0
|
||||
while (true) {
|
||||
|
@ -183,9 +188,20 @@ export const computeFills = (
|
|||
takers.push(taker)
|
||||
} else {
|
||||
// Matched against bet.
|
||||
i++
|
||||
const { userId } = maker.bet
|
||||
const makerBalance = currentBalanceByUserId[userId]
|
||||
|
||||
if (floatingGreaterEqual(makerBalance, maker.amount)) {
|
||||
currentBalanceByUserId[userId] = makerBalance - maker.amount
|
||||
} else {
|
||||
// Insufficient balance. Cancel maker bet.
|
||||
ordersToCancel.push(maker.bet)
|
||||
continue
|
||||
}
|
||||
|
||||
takers.push(taker)
|
||||
makers.push(maker)
|
||||
i++
|
||||
}
|
||||
|
||||
amount -= taker.amount
|
||||
|
@ -193,7 +209,7 @@ export const computeFills = (
|
|||
if (floatingEqual(amount, 0)) break
|
||||
}
|
||||
|
||||
return { takers, makers, totalFees, cpmmState }
|
||||
return { takers, makers, totalFees, cpmmState, ordersToCancel }
|
||||
}
|
||||
|
||||
export const getBinaryCpmmBetInfo = (
|
||||
|
@ -201,15 +217,17 @@ export const getBinaryCpmmBetInfo = (
|
|||
betAmount: number,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
limitProb: number | undefined,
|
||||
unfilledBets: LimitBet[]
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) => {
|
||||
const { pool, p } = contract
|
||||
const { takers, makers, cpmmState, totalFees } = computeFills(
|
||||
const { takers, makers, cpmmState, totalFees, ordersToCancel } = computeFills(
|
||||
outcome,
|
||||
betAmount,
|
||||
{ pool, p },
|
||||
limitProb,
|
||||
unfilledBets
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
const probBefore = getCpmmProbability(contract.pool, contract.p)
|
||||
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
@ -244,6 +262,7 @@ export const getBinaryCpmmBetInfo = (
|
|||
newP: cpmmState.p,
|
||||
newTotalLiquidity,
|
||||
makers,
|
||||
ordersToCancel,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -252,14 +271,16 @@ export const getBinaryBetStats = (
|
|||
betAmount: number,
|
||||
contract: CPMMBinaryContract | PseudoNumericContract,
|
||||
limitProb: number,
|
||||
unfilledBets: LimitBet[]
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number }
|
||||
) => {
|
||||
const { newBet } = getBinaryCpmmBetInfo(
|
||||
outcome,
|
||||
betAmount ?? 0,
|
||||
contract,
|
||||
limitProb,
|
||||
unfilledBets as LimitBet[]
|
||||
unfilledBets,
|
||||
balanceByUserId
|
||||
)
|
||||
const remainingMatched =
|
||||
((newBet.orderAmount ?? 0) - newBet.amount) /
|
||||
|
@ -276,8 +297,7 @@ export const getBinaryBetStats = (
|
|||
export const getNewBinaryDpmBetInfo = (
|
||||
outcome: 'YES' | 'NO',
|
||||
amount: number,
|
||||
contract: DPMBinaryContract,
|
||||
loanAmount: number
|
||||
contract: DPMBinaryContract
|
||||
) => {
|
||||
const { YES: yesPool, NO: noPool } = contract.pool
|
||||
|
||||
|
@ -308,7 +328,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
const newBet: CandidateBet = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount,
|
||||
loanAmount: 0,
|
||||
shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
|
@ -323,8 +343,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
export const getNewMultiBetInfo = (
|
||||
outcome: string,
|
||||
amount: number,
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
loanAmount: number
|
||||
contract: DPMContract
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
||||
|
@ -345,7 +364,7 @@ export const getNewMultiBetInfo = (
|
|||
const newBet: CandidateBet = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount,
|
||||
loanAmount: 0,
|
||||
shares,
|
||||
outcome,
|
||||
probBefore,
|
||||
|
@ -399,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
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ import {
|
|||
Numeric,
|
||||
outcomeType,
|
||||
PseudoNumeric,
|
||||
visibility,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { parseTags, richTextToString } from './util/parse'
|
||||
import { removeUndefinedProps } from './util/object'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
|
||||
|
@ -34,17 +34,9 @@ export function getNewContract(
|
|||
isLogScale: boolean,
|
||||
|
||||
// for multiple choice
|
||||
answers: string[]
|
||||
answers: string[],
|
||||
visibility: visibility
|
||||
) {
|
||||
const tags = parseTags(
|
||||
[
|
||||
question,
|
||||
richTextToString(description),
|
||||
...extraTags.map((tag) => `#${tag}`),
|
||||
].join(' ')
|
||||
)
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||
|
||||
const propsByOutcomeType =
|
||||
outcomeType === 'BINARY'
|
||||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||
|
@ -68,9 +60,10 @@ export function getNewContract(
|
|||
|
||||
question: question.trim(),
|
||||
description,
|
||||
tags,
|
||||
lowercaseTags,
|
||||
visibility: 'public',
|
||||
tags: [],
|
||||
lowercaseTags: [],
|
||||
visibility,
|
||||
unlistedById: visibility === 'unlisted' ? creator.id : undefined,
|
||||
isResolved: false,
|
||||
createdTime: Date.now(),
|
||||
closeTime,
|
||||
|
@ -78,6 +71,7 @@ export function getNewContract(
|
|||
volume: 0,
|
||||
volume24Hours: 0,
|
||||
volume7Days: 0,
|
||||
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
|
||||
|
||||
collectedFees: {
|
||||
creatorFee: 0,
|
||||
|
@ -118,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
|
||||
|
|
|
@ -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'
|
||||
|
@ -38,6 +42,11 @@ export type notification_source_types =
|
|||
| 'user'
|
||||
| 'bonus'
|
||||
| 'challenge'
|
||||
| 'betting_streak_bonus'
|
||||
| 'loan'
|
||||
| 'like'
|
||||
| 'tip_and_like'
|
||||
| 'badge'
|
||||
|
||||
export type notification_source_update_types =
|
||||
| 'created'
|
||||
|
@ -46,23 +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
|
||||
}
|
||||
|
|
|
@ -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 = 10
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@tiptap/core": "2.0.0-beta.181",
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
||||
"@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": {
|
||||
|
|
|
@ -13,7 +13,6 @@ 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)
|
||||
|
||||
|
@ -58,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,
|
||||
|
@ -110,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,
|
||||
|
@ -163,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,
|
||||
|
@ -202,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 }
|
||||
})
|
||||
|
||||
|
@ -216,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,
|
||||
|
|
|
@ -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, false)
|
||||
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, false)
|
||||
const weights = getCpmmLiquidityPoolWeights(liquidities)
|
||||
|
||||
return Object.entries(weights).map(([providerId, weight]) => ({
|
||||
userId: providerId,
|
||||
|
|
29
common/post.ts
Normal file
29
common/post.ts
Normal 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
|
|
@ -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]])
|
||||
)
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,14 @@ 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)
|
||||
|
||||
|
@ -64,6 +67,7 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
|
|||
betId,
|
||||
},
|
||||
fees,
|
||||
loanAmount: -(loanAmount ?? 0),
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -79,19 +83,20 @@ export const getCpmmSellBetInfo = (
|
|||
shares: number,
|
||||
outcome: 'YES' | 'NO',
|
||||
contract: CPMMContract,
|
||||
prevLoanAmount: number,
|
||||
unfilledBets: LimitBet[]
|
||||
unfilledBets: LimitBet[],
|
||||
balanceByUserId: { [userId: string]: number },
|
||||
loanPaid: number
|
||||
) => {
|
||||
const { pool, p } = contract
|
||||
|
||||
const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale(
|
||||
const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale(
|
||||
contract,
|
||||
shares,
|
||||
outcome,
|
||||
unfilledBets
|
||||
unfilledBets,
|
||||
balanceByUserId,
|
||||
)
|
||||
|
||||
const loanPaid = Math.min(prevLoanAmount, saleValue)
|
||||
const probBefore = getCpmmProbability(pool, p)
|
||||
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
|
||||
|
||||
|
@ -131,5 +136,6 @@ export const getCpmmSellBetInfo = (
|
|||
fees,
|
||||
makers,
|
||||
takers,
|
||||
ordersToCancel
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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 }
|
||||
|
@ -54,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
|
||||
|
|
222
common/user-notification-preferences.ts
Normal file
222
common/user-notification-preferences.ts
Normal 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§ion=${subscriptionType}`,
|
||||
}
|
||||
} catch (e) {
|
||||
// Fail safely
|
||||
console.log(
|
||||
`couldn't get notification destinations for type ${reason} for user ${privateUser.id}`
|
||||
)
|
||||
return {
|
||||
sendToEmail: false,
|
||||
sendToBrowser: false,
|
||||
unsubscribeUrl: '',
|
||||
urlToManageThisNotification: '',
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,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,40 +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
|
||||
|
||||
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
|
||||
// for sus users, i.e. multiple sign ups for same person
|
||||
export const SUS_STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 10
|
||||
export const REFERRAL_AMOUNT = ENV_CONFIG.referralBonus ?? 500
|
||||
achievements: {
|
||||
provenCorrect?: {
|
||||
badges: ProvenCorrectBadge[]
|
||||
}
|
||||
marketCreator?: {
|
||||
badges: MarketCreatorBadge[]
|
||||
}
|
||||
streaker?: {
|
||||
badges: StreakerBadge[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
|
@ -74,5 +93,15 @@ export type PortfolioMetrics = {
|
|||
userId: string
|
||||
}
|
||||
|
||||
export const MANIFOLD_USERNAME = 'ManifoldMarkets'
|
||||
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'
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { isEqual } from 'lodash'
|
||||
|
||||
export function filterDefined<T>(array: (T | null | undefined)[]) {
|
||||
return array.filter((item) => item !== null && item !== undefined) as T[]
|
||||
}
|
||||
|
@ -26,7 +28,7 @@ export function groupConsecutive<T, U>(xs: T[], key: (x: T) => U) {
|
|||
let curr = { key: key(xs[0]), items: [xs[0]] }
|
||||
for (const x of xs.slice(1)) {
|
||||
const k = key(x)
|
||||
if (k !== curr.key) {
|
||||
if (!isEqual(key, curr.key)) {
|
||||
result.push(curr)
|
||||
curr = { key: k, items: [x] }
|
||||
} else {
|
||||
|
|
24
common/util/color.ts
Normal file
24
common/util/color.ts
Normal 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]
|
||||
}
|
|
@ -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('$', '')
|
||||
}
|
||||
|
||||
|
@ -53,6 +60,16 @@ export function formatLargeNumber(num: number, sigfigs = 2): string {
|
|||
return `${numStr}${suffix[i] ?? ''}`
|
||||
}
|
||||
|
||||
export function shortFormatNumber(num: number): string {
|
||||
if (num < 1000) return showPrecision(num, 3)
|
||||
|
||||
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
|
||||
const i = Math.floor(Math.log10(num) / 3)
|
||||
|
||||
const numStr = showPrecision(num / Math.pow(10, 3 * i), 2)
|
||||
return `${numStr}${suffix[i] ?? ''}`
|
||||
}
|
||||
|
||||
export function toCamelCase(words: string) {
|
||||
const camelCase = words
|
||||
.split(' ')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { union } from 'lodash'
|
||||
|
||||
export const removeUndefinedProps = <T>(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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { MAX_TAG_LENGTH } from '../contract'
|
||||
import { generateText, JSONContent } from '@tiptap/core'
|
||||
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'
|
||||
|
@ -23,34 +23,14 @@ 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)
|
||||
}
|
||||
}
|
||||
uniqueTags.reverse()
|
||||
return uniqueTags
|
||||
}
|
||||
|
||||
export function parseWordsAsTags(text: string) {
|
||||
const taggedText = text
|
||||
.split(/\s+/)
|
||||
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
|
||||
.join(' ')
|
||||
return parseTags(taggedText)
|
||||
/** 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
|
||||
|
@ -72,8 +52,28 @@ export function parseMentions(data: JSONContent): string[] {
|
|||
return uniq(mentions)
|
||||
}
|
||||
|
||||
// can't just do [StarterKit, Image...] because it doesn't work with cjs imports
|
||||
export const exhibitExts = [
|
||||
// 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,
|
||||
|
@ -90,14 +90,26 @@ export const exhibitExts = [
|
|||
Paragraph,
|
||||
Strike,
|
||||
Text,
|
||||
|
||||
Image,
|
||||
// other extensions
|
||||
Link,
|
||||
Mention,
|
||||
Iframe,
|
||||
TiptapTweet,
|
||||
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) {
|
||||
return !text ? '' : generateText(text, exhibitExts)
|
||||
if (!text) return ''
|
||||
return generateText(text, stringParseExts)
|
||||
}
|
||||
|
||||
export function htmlToRichText(html: string) {
|
||||
return generateJSON(html, stringParseExts)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -35,7 +35,7 @@ export default Node.create<IframeOptions>({
|
|||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper' + ' ' + wrapperClasses,
|
||||
// Tailwind JIT doesn't seem to pick up `pb-[20rem]`, so we hack this in:
|
||||
style: 'padding-bottom: 20rem;',
|
||||
style: 'padding-bottom: 20rem; ',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -48,6 +48,9 @@ export default Node.create<IframeOptions>({
|
|||
frameborder: {
|
||||
default: 0,
|
||||
},
|
||||
height: {
|
||||
default: 0,
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => this.options.allowFullscreen,
|
||||
|
@ -60,6 +63,11 @@ export default Node.create<IframeOptions>({
|
|||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
this.options.HTMLAttributes.style =
|
||||
this.options.HTMLAttributes.style +
|
||||
' height: ' +
|
||||
HTMLAttributes.height +
|
||||
';'
|
||||
return [
|
||||
'div',
|
||||
this.options.HTMLAttributes,
|
||||
|
|
116
common/util/tiptap-spoiler.ts
Normal file
116
common/util/tiptap-spoiler.ts
Normal 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
|
||||
},
|
||||
})
|
2
dev.sh
2
dev.sh
|
@ -24,7 +24,7 @@ then
|
|||
npx concurrently \
|
||||
-n FIRESTORE,FUNCTIONS,NEXT,TS \
|
||||
-c green,white,magenta,cyan \
|
||||
"yarn --cwd=functions firestore" \
|
||||
"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 \
|
||||
|
|
|
@ -54,19 +54,33 @@ Returns the authenticated user.
|
|||
|
||||
Gets all groups, in no particular order.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `availableToUserId`: Optional. if specified, only groups that the user can
|
||||
join and groups they've already joined will be returned.
|
||||
|
||||
Requires no authorization.
|
||||
|
||||
### `GET /v0/groups/[slug]`
|
||||
### `GET /v0/group/[slug]`
|
||||
|
||||
Gets a group by its slug.
|
||||
|
||||
Requires no authorization.
|
||||
Note: group is singular in the URL.
|
||||
|
||||
### `GET /v0/groups/by-id/[id]`
|
||||
### `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`
|
||||
|
||||
|
@ -97,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"
|
||||
|
@ -135,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.
|
||||
|
@ -146,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
|
||||
|
@ -162,6 +177,8 @@ Requires no authorization.
|
|||
resolutionTime?: number
|
||||
resolution?: string
|
||||
resolutionProbability?: number // Used for BINARY markets resolved to MKT
|
||||
|
||||
lastUpdatedTime?: number
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -394,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 = {
|
||||
|
@ -538,9 +557,10 @@ 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.
|
||||
|
||||
|
@ -552,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:
|
||||
|
||||
|
@ -565,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.
|
||||
|
@ -576,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:
|
||||
|
||||
|
@ -639,6 +680,17 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \
|
|||
--data-raw '{"outcome": "YES", "shares": 10}'
|
||||
```
|
||||
|
||||
### `POST /v0/comment`
|
||||
|
||||
Creates a comment in the specified market. Only supports top-level comments for now.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `contractId`: Required. The ID of the market to comment on.
|
||||
- `content`: The comment to post, formatted as [TipTap json](https://tiptap.dev/guide/output#option-1-json), OR
|
||||
- `html`: The comment to post, formatted as an HTML string, OR
|
||||
- `markdown`: The comment to post, formatted as a markdown string.
|
||||
|
||||
### `GET /v0/bets`
|
||||
|
||||
Gets a list of bets, ordered by creation date descending.
|
||||
|
@ -728,6 +780,7 @@ Requires no authorization.
|
|||
|
||||
## Changelog
|
||||
|
||||
- 2022-09-24: Expand market POST docs to include new market types (`PSEUDO_NUMERIC`, `MULTIPLE_CHOICE`)
|
||||
- 2022-07-15: Add user by username and user by ID APIs
|
||||
- 2022-06-08: Add paging to markets endpoint
|
||||
- 2022-06-05: Add new authorized write endpoints
|
||||
|
|
|
@ -8,18 +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
|
||||
|
|
|
@ -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**
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
@ -496,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",
|
||||
|
|
|
@ -10,7 +10,12 @@ service cloud.firestore {
|
|||
'akrolsmir@gmail.com',
|
||||
'jahooma@gmail.com',
|
||||
'taowell@gmail.com',
|
||||
'manticmarkets@gmail.com'
|
||||
'abc.sinclair@gmail.com',
|
||||
'manticmarkets@gmail.com',
|
||||
'iansphilips@gmail.com',
|
||||
'd4vidchee@gmail.com',
|
||||
'federicoruizcassarino@gmail.com',
|
||||
'ingawei@gmail.com'
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -18,11 +23,17 @@ service cloud.firestore {
|
|||
allow read;
|
||||
}
|
||||
|
||||
match /globalConfig/globalConfig {
|
||||
allow read;
|
||||
allow update: if isAdmin()
|
||||
allow create: if isAdmin()
|
||||
}
|
||||
|
||||
match /users/{userId} {
|
||||
allow read;
|
||||
allow update: if userId == request.auth.uid
|
||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome']);
|
||||
.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()
|
||||
|
@ -39,10 +50,19 @@ service cloud.firestore {
|
|||
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;
|
||||
|
@ -55,6 +75,11 @@ service cloud.firestore {
|
|||
allow write: if request.auth.uid == userId;
|
||||
}
|
||||
|
||||
match /users/{userId}/likes/{likeId} {
|
||||
allow read;
|
||||
allow write: if request.auth.uid == userId;
|
||||
}
|
||||
|
||||
match /{somePath=**}/follows/{followUserId} {
|
||||
allow read;
|
||||
}
|
||||
|
@ -63,7 +88,7 @@ service cloud.firestore {
|
|||
allow read: if userId == request.auth.uid || isAdmin();
|
||||
allow update: if (userId == request.auth.uid || isAdmin())
|
||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||
.hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences' ]);
|
||||
.hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']);
|
||||
}
|
||||
|
||||
match /private-users/{userId}/views/{viewId} {
|
||||
|
@ -85,9 +110,9 @@ service cloud.firestore {
|
|||
match /contracts/{contractId} {
|
||||
allow read;
|
||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks']);
|
||||
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks', 'flaggedByUsernames']);
|
||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||
.hasOnly(['description', 'closeTime', 'question'])
|
||||
.hasOnly(['description', 'closeTime', 'question', 'visibility', 'unlistedById'])
|
||||
&& resource.data.creatorId == request.auth.uid;
|
||||
allow update: if isAdmin();
|
||||
match /comments/{commentId} {
|
||||
|
@ -148,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
3
functions/.env.dev
Normal file
|
@ -0,0 +1,3 @@
|
|||
# This sets which EnvConfig is deployed to Firebase Cloud Functions
|
||||
|
||||
NEXT_PUBLIC_FIREBASE_ENV=DEV
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
1
functions/.gitignore
vendored
1
functions/.gitignore
vendored
|
@ -17,4 +17,5 @@ package-lock.json
|
|||
ui-debug.log
|
||||
firebase-debug.log
|
||||
firestore-debug.log
|
||||
pubsub-debug.log
|
||||
firestore_export/
|
||||
|
|
|
@ -20,7 +20,7 @@ Adapted from https://firebase.google.com/docs/functions/get-started
|
|||
3. `$ firebase login` to authenticate the CLI tools to Firebase
|
||||
4. `$ firebase use dev` to choose the dev project
|
||||
|
||||
### 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.`):
|
||||
|
@ -35,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
|
||||
|
||||
|
@ -65,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`
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"firestore": "dev-mantic-markets.appspot.com"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist && cp .env 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",
|
||||
|
@ -13,11 +13,11 @@
|
|||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log",
|
||||
"dev": "nodemon src/serve.ts",
|
||||
"firestore": "firebase emulators:start --only firestore --import=./firestore_export",
|
||||
"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",
|
||||
"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"
|
||||
|
@ -26,11 +26,13 @@
|
|||
"dependencies": {
|
||||
"@amplitude/node": "1.10.0",
|
||||
"@google-cloud/functions-framework": "3.1.2",
|
||||
"@tiptap/core": "2.0.0-beta.181",
|
||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
||||
"@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",
|
||||
|
@ -38,14 +40,19 @@
|
|||
"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
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ 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(),
|
||||
|
@ -163,5 +164,7 @@ export const acceptchallenge = newEndpoint({}, async (req, auth) => {
|
|||
return yourNewBetDoc
|
||||
})
|
||||
|
||||
await redeemShares(auth.uid, contractId)
|
||||
|
||||
return { betId: result.id }
|
||||
})
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Contract } from '../../common/contract'
|
||||
import { Contract, CPMMContract } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import { removeUndefinedProps } from '../../common/util/object'
|
||||
import { getNewLiquidityProvision } from '../../common/add-liquidity'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
|
||||
|
@ -12,10 +11,10 @@ const bodySchema = z.object({
|
|||
amount: z.number().gt(0),
|
||||
})
|
||||
|
||||
export const addliquidity = newEndpoint({}, async (req, auth) => {
|
||||
export const addsubsidy = newEndpoint({}, async (req, auth) => {
|
||||
const { amount, contractId } = validate(bodySchema, req.body)
|
||||
|
||||
if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
|
||||
if (!isFinite(amount) || amount < 1) throw new APIError(400, 'Invalid amount')
|
||||
|
||||
// run as transaction to prevent race conditions
|
||||
return await firestore.runTransaction(async (transaction) => {
|
||||
|
@ -45,29 +44,18 @@ export const addliquidity = newEndpoint({}, async (req, auth) => {
|
|||
.collection(`contracts/${contractId}/liquidity`)
|
||||
.doc()
|
||||
|
||||
const { newLiquidityProvision, newPool, newP, newTotalLiquidity } =
|
||||
const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } =
|
||||
getNewLiquidityProvision(
|
||||
user,
|
||||
user.id,
|
||||
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,
|
||||
})
|
||||
)
|
||||
transaction.update(contractDoc, {
|
||||
subsidyPool: newSubsidyPool,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
} as Partial<CPMMContract>)
|
||||
|
||||
const newBalance = user.balance - amount
|
||||
const newTotalDeposits = user.totalDeposits - amount
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
export { APIError } from '../../common/api'
|
||||
|
||||
type Output = Record<string, unknown>
|
||||
type AuthedUser = {
|
||||
export type AuthedUser = {
|
||||
uid: string
|
||||
creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser })
|
||||
}
|
||||
|
@ -23,13 +23,8 @@ type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
|
|||
type KeyCredentials = { kind: 'key'; data: string }
|
||||
type Credentials = JwtCredentials | KeyCredentials
|
||||
|
||||
const auth = admin.auth()
|
||||
const firestore = admin.firestore()
|
||||
const privateUsers = firestore.collection(
|
||||
'private-users'
|
||||
) as admin.firestore.CollectionReference<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.')
|
||||
|
@ -57,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') {
|
||||
|
@ -70,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:
|
||||
|
@ -149,3 +146,24 @@ export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
|
|||
},
|
||||
} as EndpointDefinition
|
||||
}
|
||||
|
||||
export const newEndpointNoAuth = (
|
||||
endpointOpts: EndpointOptions,
|
||||
fn: (req: Request) => Promise<Output>
|
||||
) => {
|
||||
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
|
||||
return {
|
||||
opts,
|
||||
handler: async (req: Request, res: Response) => {
|
||||
log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`)
|
||||
try {
|
||||
if (opts.method !== req.method) {
|
||||
throw new APIError(405, `This endpoint supports only ${opts.method}.`)
|
||||
}
|
||||
res.status(200).json(await fn(req))
|
||||
} catch (e) {
|
||||
writeResponseError(e, res)
|
||||
}
|
||||
},
|
||||
} as EndpointDefinition
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ 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'
|
||||
|
@ -37,6 +38,56 @@ 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)
|
||||
|
@ -58,42 +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)
|
||||
|
||||
transaction.update(userRef, userUpdate)
|
||||
commentSnap.docs.forEach((d) => transaction.update(d.ref, commentUpdate))
|
||||
answerSnap.docs.forEach((d) => transaction.update(d.ref, answerUpdate))
|
||||
contracts.docs.forEach((d) => transaction.update(d.ref, contractUpdate))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
58
functions/src/close-market.ts
Normal file
58
functions/src/close-market.ts
Normal 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()
|
|
@ -5,9 +5,9 @@ import { Contract } from '../../common/contract'
|
|||
import { User } from '../../common/user'
|
||||
import { getNewMultiBetInfo } from '../../common/new-bet'
|
||||
import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
|
||||
import { getContract, getValues } from './utils'
|
||||
import { sendNewAnswerEmail } from './emails'
|
||||
import { getValues } from './utils'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { addUserToContractFollowers } from './follow-market'
|
||||
|
||||
const bodySchema = z.object({
|
||||
contractId: z.string().max(MAX_ANSWER_LENGTH),
|
||||
|
@ -75,10 +75,8 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
|
|||
}
|
||||
transaction.create(newAnswerDoc, answer)
|
||||
|
||||
const loanAmount = 0
|
||||
|
||||
const { newBet, newPool, newTotalShares, newTotalBets } =
|
||||
getNewMultiBetInfo(answerId, amount, contract, loanAmount)
|
||||
getNewMultiBetInfo(answerId, amount, contract)
|
||||
|
||||
const newBalance = user.balance - amount
|
||||
const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
|
||||
|
@ -99,9 +97,7 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
|
|||
return answer
|
||||
})
|
||||
|
||||
const contract = await getContract(contractId)
|
||||
|
||||
if (answer && contract) await sendNewAnswerEmail(answer, contract)
|
||||
await addUserToContractFollowers(contractId, auth.uid)
|
||||
|
||||
return answer
|
||||
})
|
||||
|
|
105
functions/src/create-comment.ts
Normal file
105
functions/src/create-comment.ts
Normal 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 }
|
||||
})
|
|
@ -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)
|
||||
|
|
|
@ -10,19 +10,22 @@ import {
|
|||
MultipleChoiceContract,
|
||||
NumericContract,
|
||||
OUTCOME_TYPES,
|
||||
VISIBILITIES,
|
||||
} from '../../common/contract'
|
||||
import { slugify } from '../../common/util/slugify'
|
||||
import { randomString } from '../../common/util/random'
|
||||
|
||||
import { chargeUser, getContract } from './utils'
|
||||
import { APIError, newEndpoint, validate, zTimestamp } from './api'
|
||||
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 {
|
||||
FIXED_ANTE,
|
||||
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'
|
||||
|
@ -33,6 +36,7 @@ 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(
|
||||
|
@ -59,7 +63,7 @@ const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
|
|||
|
||||
const bodySchema = z.object({
|
||||
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
||||
description: descScehma.optional(),
|
||||
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(),
|
||||
|
@ -67,6 +71,7 @@ const bodySchema = z.object({
|
|||
),
|
||||
outcomeType: z.enum(OUTCOME_TYPES),
|
||||
groupId: z.string().min(1).max(MAX_ID_LENGTH).optional(),
|
||||
visibility: z.enum(VISIBILITIES).optional(),
|
||||
})
|
||||
|
||||
const binarySchema = z.object({
|
||||
|
@ -87,18 +92,26 @@ const multipleChoiceSchema = z.object({
|
|||
answers: z.string().trim().min(1).array().min(2),
|
||||
})
|
||||
|
||||
export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||
const { question, description, tags, closeTime, outcomeType, groupId } =
|
||||
validate(bodySchema, req.body)
|
||||
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,
|
||||
req.body
|
||||
))
|
||||
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
|
||||
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
|
||||
throw new APIError(400, 'Invalid range.')
|
||||
|
||||
|
@ -114,11 +127,11 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
}
|
||||
|
||||
if (outcomeType === 'BINARY') {
|
||||
;({ initialProb } = validate(binarySchema, req.body))
|
||||
;({ initialProb } = validate(binarySchema, body))
|
||||
}
|
||||
|
||||
if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||
;({ answers } = validate(multipleChoiceSchema, req.body))
|
||||
;({ answers } = validate(multipleChoiceSchema, body))
|
||||
}
|
||||
|
||||
const userDoc = await firestore.collection('users').doc(auth.uid).get()
|
||||
|
@ -128,11 +141,38 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
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)
|
||||
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()
|
||||
|
||||
|
@ -145,13 +185,27 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
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,
|
||||
description ?? {},
|
||||
newDescription,
|
||||
initialProb ?? 0,
|
||||
ante,
|
||||
closeTime.getTime(),
|
||||
|
@ -160,42 +214,44 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
min ?? 0,
|
||||
max ?? 0,
|
||||
isLogScale ?? false,
|
||||
answers ?? []
|
||||
answers ?? [],
|
||||
visibility
|
||||
)
|
||||
|
||||
if (ante) await chargeUser(user.id, ante, true)
|
||||
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)
|
||||
|
||||
let group = 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
|
||||
if (
|
||||
!group.memberIds.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.'
|
||||
)
|
||||
}
|
||||
if (!group.contractIds.includes(contractRef.id)) {
|
||||
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)
|
||||
await groupDocRef.update({
|
||||
contractIds: uniq([...group.contractIds, contractRef.id]),
|
||||
const groupContractRef = firestore
|
||||
.collection(`groups/${groupId}/groupContracts`)
|
||||
.doc(contract.id)
|
||||
await groupContractRef.set({
|
||||
contractId: contract.id,
|
||||
createdTime: Date.now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const providerId = user.id
|
||||
|
||||
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
||||
const liquidityDoc = firestore
|
||||
.collection(`contracts/${contract.id}/liquidity`)
|
||||
|
@ -268,7 +324,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
}
|
||||
|
||||
return contract
|
||||
})
|
||||
}
|
||||
|
||||
const getSlug = async (question: string) => {
|
||||
const proposedSlug = slugify(question)
|
File diff suppressed because it is too large
Load Diff
142
functions/src/create-post.ts
Normal file
142
functions/src/create-post.ts
Normal 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)
|
||||
}
|
|
@ -1,22 +1,13 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { z } from 'zod'
|
||||
import { uniq } from 'lodash'
|
||||
|
||||
import {
|
||||
MANIFOLD_AVATAR_URL,
|
||||
MANIFOLD_USERNAME,
|
||||
PrivateUser,
|
||||
STARTING_BALANCE,
|
||||
SUS_STARTING_BALANCE,
|
||||
User,
|
||||
} from '../../common/user'
|
||||
import { getUser, getUserByUsername, getValues, isProd } 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 {
|
||||
CATEGORIES_GROUP_SLUG_POSTFIX,
|
||||
|
@ -25,11 +16,9 @@ import {
|
|||
|
||||
import { track } from './analytics'
|
||||
import { APIError, newEndpoint, validate } from './api'
|
||||
import { Group, NEW_USER_GROUP_SLUGS } from '../../common/group'
|
||||
import {
|
||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
} from '../../common/antes'
|
||||
import { Group } from '../../common/group'
|
||||
import { SUS_STARTING_BALANCE, STARTING_BALANCE } from '../../common/economy'
|
||||
import { getDefaultNotificationPreferences } from '../../common/user-notification-preferences'
|
||||
|
||||
const bodySchema = z.object({
|
||||
deviceToken: z.string().optional(),
|
||||
|
@ -76,9 +65,12 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
|||
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: {},
|
||||
}
|
||||
|
||||
await firestore.collection('users').doc(auth.uid).create(user)
|
||||
|
@ -90,12 +82,12 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
|||
email,
|
||||
initialIpAddress: req.ip,
|
||||
initialDeviceToken: deviceToken,
|
||||
notificationPreferences: getDefaultNotificationPreferences(auth.uid),
|
||||
}
|
||||
|
||||
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
||||
|
||||
await addUserToDefaultGroups(user)
|
||||
await sendWelcomeEmail(user, privateUser)
|
||||
|
||||
await track(auth.uid, 'create user', { username }, { ip: req.ip })
|
||||
|
||||
return { user, privateUser }
|
||||
|
@ -128,42 +120,8 @@ const addUserToDefaultGroups = async (user: User) => {
|
|||
firestore.collection('groups').where('slug', '==', slug)
|
||||
)
|
||||
await firestore
|
||||
.collection('groups')
|
||||
.doc(groups[0].id)
|
||||
.update({
|
||||
memberIds: uniq(groups[0].memberIds.concat(user.id)),
|
||||
})
|
||||
}
|
||||
|
||||
for (const slug of NEW_USER_GROUP_SLUGS) {
|
||||
const groups = await getValues<Group>(
|
||||
firestore.collection('groups').where('slug', '==', slug)
|
||||
)
|
||||
const group = groups[0]
|
||||
await firestore
|
||||
.collection('groups')
|
||||
.doc(group.id)
|
||||
.update({
|
||||
memberIds: uniq(group.memberIds.concat(user.id)),
|
||||
})
|
||||
const manifoldAccount = isProd()
|
||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
|
||||
if (slug === 'welcome') {
|
||||
const welcomeCommentDoc = firestore
|
||||
.collection(`groups/${group.id}/comments`)
|
||||
.doc()
|
||||
await welcomeCommentDoc.create({
|
||||
id: welcomeCommentDoc.id,
|
||||
groupId: group.id,
|
||||
userId: manifoldAccount,
|
||||
text: `Welcome, @${user.username} aka ${user.name}!`,
|
||||
createdTime: Date.now(),
|
||||
userName: 'Manifold Markets',
|
||||
userUsername: MANIFOLD_USERNAME,
|
||||
userAvatarUrl: MANIFOLD_AVATAR_URL,
|
||||
})
|
||||
}
|
||||
.collection(`groups/${groups[0].id}/groupMembers`)
|
||||
.doc(user.id)
|
||||
.set({ userId: user.id, createdTime: Date.now() })
|
||||
}
|
||||
}
|
||||
|
|
69
functions/src/drizzle-liquidity.ts
Normal file
69
functions/src/drizzle-liquidity.ts
Normal 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()
|
||||
})
|
||||
}
|
|
@ -1,260 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>Manifold Markets 7th Day Anniversary Gift!</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
[owa] .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing:normal;background-color:#F4F4F4;">
|
||||
<div style="background-color:#F4F4F4;">
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="direction:ltr;font-size:0px;padding:0px 0px 0px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:0px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:550px;"><a href="https://manifold.markets/home" target="_blank"><img
|
||||
alt="" height="auto" src="https://i.imgur.com/8EP8Y8q.gif"
|
||||
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
||||
width="550"></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; text-align: center; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">Thanks for
|
||||
using Manifold Markets. Running low
|
||||
on mana (M$)? Click the link below to receive a one time gift of M$500!</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="border-radius: 2px;" bgcolor="#4337c9">
|
||||
<a href="{{manalink}}" target="_blank"
|
||||
style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:bold;display: inline-block;">
|
||||
Claim M$500
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
|
||||
data-testid="3Q8BP69fq"></a></li>
|
||||
</ul>
|
||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"> </p>
|
||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
||||
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span></p>
|
||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
||||
style="color:#000000;font-family:Arial;font-size:18px;">David from Manifold</span></p>
|
||||
<p class="text-build-content" data-testid="3Q8BP69fq"
|
||||
style="margin: 10px 0; margin-bottom: 10px;"> </p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0px 20px 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align:top;padding:0;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
|
||||
<p style="margin: 10px 0;">This e-mail has been sent to {{name}}, <a
|
||||
href="{{unsubscribeLink}}” style=" color:inherit;text-decoration:none;"
|
||||
target="_blank">click here to unsubscribe</a>.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
475
functions/src/email-templates/interesting-markets.html
Normal file
475
functions/src/email-templates/interesting-markets.html
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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="{{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>!
|
||||
</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>
|
|
@ -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 comment</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 comment</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,59 +218,42 @@
|
|||
width: 80%;
|
||||
margin: 40px auto;
|
||||
margin-top: 10px;
|
||||
"
|
||||
>
|
||||
<tr
|
||||
style="
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src="{{commentorAvatarUrl}}"
|
||||
width="30"
|
||||
height="30"
|
||||
style="
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{commentorAvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
"
|
||||
alt=""
|
||||
/>
|
||||
<span style="font-weight: bold"
|
||||
>{{commentorName}}</span
|
||||
>
|
||||
{{betDescription}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{commentorName}}</span>
|
||||
{{betDescription}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
style="
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -326,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"
|
||||
>{{comment}}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
">
|
||||
<span style="white-space: pre-line">{{comment}}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td style="padding: 20px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a
|
||||
href="{{marketUrl}}"
|
||||
target="_blank"
|
||||
style="
|
||||
">
|
||||
<td style="padding: 20px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a href="{{marketUrl}}" target="_blank" style="
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
|
@ -377,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 comment</span
|
||||
></span
|
||||
>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div
|
||||
class="footer"
|
||||
style="
|
||||
">View comment</span></span>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
|
@ -417,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;
|
||||
|
@ -448,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;
|
||||
|
@ -463,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>
|
491
functions/src/email-templates/market-resolved-no-bets.html
Normal file
491
functions/src/email-templates/market-resolved-no-bets.html
Normal file
|
@ -0,0 +1,491 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Market resolved</title>
|
||||
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<table class="body-wrap" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
<td class="container" width="600" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
clear: both !important;
|
||||
margin: 0 auto;
|
||||
" valign="top">
|
||||
<div class="content" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
max-width: 600px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
margin: 0;
|
||||
border: 1px solid #e9e9e9;
|
||||
" bgcolor="#fff">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-wrap aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
" align="center" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 40px 0;
|
||||
text-align: left;
|
||||
" valign="top">
|
||||
<a href="https://manifold.markets" target="_blank">
|
||||
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
|
||||
alt="Manifold Markets" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 6px 0;
|
||||
text-align: left;
|
||||
" valign="top">
|
||||
{{creatorName}} asked
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" valign="top">
|
||||
<a href="{{url}}" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 24px;
|
||||
color: #000;
|
||||
line-height: 1.2em;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
margin: 0 0 0 0;
|
||||
color: #4337c9;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
">
|
||||
{{question}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 0px;
|
||||
" valign="top">
|
||||
<h2 class="aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
'Lucida Grande', sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 24px;
|
||||
color: #000;
|
||||
line-height: 1.2em;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin: 10px 0 0;
|
||||
" align="center">
|
||||
Resolved {{outcome}}
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
" align="center" valign="top">
|
||||
<table class="invoice" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
width: 80%;
|
||||
margin: 40px auto;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
" valign="top">
|
||||
Dear {{name}},
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
A market you were following has been resolved!
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
Thanks,
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
Manifold Team
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="padding: 10px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a href="{{url}}" target="_blank" style="
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
text-decoration: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
background-color: #11b981;
|
||||
border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
mso-border-alt: none;
|
||||
">
|
||||
<span style="
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
line-height: 120%;
|
||||
"><span style="
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 18.8px;
|
||||
">View market</span></span>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
">
|
||||
<table width="100%" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="aligncenter content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
vertical-align: top;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" align="center" valign="top">
|
||||
Questions? Come ask in
|
||||
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
">our Discord</a>! Or,
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,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 resolved</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 resolved</title>
|
||||
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
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"
|
||||
>
|
||||
{{creatorName}} asked
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
" valign="top">
|
||||
{{creatorName}} asked
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block"
|
||||
style="
|
||||
">
|
||||
<td class="content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -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"
|
||||
>
|
||||
Resolved {{outcome}}
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
" align="center">
|
||||
Resolved {{outcome}}
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="content-block aligncenter"
|
||||
style="
|
||||
">
|
||||
<td class="content-block aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
@ -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,138 +318,105 @@
|
|||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
"
|
||||
valign="top"
|
||||
>
|
||||
Dear {{name}},
|
||||
<br
|
||||
style="
|
||||
" valign="top">
|
||||
Dear {{name}},
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
A market you bet in has been resolved!
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
A market you bet in has been resolved!
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Your investment was
|
||||
<span style="font-weight: bold"
|
||||
>M$ {{investment}}</span
|
||||
>.
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
Your investment was
|
||||
<span style="font-weight: bold">{{investment}}</span>.
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Your payout is
|
||||
<span style="font-weight: bold"
|
||||
>M$ {{payout}}</span
|
||||
>.
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
Your payout is
|
||||
<span style="font-weight: bold">{{payout}}</span>.
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Thanks,
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
Thanks,
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
Manifold Team
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
Manifold Team
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
<br
|
||||
style="
|
||||
" />
|
||||
<br style="
|
||||
font-family: 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
style="
|
||||
" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
>
|
||||
<td style="padding: 10px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a
|
||||
href="{{url}}"
|
||||
target="_blank"
|
||||
style="
|
||||
">
|
||||
<td style="padding: 10px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a href="{{url}}" target="_blank" style="
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
|
@ -545,38 +434,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;
|
||||
|
@ -585,28 +465,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;
|
||||
|
@ -616,14 +488,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;
|
||||
|
@ -631,39 +498,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>
|
354
functions/src/email-templates/new-market-from-followed-user.html
Normal file
354
functions/src/email-templates/new-market-from-followed-user.html
Normal file
|
@ -0,0 +1,354 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>New market from {{creatorName}}</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width: 480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
[owa] .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width: 480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing: normal; background-color: #f4f4f4">
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:550px;">
|
||||
|
||||
<a href="https://manifold.markets" target="_blank">
|
||||
|
||||
<img alt="banner logo" height="auto"
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
||||
title="" width="550">
|
||||
|
||||
</a>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
margin: 0px auto;
|
||||
max-width: 600px;
|
||||
">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background: #ffffff; background-color: #ffffff; width: 100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 20px 0px 0px 0px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align: top" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
</span>Hi {{name}},</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
{{creatorName}}, (who you're following) just created a new market, check it out!</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
|
||||
<a href="{{questionUrl}}">
|
||||
<img alt="{{questionTitle}}" width="375" height="200"
|
||||
style="border: 1px solid #4337c9;" src="{{questionImgSrc}}">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="border-radius: 4px;" bgcolor="#4337c9">
|
||||
<a href="{{questionUrl}}" target="_blank"
|
||||
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
|
||||
View market
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 600px">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 0 0 20px 0;
|
||||
text-align: center;
|
||||
">
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 600px">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 20px 0px 20px 0px;
|
||||
text-align: center;
|
||||
">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding: 0">
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
">
|
||||
<div style="
|
||||
font-family: Ubuntu, Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 11px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
color: #000000;
|
||||
">
|
||||
<p style="margin: 10px 0">
|
||||
This e-mail has been sent to
|
||||
{{name}},
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
397
functions/src/email-templates/new-unique-bettor.html
Normal file
397
functions/src/email-templates/new-unique-bettor.html
Normal file
|
@ -0,0 +1,397 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>New unique traders on your market</title>
|
||||
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<table class="body-wrap" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
<td class="container" width="600" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
clear: both !important;
|
||||
margin: 0 auto;
|
||||
" valign="top">
|
||||
<div class="content" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
max-width: 600px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
margin: 0;
|
||||
border: 1px solid #e9e9e9;
|
||||
" bgcolor="#fff">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-wrap aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
" align="center" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 0px 0;
|
||||
text-align: left;
|
||||
" valign="top">
|
||||
<a href="https://manifold.markets" target="_blank">
|
||||
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
|
||||
alt="Manifold Markets" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
</span>Hi {{name}},</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
Your market <a href='{{marketUrl}}'>{{marketTitle}}</a> just got its first trade from a user!
|
||||
<br/>
|
||||
<br/>
|
||||
We sent you a <span style='color: #11b981'>{{bonusString}}</span> bonus for
|
||||
creating a market that appeals to others, and we'll do so for each new trader.
|
||||
<br/>
|
||||
<br/>
|
||||
Keep up the good work and check out your newest trader below!
|
||||
</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
" align="center" valign="top">
|
||||
<table class="invoice" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
width: 80%;
|
||||
margin: 40px auto;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor1AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor1Name}}</span>
|
||||
{{bet1Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="padding: 20px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a href="{{marketUrl}}" target="_blank" style="
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
text-decoration: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
background-color: #11b981;
|
||||
border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
mso-border-alt: none;
|
||||
">
|
||||
<span style="
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
line-height: 120%;
|
||||
"><span style="
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 18.8px;
|
||||
">View market</span></span>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
">
|
||||
<table width="100%" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="aligncenter content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
vertical-align: top;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" align="center" valign="top">
|
||||
Questions? Come ask in
|
||||
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
">our Discord</a>! Or,
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
501
functions/src/email-templates/new-unique-bettors.html
Normal file
501
functions/src/email-templates/new-unique-bettors.html
Normal file
|
@ -0,0 +1,501 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>New unique traders on your market</title>
|
||||
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<table class="body-wrap" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
background-color: #f6f6f6;
|
||||
margin: 0;
|
||||
" bgcolor="#f6f6f6">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
<td class="container" width="600" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
clear: both !important;
|
||||
margin: 0 auto;
|
||||
" valign="top">
|
||||
<div class="content" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
max-width: 600px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
margin: 0;
|
||||
border: 1px solid #e9e9e9;
|
||||
" bgcolor="#fff">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-wrap aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
" align="center" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
padding: 0 0 0px 0;
|
||||
text-align: left;
|
||||
" valign="top">
|
||||
<a href="https://manifold.markets" target="_blank">
|
||||
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
|
||||
alt="Manifold Markets" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
</span>Hi {{name}},</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
Your market <a href='{{marketUrl}}'>{{marketTitle}}</a> has attracted {{totalPredictors}} total traders!
|
||||
<br/>
|
||||
<br/>
|
||||
We sent you a <span style='color: #11b981'>{{bonusString}}</span> bonus for getting {{newPredictors}} new traders,
|
||||
and we'll continue to do so for each new trader, (although we won't send you any more emails about it for this market).
|
||||
<br/>
|
||||
<br/>
|
||||
Keep up the good work and check out your newest traders below!
|
||||
</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="content-block aligncenter" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
" align="center" valign="top">
|
||||
<table class="invoice" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
width: 80%;
|
||||
margin: 40px auto;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor1AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor1Name}}</span>
|
||||
{{bet1Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor2AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor2Name}}</span>
|
||||
{{bet2Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor3AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor3Name}}</span>
|
||||
{{bet3Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor4AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor4Name}}</span>
|
||||
{{bet4Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
" valign="top">
|
||||
<div>
|
||||
<img src="{{bettor5AvatarUrl}}" width="30" height="30" style="
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
" alt="" />
|
||||
<span style="font-weight: bold">{{bettor5Name}}</span>
|
||||
{{bet5Description}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
">
|
||||
<td style="padding: 20px 0 0 0; margin: 0">
|
||||
<div align="center">
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
|
||||
<a href="{{marketUrl}}" target="_blank" style="
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
text-decoration: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
background-color: #11b981;
|
||||
border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
mso-border-alt: none;
|
||||
">
|
||||
<span style="
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
line-height: 120%;
|
||||
"><span style="
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 18.8px;
|
||||
">View market</span></span>
|
||||
</a>
|
||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
">
|
||||
<table width="100%" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<tr style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
">
|
||||
<td class="aligncenter content-block" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
vertical-align: top;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0 0 20px;
|
||||
" align="center" valign="top">
|
||||
Questions? Come ask in
|
||||
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
margin: 0;
|
||||
">our Discord</a>! Or,
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
" valign="top"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,519 +1,316 @@
|
|||
<!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>7th Day Anniversary Gift!</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style type="text/css">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>Manifold Markets 7th Day Anniversary Gift!</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml> </noscript
|
||||
>z
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width: 480px) {
|
||||
.mj-column-per-100 {
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
</style>
|
||||
<style type="text/css">
|
||||
[owa] .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width: 480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
<body style="word-spacing: normal; background-color: #f4f4f4">
|
||||
<div style="background-color: #f4f4f4">
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div
|
||||
style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
margin: 0px auto;
|
||||
max-width: 600px;
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="background: #ffffff; background-color: #ffffff; width: 100%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 0px 0px 0px 0px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
padding-top: 0px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div
|
||||
class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="vertical-align: top"
|
||||
width="100%"
|
||||
>
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing:normal;background-color:#F4F4F4;">
|
||||
<div style="background-color:#F4F4F4;">
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="direction:ltr;font-size:0px;padding:0px 0px 0px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:0px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 0px 25px 0px 25px;
|
||||
padding-top: 0px;
|
||||
padding-right: 25px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 25px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 550px">
|
||||
<a
|
||||
href="https://manifold.markets/home"
|
||||
target="_blank"
|
||||
><img
|
||||
alt=""
|
||||
height="auto"
|
||||
src="https://03jlj.mjt.lu/img/03jlj/b/u71/sjvu.gif"
|
||||
style="
|
||||
border: none;
|
||||
display: block;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
"
|
||||
width="550"
|
||||
/></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
letter-spacing: normal;
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
color: #000000;
|
||||
"
|
||||
>
|
||||
<p
|
||||
class="text-build-content"
|
||||
style="
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
"
|
||||
data-testid="4XoHRGw1Y"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
color: #000000;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 18px;
|
||||
"
|
||||
>Hopefully you haven't gambled all your M$
|
||||
away already... but if you have I bring good
|
||||
news! Click the link below to recieve a one time
|
||||
gift of M$ 500 to your account!</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px 25px 25px;
|
||||
padding-top: 10px;
|
||||
padding-right: 25px;
|
||||
padding-bottom: 25px;
|
||||
padding-left: 25px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 550px">
|
||||
<a href="{{manalink}}" target="_blank">
|
||||
<img
|
||||
alt="Get M$500"
|
||||
height="auto"
|
||||
src="https://03jlj.mjt.lu/img/03jlj/b/u71/sjgt.png"
|
||||
style="
|
||||
border: none;
|
||||
display: block;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
"
|
||||
width="550"
|
||||
/></a>
|
||||
<< /td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
letter-spacing: normal;
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
color: #000000;
|
||||
"
|
||||
>
|
||||
<p
|
||||
class="text-build-content"
|
||||
style="
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
margin-top: 10px;
|
||||
"
|
||||
data-testid="3Q8BP69fq"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
color: #000000;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 18px;
|
||||
"
|
||||
>If you are still engaging with our markets then
|
||||
at this point you might as well join our </span
|
||||
><a
|
||||
class="link-build-content"
|
||||
style="color: inherit; text-decoration: none"
|
||||
target="_blank"
|
||||
href="https://discord.gg/VARzUpyCSa"
|
||||
><span
|
||||
style="
|
||||
color: #0c21bf;
|
||||
font-family: Arial;
|
||||
font-size: 18px;
|
||||
"
|
||||
><u>Discord server</u></span
|
||||
><span
|
||||
style="
|
||||
color: #000000;
|
||||
font-family: Arial;
|
||||
font-size: 18px;
|
||||
"
|
||||
><u>.</u>
|
||||
</span></a
|
||||
><span
|
||||
style="
|
||||
color: #000000;
|
||||
font-family: Arial;
|
||||
font-size: 18px;
|
||||
"
|
||||
>You can always leave if you dont like it but
|
||||
I'd be willing to make a market betting
|
||||
you'll stay.</span
|
||||
>
|
||||
</p>
|
||||
<p
|
||||
class="text-build-content"
|
||||
data-testid="3Q8BP69fq"
|
||||
style="margin: 10px 0"
|
||||
></p>
|
||||
<br />
|
||||
<p
|
||||
class="text-build-content"
|
||||
data-testid="3Q8BP69fq"
|
||||
style="margin: 10px 0"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
color: #000000;
|
||||
font-family: Arial;
|
||||
font-size: 18px;
|
||||
"
|
||||
>Cheers,</span
|
||||
>
|
||||
</p>
|
||||
<p
|
||||
class="text-build-content"
|
||||
data-testid="3Q8BP69fq"
|
||||
style="margin: 10px 0"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
color: #000000;
|
||||
font-family: Arial;
|
||||
font-size: 18px;
|
||||
"
|
||||
>David from Manifold</span
|
||||
>
|
||||
</p>
|
||||
<p
|
||||
class="text-build-content"
|
||||
data-testid="3Q8BP69fq"
|
||||
style="margin: 10px 0; margin-bottom: 10px"
|
||||
></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:550px;"><a href="https://manifold.markets/home" target="_blank"><img
|
||||
alt="" height="auto" src="https://i.imgur.com/8EP8Y8q.gif"
|
||||
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
||||
width="550"></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 600px">
|
||||
<table
|
||||
align="center"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 100%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 20px 0px 20px 0px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div
|
||||
class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
width="100%"
|
||||
>
|
||||
<tbody>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
Hi {{name}},</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">Thanks for
|
||||
using Manifold Markets. Running low
|
||||
on mana (M$)? Click the link below to receive a one time gift of M$500!</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="border-radius: 2px;" bgcolor="#4337c9">
|
||||
<a href="{{manalink}}" target="_blank"
|
||||
style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:bold;display: inline-block;">
|
||||
Claim M$500
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
|
||||
data-testid="3Q8BP69fq"><span style="font-family:Arial, sans-serif;font-size:18px;">Did
|
||||
you know, besides making correct predictions, there are
|
||||
plenty of other ways to earn mana?</span></p>
|
||||
<ul>
|
||||
<li style="line-height:23px;"><span
|
||||
style="font-family:Arial, sans-serif;font-size:18px;">Predicting
|
||||
consecutive days to earn streak rewards</span></li>
|
||||
<li style="line-height:23px;"><span
|
||||
style="font-family:Arial, sans-serif;font-size:18px;">Receiving
|
||||
tips on comments and markets</span></li>
|
||||
<li style="line-height:23px;"><span
|
||||
style="font-family:Arial, sans-serif;font-size:18px;">Unique
|
||||
trader bonus for each user who trades on your
|
||||
markets</span></li>
|
||||
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
|
||||
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||
target="_blank" href="https://manifold.markets/referrals"><span
|
||||
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Referring
|
||||
friends</u></span></a></span></li>
|
||||
<li style="line-height:23px;"><a class="link-build-content"
|
||||
style="color:inherit;; text-decoration: none;" target="_blank"
|
||||
href="https://manifold.markets/group/bugs?s=most-traded"><span
|
||||
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Reporting
|
||||
bugs</u></span></a><span style="font-family:Arial, sans-serif;font-size:18px;">
|
||||
and </span><a class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||
target="_blank"
|
||||
href="https://manifold.markets/group/manifold-features-25bad7c7792e/chat?s=most-traded"><span
|
||||
style="color:#55575d;font-family:Arial;font-size:18px;"><u>giving
|
||||
feedback</u></span></a></li>
|
||||
</ul>
|
||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"> </p>
|
||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
||||
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span>
|
||||
</p>
|
||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
||||
style="color:#000000;font-family:Arial;font-size:18px;">David
|
||||
from Manifold</span></p>
|
||||
<p class="text-build-content" data-testid="3Q8BP69fq"
|
||||
style="margin: 10px 0; margin-bottom: 10px;"> </p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0px 20px 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding: 0">
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
width="100%"
|
||||
>
|
||||
<td style="vertical-align:top;padding:0;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="
|
||||
<tr>
|
||||
<td align="center" style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Arial, sans-serif;
|
||||
">
|
||||
<div style="
|
||||
font-family: Ubuntu, Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 11px;
|
||||
letter-spacing: normal;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
color: #000000;
|
||||
"
|
||||
>
|
||||
<p style="margin: 10px 0">
|
||||
This e-mail has been sent to {{name}},
|
||||
<a
|
||||
href="{{unsubscribeLink}}"
|
||||
style="
|
||||
">
|
||||
<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</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -214,10 +214,12 @@
|
|||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
|
||||
<p style="margin: 10px 0;">This e-mail has been sent
|
||||
to {{name}}, <a href="{{unsubscribeLink}}"
|
||||
style="color:inherit;text-decoration:none;"
|
||||
target="_blank">click here to
|
||||
unsubscribe</a>.</p>
|
||||
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>
|
||||
|
|
|
@ -0,0 +1,411 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>Weekly Portfolio Update on Manifold</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
font-family:"Readex Pro", Helvetica, sans-serif;
|
||||
}
|
||||
table { margin: 0 auto; }
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0;
|
||||
mso-table-rspace: 0;
|
||||
}
|
||||
th {color:#000000; font-size:17px;}
|
||||
th, td {padding: 10px; }
|
||||
td{ font-size: 17px}
|
||||
th, td { vertical-align: center; text-align: left }
|
||||
a { vertical-align: center; text-align: left}
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
p.change{
|
||||
margin: 0; vertical-align: middle;font-size:16px;display: inline; padding: 2px; border-radius: 5px; width: 20px; text-align: right;
|
||||
}
|
||||
p.prob{
|
||||
font-size: 22px;display: inline; vertical-align: middle; font-weight: bold; width: 50px;
|
||||
}
|
||||
a.question{
|
||||
font-size: 18px;display: inline; vertical-align: middle;
|
||||
}
|
||||
td.question{
|
||||
vertical-align: middle; padding-bottom: 15px; text-align: left;
|
||||
}
|
||||
td.probs{
|
||||
text-align: right; padding-left: 10px; min-width: 115px
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width: 480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
[owa] .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width: 480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing: normal; background-color: #f4f4f4">
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:550px;">
|
||||
|
||||
<a href="https://manifold.markets" target="_blank">
|
||||
|
||||
<img alt="banner logo" height="auto"
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
||||
title="" width="550">
|
||||
|
||||
</a>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
margin: 0px auto;
|
||||
max-width: 600px;
|
||||
">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background: #ffffff; background-color: #ffffff; width: 100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 20px 0px 0px 0px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align: top; margin-bottom: 30px" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
</span>Hi {{name}},</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 0px;"
|
||||
data-testid="4XoHRGw1Y">
|
||||
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
We ran the numbers and here's how you did this past week!
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!--/ show 5 columns with headers titled: Investment value, 7-day change, current balance, tips received, and markets made/-->
|
||||
<tr>
|
||||
<tr>
|
||||
<th style='font-size: 22px; text-align: center'>
|
||||
Profit
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style='padding-bottom: 30px; text-align: center'>
|
||||
<p class='change' style='font-size: 24px; padding:4px; {{profit_style}}'>
|
||||
{{profit}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:10px 20px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px; ">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style='width: 170px'>
|
||||
🔥 Prediction streak
|
||||
</th>
|
||||
<td>
|
||||
{{prediction_streak}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
💸 Tips received
|
||||
</th>
|
||||
<td>
|
||||
{{tips_received}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
📈 Markets traded
|
||||
</th>
|
||||
<td>
|
||||
{{markets_traded}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
❓ Markets created
|
||||
</th>
|
||||
|
||||
<td>
|
||||
{{markets_created}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='width: 55px'>
|
||||
🥳 Traders attracted
|
||||
</th>
|
||||
<td>
|
||||
{{unique_bettors}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 600px">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 0 0 20px 0;
|
||||
text-align: center;
|
||||
">
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 600px">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 20px 0px 20px 0px;
|
||||
text-align: center;
|
||||
">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding: 0">
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
">
|
||||
<div style="
|
||||
font-family: Ubuntu, Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 11px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
color: #000000;
|
||||
">
|
||||
<p style="margin: 10px 0">
|
||||
This e-mail has been sent to
|
||||
{{name}},
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
510
functions/src/email-templates/weekly-portfolio-update.html
Normal file
510
functions/src/email-templates/weekly-portfolio-update.html
Normal file
|
@ -0,0 +1,510 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>Weekly Portfolio Update on Manifold</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
font-family:"Readex Pro", Helvetica, sans-serif;
|
||||
}
|
||||
table { margin: 0 auto; }
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0;
|
||||
mso-table-rspace: 0;
|
||||
}
|
||||
th {color:#000000; font-size:17px;}
|
||||
th, td {padding: 10px; }
|
||||
td{ font-size: 17px}
|
||||
th, td { vertical-align: center; text-align: left }
|
||||
a { vertical-align: center; text-align: left}
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
p.change{
|
||||
margin: 0; vertical-align: middle;font-size:16px;display: inline; padding: 2px; border-radius: 5px; width: 20px; text-align: right;
|
||||
}
|
||||
p.prob{
|
||||
font-size: 22px;display: inline; vertical-align: middle; font-weight: bold; width: 50px;
|
||||
}
|
||||
a.question{
|
||||
font-size: 18px;display: inline; vertical-align: middle;
|
||||
}
|
||||
td.question{
|
||||
vertical-align: middle; padding-bottom: 15px; text-align: left;
|
||||
}
|
||||
td.probs{
|
||||
text-align: right; padding-left: 10px; min-width: 115px
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width: 480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
[owa] .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width: 480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing: normal; background-color: #f4f4f4">
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:550px;">
|
||||
|
||||
<a href="https://manifold.markets" target="_blank">
|
||||
|
||||
<img alt="banner logo" height="auto"
|
||||
src="https://manifold.markets/logo-banner.png"
|
||||
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
||||
title="" width="550">
|
||||
|
||||
</a>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
margin: 0px auto;
|
||||
max-width: 600px;
|
||||
">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background: #ffffff; background-color: #ffffff; width: 100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 20px 0px 0px 0px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align: top" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
</span>Hi {{name}},</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 0px;"
|
||||
data-testid="4XoHRGw1Y">
|
||||
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
We ran the numbers and here's how you did this past week!
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!--/ show 5 columns with headers titled: Investment value, 7-day change, current balance, tips received, and markets made/-->
|
||||
<tr>
|
||||
<tr>
|
||||
<th style='font-size: 22px; text-align: center'>
|
||||
Profit
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style='padding-bottom: 30px; text-align: center'>
|
||||
<p class='change' style='font-size: 24px; padding:4px; {{profit_style}}'>
|
||||
{{profit}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:10px 20px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px; ">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style='width: 170px'>
|
||||
🔥 Prediction streak
|
||||
</th>
|
||||
<td>
|
||||
{{prediction_streak}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
💸 Tips received
|
||||
</th>
|
||||
<td>
|
||||
{{tips_received}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
📈 Markets traded
|
||||
</th>
|
||||
<td>
|
||||
{{markets_traded}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
❓ Markets created
|
||||
</th>
|
||||
|
||||
<td>
|
||||
{{markets_created}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='width: 55px'>
|
||||
🥳 Traders attracted
|
||||
</th>
|
||||
<td>
|
||||
{{unique_bettors}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:10px 25px;padding-top:20px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content"
|
||||
style="line-height: 24px; margin: 10px 0; margin-top: 20px; margin-bottom: 20px;"
|
||||
data-testid="4XoHRGw1Y">
|
||||
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
And here's some recent changes in your investments:
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0; padding-left:10px;padding-top:10px;padding-bottom:0;word-break:break-word;">
|
||||
<table role="presentation">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class='question'>
|
||||
<a class='question' href='{{question1Url}}'>
|
||||
{{question1Title}}
|
||||
<!-- Will the US economy recover from the pandemic?-->
|
||||
</a>
|
||||
</td>
|
||||
<td class='probs'>
|
||||
<p class='prob'>
|
||||
{{question1Prob}}
|
||||
<!-- 9.9%-->
|
||||
<p class='change' style='{{question1ChangeStyle}}'>
|
||||
{{question1Change}}
|
||||
<!-- +17%-->
|
||||
</p>
|
||||
</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class='question'>
|
||||
<a class='question' href='{{question2Url}}'>
|
||||
{{question2Title}}
|
||||
<!-- Will the US economy recover from the pandemic? blah blah blah-->
|
||||
</a>
|
||||
</td>
|
||||
<td class='probs'>
|
||||
<p class='prob'>
|
||||
{{question2Prob}}
|
||||
<!-- 99.9%-->
|
||||
<p class='change' style='{{question2ChangeStyle}}'>
|
||||
{{question2Change}}
|
||||
<!-- +7%-->
|
||||
</p>
|
||||
</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<!-- <td style="{{investment_value_style}}">-->
|
||||
<td class='question'>
|
||||
<a class='question' href='{{question3Url}}'>
|
||||
{{question3Title}}
|
||||
<!-- Will the US economy recover from the pandemic?-->
|
||||
</a>
|
||||
</td>
|
||||
<td class='probs'>
|
||||
<p class='prob'>
|
||||
{{question3Prob}}
|
||||
<!-- 99.9%-->
|
||||
<p class='change' style='{{question3ChangeStyle}}'>
|
||||
{{question3Change}}
|
||||
<!-- +17%-->
|
||||
</p>
|
||||
</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<!-- <td style="{{investment_value_style}}">-->
|
||||
<td class='question'>
|
||||
<a class='question' href='{{question4Url}}'>
|
||||
{{question4Title}}
|
||||
<!-- Will the US economy recover from the pandemic?-->
|
||||
</a>
|
||||
</td>
|
||||
<td class='probs'>
|
||||
<p class='prob'>
|
||||
{{question4Prob}}
|
||||
<!-- 99.9%-->
|
||||
<p class='change' style='{{question4ChangeStyle}}'>
|
||||
{{question4Change}}
|
||||
<!-- +17%-->
|
||||
</p>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 600px">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 0 0 20px 0;
|
||||
text-align: center;
|
||||
">
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 600px">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 20px 0px 20px 0px;
|
||||
text-align: center;
|
||||
">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding: 0">
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
">
|
||||
<div style="
|
||||
font-family: Ubuntu, Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 11px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
color: #000000;
|
||||
">
|
||||
<p style="margin: 10px 0">
|
||||
This e-mail has been sent to
|
||||
{{name}},
|
||||
<a href="{{unsubscribeUrl}}" style="
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
" target="_blank">click here to unsubscribe from this type of notification</a>.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -107,19 +107,12 @@
|
|||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="font-size:0px;padding:0px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:550px;"><a href="https://manifold.markets/home" target="_blank"><img
|
||||
alt="" height="auto" src="https://03jlj.mjt.lu/img/03jlj/b/urx/sjtz.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 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>
|
||||
|
@ -144,7 +137,7 @@
|
|||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||
data-testid="4XoHRGw1Y"><span
|
||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||
Welcome! Manifold Markets is a play-money prediction market platform where you can bet on
|
||||
Welcome! Manifold Markets is a play-money prediction market platform where you can predict
|
||||
anything, from elections to Elon Musk to scientific papers to the NBA. </span></p>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -175,9 +168,9 @@
|
|||
<td>
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="border-radius: 2px;" bgcolor="#4337c9">
|
||||
<td style="border-radius: 4px;" bgcolor="#4337c9">
|
||||
<a href="https://manifold.markets" target="_blank"
|
||||
style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:bold;display: inline-block;">
|
||||
style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
|
||||
Explore markets
|
||||
</a>
|
||||
</td>
|
||||
|
@ -191,6 +184,46 @@
|
|||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
|
||||
data-testid="3Q8BP69fq"><span style="font-family:Arial, sans-serif;font-size:18px;">Did
|
||||
you know, besides betting and making predictions, you can also <a
|
||||
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||
target="_blank" href="https://manifold.markets/create"><span
|
||||
style="color:#55575d;font-family:Arial;font-size:18px;font-weight: bold;"><u>create
|
||||
your
|
||||
own
|
||||
market</u></span></a> on
|
||||
any question you care about?</span></p>
|
||||
|
||||
<p>More resources:</p>
|
||||
|
||||
<ul>
|
||||
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
|
||||
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||
target="_blank" href="https://manifold.markets/about"><span
|
||||
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Learn more</u></span></a>
|
||||
about Manifold and how our markets work</span></li>
|
||||
|
||||
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
|
||||
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||
target="_blank" href="https://manifold.markets/referrals"><span
|
||||
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Refer
|
||||
your friends</u></span></a> and earn M$250 for each signup!</span></li>
|
||||
|
||||
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
|
||||
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
||||
target="_blank" href="https://discord.com/invite/eHQBNBqXuh"><span
|
||||
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Join our Discord
|
||||
chat</u></span></a></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left"
|
||||
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
|
||||
|
@ -253,9 +286,12 @@
|
|||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
|
||||
<p style="margin: 10px 0;">This e-mail has been sent to {{name}}, <a
|
||||
href="{{unsubscribeLink}}” style=" color:inherit;text-decoration:none;"
|
||||
target="_blank">click here to unsubscribe</a>.</p>
|
||||
<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>
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { DOMAIN } from '../../common/envs/constants'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { DPM_CREATOR_FEE } from '../../common/fees'
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
import {
|
||||
formatLargeNumber,
|
||||
|
@ -14,15 +11,20 @@ import {
|
|||
import { getValueFromBucket } from '../../common/calculate-dpm'
|
||||
import { formatNumericProbability } from '../../common/pseudo-numeric'
|
||||
|
||||
import { sendTemplateEmail } from './send-email'
|
||||
import { getPrivateUser, getUser } from './utils'
|
||||
import { getFunctionUrl } from '../../common/api'
|
||||
import { richTextToString } from '../../common/util/parse'
|
||||
|
||||
const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe')
|
||||
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
||||
import { contractUrl, getUser, log } from './utils'
|
||||
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
|
||||
import { notification_reason_types } from '../../common/notification'
|
||||
import { Dictionary } from 'lodash'
|
||||
import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences'
|
||||
import {
|
||||
PerContractInvestmentsData,
|
||||
OverallPerformanceData,
|
||||
} from './weekly-portfolio-emails'
|
||||
|
||||
export const sendMarketResolutionEmail = async (
|
||||
userId: string,
|
||||
reason: notification_reason_types,
|
||||
privateUser: PrivateUser,
|
||||
investment: number,
|
||||
payout: number,
|
||||
creator: User,
|
||||
|
@ -32,15 +34,13 @@ export const sendMarketResolutionEmail = async (
|
|||
resolutionProbability?: number,
|
||||
resolutions?: { [outcome: string]: number }
|
||||
) => {
|
||||
const privateUser = await getPrivateUser(userId)
|
||||
if (
|
||||
!privateUser ||
|
||||
privateUser.unsubscribedFromResolutionEmails ||
|
||||
!privateUser.email
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
)
|
||||
return
|
||||
if (!privateUser || !privateUser.email || !sendToEmail) return
|
||||
|
||||
const user = await getUser(userId)
|
||||
const user = await getUser(privateUser.id)
|
||||
if (!user) return
|
||||
|
||||
const outcome = toDisplayResolution(
|
||||
|
@ -53,12 +53,15 @@ export const sendMarketResolutionEmail = async (
|
|||
const subject = `Resolved ${outcome}: ${contract.question}`
|
||||
|
||||
const creatorPayoutText =
|
||||
userId === creator.id
|
||||
creatorPayout >= 1 && privateUser.id === creator.id
|
||||
? ` (plus ${formatMoney(creatorPayout)} in commissions)`
|
||||
: ''
|
||||
|
||||
const emailType = 'market-resolved'
|
||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
const correctedInvestment =
|
||||
Number.isNaN(investment) || investment < 0 ? 0 : investment
|
||||
const displayedInvestment = formatMoney(correctedInvestment)
|
||||
|
||||
const displayedPayout = formatMoney(payout)
|
||||
|
||||
const templateData: market_resolved_template = {
|
||||
userId: user.id,
|
||||
|
@ -66,20 +69,19 @@ export const sendMarketResolutionEmail = async (
|
|||
creatorName: creator.name,
|
||||
question: contract.question,
|
||||
outcome,
|
||||
investment: `${Math.floor(investment)}`,
|
||||
payout: `${Math.floor(payout)}${creatorPayoutText}`,
|
||||
investment: displayedInvestment,
|
||||
payout: displayedPayout + creatorPayoutText,
|
||||
url: `https://${DOMAIN}/${creator.username}/${contract.slug}`,
|
||||
unsubscribeUrl,
|
||||
}
|
||||
|
||||
// Modify template here:
|
||||
// https://app.mailgun.com/app/sending/domains/mg.manifold.markets/templates/edit/market-resolved/initial
|
||||
// Mailgun username: james@mantic.markets
|
||||
|
||||
await sendTemplateEmail(
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
'market-resolved',
|
||||
correctedInvestment === 0 ? 'market-resolved-no-bets' : 'market-resolved',
|
||||
templateData
|
||||
)
|
||||
}
|
||||
|
@ -116,7 +118,9 @@ const toDisplayResolution = (
|
|||
}
|
||||
|
||||
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
||||
const { resolutionValue } = contract
|
||||
const { resolution, resolutionValue } = contract
|
||||
|
||||
if (resolution === 'CANCEL') return 'N/A'
|
||||
|
||||
return resolutionValue
|
||||
? formatLargeNumber(resolutionValue)
|
||||
|
@ -146,19 +150,21 @@ export const sendWelcomeEmail = async (
|
|||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name, id: userId } = user
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const emailType = 'generic'
|
||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
const { unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'onboarding_flow'
|
||||
)
|
||||
|
||||
await sendTemplateEmail(
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Welcome to Manifold Markets!',
|
||||
'welcome',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeLink,
|
||||
unsubscribeUrl,
|
||||
},
|
||||
{
|
||||
from: 'David from Manifold <david@manifold.markets>',
|
||||
|
@ -166,30 +172,64 @@ export const sendWelcomeEmail = async (
|
|||
)
|
||||
}
|
||||
|
||||
export const sendPersonalFollowupEmail = async (
|
||||
user: User,
|
||||
privateUser: PrivateUser,
|
||||
sendTime: string
|
||||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const emailBody = `Hi ${firstName},
|
||||
|
||||
Thanks for signing up! I'm one of the cofounders of Manifold Markets, and was wondering how you've found your experience on the platform so far?
|
||||
|
||||
If you haven't already, I encourage you to try creating your own prediction market (https://manifold.markets/create) and joining our Discord chat (https://discord.com/invite/eHQBNBqXuh).
|
||||
|
||||
Feel free to reply to this email with any questions or concerns you have.
|
||||
|
||||
Cheers,
|
||||
|
||||
James
|
||||
Cofounder of Manifold Markets
|
||||
https://manifold.markets
|
||||
`
|
||||
|
||||
await sendTextEmail(
|
||||
privateUser.email,
|
||||
'How are you finding Manifold?',
|
||||
emailBody,
|
||||
{
|
||||
from: 'James from Manifold <james@manifold.markets>',
|
||||
'o:deliverytime': sendTime,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const sendOneWeekBonusEmail = async (
|
||||
user: User,
|
||||
privateUser: PrivateUser
|
||||
) => {
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromGenericEmails
|
||||
)
|
||||
return
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name, id: userId } = user
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const emailType = 'generic'
|
||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'onboarding_flow'
|
||||
)
|
||||
if (!sendToEmail) return
|
||||
|
||||
await sendTemplateEmail(
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Manifold Markets one week anniversary gift',
|
||||
'one-week',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeLink,
|
||||
unsubscribeUrl,
|
||||
manalink: 'https://manifold.markets/link/lj4JbBvE',
|
||||
},
|
||||
{
|
||||
|
@ -198,30 +238,56 @@ export const sendOneWeekBonusEmail = async (
|
|||
)
|
||||
}
|
||||
|
||||
export const sendCreatorGuideEmail = async (
|
||||
user: User,
|
||||
privateUser: PrivateUser,
|
||||
sendTime: string
|
||||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'onboarding_flow'
|
||||
)
|
||||
if (!sendToEmail) return
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Create your own prediction market',
|
||||
'creating-market',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeUrl,
|
||||
},
|
||||
{
|
||||
from: 'David from Manifold <david@manifold.markets>',
|
||||
'o:deliverytime': sendTime,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const sendThankYouEmail = async (
|
||||
user: User,
|
||||
privateUser: PrivateUser
|
||||
) => {
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromGenericEmails
|
||||
)
|
||||
return
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { name, id: userId } = user
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'thank_you_for_purchases'
|
||||
)
|
||||
|
||||
const emailType = 'generic'
|
||||
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
await sendTemplateEmail(
|
||||
if (!sendToEmail) return
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Thanks for your Manifold purchase',
|
||||
'thank-you',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeLink,
|
||||
unsubscribeUrl,
|
||||
},
|
||||
{
|
||||
from: 'David from Manifold <david@manifold.markets>',
|
||||
|
@ -230,70 +296,57 @@ export const sendThankYouEmail = async (
|
|||
}
|
||||
|
||||
export const sendMarketCloseEmail = async (
|
||||
reason: notification_reason_types,
|
||||
user: User,
|
||||
privateUser: PrivateUser,
|
||||
contract: Contract
|
||||
) => {
|
||||
if (
|
||||
!privateUser ||
|
||||
privateUser.unsubscribedFromResolutionEmails ||
|
||||
!privateUser.email
|
||||
)
|
||||
return
|
||||
if (!privateUser.email) return
|
||||
|
||||
const { username, name, id: userId } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
const { question, slug, volume, mechanism, collectedFees } = contract
|
||||
const { question, slug, volume } = contract
|
||||
|
||||
const url = `https://${DOMAIN}/${username}/${slug}`
|
||||
const emailType = 'market-resolve'
|
||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
await sendTemplateEmail(
|
||||
// We ignore if they were able to unsubscribe from market close emails, this is a necessary email
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
'Your market has closed',
|
||||
'market-close',
|
||||
{
|
||||
question,
|
||||
url,
|
||||
unsubscribeUrl,
|
||||
unsubscribeUrl: '',
|
||||
userId,
|
||||
name: firstName,
|
||||
volume: formatMoney(volume),
|
||||
creatorFee:
|
||||
mechanism === 'dpm-2'
|
||||
? `${DPM_CREATOR_FEE * 100}% of the profits`
|
||||
: formatMoney(collectedFees.creatorFee),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const sendNewCommentEmail = async (
|
||||
userId: string,
|
||||
reason: notification_reason_types,
|
||||
privateUser: PrivateUser,
|
||||
commentCreator: User,
|
||||
contract: Contract,
|
||||
comment: Comment,
|
||||
commentText: string,
|
||||
commentId: string,
|
||||
bet?: Bet,
|
||||
answerText?: string,
|
||||
answerId?: string
|
||||
) => {
|
||||
const privateUser = await getPrivateUser(userId)
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromCommentEmails
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
)
|
||||
return
|
||||
if (!privateUser || !privateUser.email || !sendToEmail) return
|
||||
|
||||
const { question, creatorUsername, slug } = contract
|
||||
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}#${comment.id}`
|
||||
const emailType = 'market-comment'
|
||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
const { question } = contract
|
||||
const marketUrl = `https://${DOMAIN}/${contract.creatorUsername}/${contract.slug}#${commentId}`
|
||||
|
||||
const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
|
||||
const { content } = comment
|
||||
const text = richTextToString(content)
|
||||
|
||||
let betDescription = ''
|
||||
if (bet) {
|
||||
|
@ -307,9 +360,9 @@ export const sendNewCommentEmail = async (
|
|||
const from = `${commentorName} on Manifold <no-reply@manifold.markets>`
|
||||
|
||||
if (contract.outcomeType === 'FREE_RESPONSE' && answerId && answerText) {
|
||||
const answerNumber = `#${answerId}`
|
||||
const answerNumber = answerId ? `#${answerId}` : ''
|
||||
|
||||
await sendTemplateEmail(
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
'market-answer-comment',
|
||||
|
@ -318,7 +371,7 @@ export const sendNewCommentEmail = async (
|
|||
answerNumber,
|
||||
commentorName,
|
||||
commentorAvatarUrl: commentorAvatarUrl ?? '',
|
||||
comment: text,
|
||||
comment: commentText,
|
||||
marketUrl,
|
||||
unsubscribeUrl,
|
||||
betDescription,
|
||||
|
@ -332,14 +385,14 @@ export const sendNewCommentEmail = async (
|
|||
bet.outcome
|
||||
)}`
|
||||
}
|
||||
await sendTemplateEmail(
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
'market-comment',
|
||||
{
|
||||
commentorName,
|
||||
commentorAvatarUrl: commentorAvatarUrl ?? '',
|
||||
comment: text,
|
||||
comment: commentText,
|
||||
marketUrl,
|
||||
unsubscribeUrl,
|
||||
betDescription,
|
||||
|
@ -350,34 +403,31 @@ export const sendNewCommentEmail = async (
|
|||
}
|
||||
|
||||
export const sendNewAnswerEmail = async (
|
||||
answer: Answer,
|
||||
contract: Contract
|
||||
reason: notification_reason_types,
|
||||
privateUser: PrivateUser,
|
||||
name: string,
|
||||
text: string,
|
||||
contract: Contract,
|
||||
avatarUrl?: string
|
||||
) => {
|
||||
// Send to just the creator for now.
|
||||
const { creatorId: userId } = contract
|
||||
|
||||
const { creatorId } = contract
|
||||
// Don't send the creator's own answers.
|
||||
if (answer.userId === userId) return
|
||||
if (privateUser.id === creatorId) return
|
||||
|
||||
const privateUser = await getPrivateUser(userId)
|
||||
if (
|
||||
!privateUser ||
|
||||
!privateUser.email ||
|
||||
privateUser.unsubscribedFromAnswerEmails
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
)
|
||||
return
|
||||
if (!privateUser.email || !sendToEmail) return
|
||||
|
||||
const { question, creatorUsername, slug } = contract
|
||||
const { name, avatarUrl, text } = answer
|
||||
|
||||
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}`
|
||||
const emailType = 'market-answer'
|
||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||
|
||||
const subject = `New answer on ${question}`
|
||||
const from = `${name} <info@manifold.markets>`
|
||||
|
||||
await sendTemplateEmail(
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
'market-answer',
|
||||
|
@ -391,3 +441,195 @@ export const sendNewAnswerEmail = async (
|
|||
{ from }
|
||||
)
|
||||
}
|
||||
|
||||
export const sendInterestingMarketsEmail = async (
|
||||
user: User,
|
||||
privateUser: PrivateUser,
|
||||
contractsToSend: Contract[],
|
||||
deliveryTime?: string
|
||||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'trending_markets'
|
||||
)
|
||||
if (!sendToEmail) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
|
||||
await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
`${contractsToSend[0].question} & 5 more interesting markets on Manifold`,
|
||||
'interesting-markets',
|
||||
{
|
||||
name: firstName,
|
||||
unsubscribeUrl,
|
||||
|
||||
question1Title: contractsToSend[0].question,
|
||||
question1Link: contractUrl(contractsToSend[0]),
|
||||
question1ImgSrc: imageSourceUrl(contractsToSend[0]),
|
||||
question2Title: contractsToSend[1].question,
|
||||
question2Link: contractUrl(contractsToSend[1]),
|
||||
question2ImgSrc: imageSourceUrl(contractsToSend[1]),
|
||||
question3Title: contractsToSend[2].question,
|
||||
question3Link: contractUrl(contractsToSend[2]),
|
||||
question3ImgSrc: imageSourceUrl(contractsToSend[2]),
|
||||
question4Title: contractsToSend[3].question,
|
||||
question4Link: contractUrl(contractsToSend[3]),
|
||||
question4ImgSrc: imageSourceUrl(contractsToSend[3]),
|
||||
question5Title: contractsToSend[4].question,
|
||||
question5Link: contractUrl(contractsToSend[4]),
|
||||
question5ImgSrc: imageSourceUrl(contractsToSend[4]),
|
||||
question6Title: contractsToSend[5].question,
|
||||
question6Link: contractUrl(contractsToSend[5]),
|
||||
question6ImgSrc: imageSourceUrl(contractsToSend[5]),
|
||||
},
|
||||
deliveryTime ? { 'o:deliverytime': deliveryTime } : undefined
|
||||
)
|
||||
}
|
||||
|
||||
function imageSourceUrl(contract: Contract) {
|
||||
return buildCardUrl(getOpenGraphProps(contract))
|
||||
}
|
||||
|
||||
export const sendNewFollowedMarketEmail = async (
|
||||
reason: notification_reason_types,
|
||||
userId: string,
|
||||
privateUser: PrivateUser,
|
||||
contract: Contract
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
)
|
||||
if (!privateUser.email || !sendToEmail) return
|
||||
const user = await getUser(privateUser.id)
|
||||
if (!user) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const creatorName = contract.creatorName
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
`${creatorName} asked ${contract.question}`,
|
||||
'new-market-from-followed-user',
|
||||
{
|
||||
name: firstName,
|
||||
creatorName,
|
||||
unsubscribeUrl,
|
||||
questionTitle: contract.question,
|
||||
questionUrl: contractUrl(contract),
|
||||
questionImgSrc: imageSourceUrl(contract),
|
||||
},
|
||||
{
|
||||
from: `${creatorName} on Manifold <no-reply@manifold.markets>`,
|
||||
}
|
||||
)
|
||||
}
|
||||
export const sendNewUniqueBettorsEmail = async (
|
||||
reason: notification_reason_types,
|
||||
userId: string,
|
||||
privateUser: PrivateUser,
|
||||
contract: Contract,
|
||||
totalPredictors: number,
|
||||
newPredictors: User[],
|
||||
userBets: Dictionary<[Bet, ...Bet[]]>,
|
||||
bonusAmount: number
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
reason
|
||||
)
|
||||
if (!privateUser.email || !sendToEmail) return
|
||||
const user = await getUser(privateUser.id)
|
||||
if (!user) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const creatorName = contract.creatorName
|
||||
// make the emails stack for the same contract
|
||||
const subject = `You made a popular market! ${
|
||||
contract.question.length > 50
|
||||
? contract.question.slice(0, 50) + '...'
|
||||
: contract.question
|
||||
} just got ${
|
||||
newPredictors.length
|
||||
} new predictions. Check out who's predicting on it inside.`
|
||||
const templateData: Record<string, string> = {
|
||||
name: firstName,
|
||||
creatorName,
|
||||
totalPredictors: totalPredictors.toString(),
|
||||
bonusString: formatMoney(bonusAmount),
|
||||
marketTitle: contract.question,
|
||||
marketUrl: contractUrl(contract),
|
||||
unsubscribeUrl,
|
||||
newPredictors: newPredictors.length.toString(),
|
||||
}
|
||||
|
||||
newPredictors.forEach((p, i) => {
|
||||
templateData[`bettor${i + 1}Name`] = p.name
|
||||
if (p.avatarUrl) templateData[`bettor${i + 1}AvatarUrl`] = p.avatarUrl
|
||||
const bet = userBets[p.id][0]
|
||||
if (bet) {
|
||||
const { amount, sale } = bet
|
||||
templateData[`bet${i + 1}Description`] = `${
|
||||
sale || amount < 0 ? 'sold' : 'bought'
|
||||
} ${formatMoney(Math.abs(amount))}`
|
||||
}
|
||||
})
|
||||
|
||||
return await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
newPredictors.length === 1 ? 'new-unique-bettor' : 'new-unique-bettors',
|
||||
templateData,
|
||||
{
|
||||
from: `Manifold Markets <no-reply@manifold.markets>`,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const sendWeeklyPortfolioUpdateEmail = async (
|
||||
user: User,
|
||||
privateUser: PrivateUser,
|
||||
investments: PerContractInvestmentsData[],
|
||||
overallPerformance: OverallPerformanceData
|
||||
) => {
|
||||
if (!privateUser || !privateUser.email) return
|
||||
|
||||
const { unsubscribeUrl, sendToEmail } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'profit_loss_updates'
|
||||
)
|
||||
|
||||
if (!sendToEmail) return
|
||||
|
||||
const { name } = user
|
||||
const firstName = name.split(' ')[0]
|
||||
const templateData: Record<string, string> = {
|
||||
name: firstName,
|
||||
unsubscribeUrl,
|
||||
...overallPerformance,
|
||||
}
|
||||
investments.forEach((investment, i) => {
|
||||
templateData[`question${i + 1}Title`] = investment.questionTitle
|
||||
templateData[`question${i + 1}Url`] = investment.questionUrl
|
||||
templateData[`question${i + 1}Prob`] = investment.questionProb
|
||||
templateData[`question${i + 1}Change`] = formatMoney(investment.profit)
|
||||
templateData[`question${i + 1}ChangeStyle`] = investment.profitStyle
|
||||
})
|
||||
|
||||
await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
// 'iansphilips@gmail.com',
|
||||
`Here's your weekly portfolio update!`,
|
||||
investments.length === 0
|
||||
? 'portfolio-update-no-movers'
|
||||
: 'portfolio-update',
|
||||
templateData
|
||||
)
|
||||
log('Sent portfolio update email to', privateUser.email)
|
||||
}
|
||||
|
|
36
functions/src/follow-market.ts
Normal file
36
functions/src/follow-market.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const addUserToContractFollowers = async (
|
||||
contractId: string,
|
||||
userId: string
|
||||
) => {
|
||||
const followerDoc = await firestore
|
||||
.collection(`contracts/${contractId}/follows`)
|
||||
.doc(userId)
|
||||
.get()
|
||||
if (followerDoc.exists) return
|
||||
await firestore
|
||||
.collection(`contracts/${contractId}/follows`)
|
||||
.doc(userId)
|
||||
.set({
|
||||
id: userId,
|
||||
createdTime: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
export const removeUserFromContractFollowers = async (
|
||||
contractId: string,
|
||||
userId: string
|
||||
) => {
|
||||
const followerDoc = await firestore
|
||||
.collection(`contracts/${contractId}/follows`)
|
||||
.doc(userId)
|
||||
.get()
|
||||
if (!followerDoc.exists) return
|
||||
await firestore
|
||||
.collection(`contracts/${contractId}/follows`)
|
||||
.doc(userId)
|
||||
.delete()
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import {
|
||||
APIError,
|
||||
EndpointDefinition,
|
||||
lookupUser,
|
||||
parseCredentials,
|
||||
writeResponseError,
|
||||
} from './api'
|
||||
|
||||
const opts = {
|
||||
method: 'GET',
|
||||
minInstances: 1,
|
||||
concurrency: 100,
|
||||
memory: '2GiB',
|
||||
cpu: 1,
|
||||
} as const
|
||||
|
||||
export const getcustomtoken: EndpointDefinition = {
|
||||
opts,
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
const credentials = await parseCredentials(req)
|
||||
if (credentials.kind != 'jwt') {
|
||||
throw new APIError(403, 'API keys cannot mint custom tokens.')
|
||||
}
|
||||
const user = await lookupUser(credentials)
|
||||
const token = await admin.auth().createCustomToken(user.uid)
|
||||
res.status(200).json({ token: token })
|
||||
} catch (e) {
|
||||
writeResponseError(e, res)
|
||||
}
|
||||
},
|
||||
}
|
42
functions/src/helpers/add-house-subsidy.ts
Normal file
42
functions/src/helpers/add-house-subsidy.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { CPMMContract } from '../../../common/contract'
|
||||
import { isProd } from '../utils'
|
||||
import {
|
||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
} from '../../../common/antes'
|
||||
import { getNewLiquidityProvision } from '../../../common/add-liquidity'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const addHouseSubsidy = (contractId: string, amount: number) => {
|
||||
return firestore.runTransaction(async (transaction) => {
|
||||
const newLiquidityProvisionDoc = firestore
|
||||
.collection(`contracts/${contractId}/liquidity`)
|
||||
.doc()
|
||||
|
||||
const providerId = isProd()
|
||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
const snap = await contractDoc.get()
|
||||
const contract = snap.data() as CPMMContract
|
||||
|
||||
const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } =
|
||||
getNewLiquidityProvision(
|
||||
providerId,
|
||||
amount,
|
||||
contract,
|
||||
newLiquidityProvisionDoc.id
|
||||
)
|
||||
|
||||
transaction.update(contractDoc, {
|
||||
subsidyPool: newSubsidyPool,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
} as Partial<CPMMContract>)
|
||||
|
||||
transaction.create(newLiquidityProvisionDoc, newLiquidityProvision)
|
||||
})
|
||||
}
|
|
@ -5,11 +5,13 @@ import { EndpointDefinition } from './api'
|
|||
admin.initializeApp()
|
||||
|
||||
// v1
|
||||
export * from './on-create-user'
|
||||
export * from './on-create-bet'
|
||||
export * from './on-create-comment-on-contract'
|
||||
export * from './on-view'
|
||||
export * from './update-metrics'
|
||||
export { scheduleUpdateMetrics } from './update-metrics'
|
||||
export * from './update-stats'
|
||||
export * from './update-loans'
|
||||
export * from './backup-db'
|
||||
export * from './market-close-notifications'
|
||||
export * from './on-create-answer'
|
||||
|
@ -19,12 +21,17 @@ export * from './on-follow-user'
|
|||
export * from './on-unfollow-user'
|
||||
export * from './on-create-liquidity-provision'
|
||||
export * from './on-update-group'
|
||||
export * from './on-create-group'
|
||||
export * from './on-update-user'
|
||||
export * from './on-create-comment-on-group'
|
||||
export * from './on-create-txn'
|
||||
export * from './on-delete-group'
|
||||
export * from './score-contracts'
|
||||
export * from './weekly-markets-emails'
|
||||
export * from './reset-betting-streaks'
|
||||
export * from './reset-weekly-emails-flags'
|
||||
export * from './on-update-contract-follow'
|
||||
export * from './on-update-like'
|
||||
export * from './weekly-portfolio-emails'
|
||||
export * from './drizzle-liquidity'
|
||||
|
||||
// v2
|
||||
export * from './health'
|
||||
|
@ -37,14 +44,15 @@ export * from './cancel-bet'
|
|||
export * from './sell-bet'
|
||||
export * from './sell-shares'
|
||||
export * from './claim-manalink'
|
||||
export * from './create-contract'
|
||||
export * from './add-liquidity'
|
||||
export * from './withdraw-liquidity'
|
||||
export * from './create-market'
|
||||
export * from './create-group'
|
||||
export * from './resolve-market'
|
||||
export * from './unsubscribe'
|
||||
export * from './stripe'
|
||||
export * from './mana-bonus-email'
|
||||
export * from './close-market'
|
||||
export * from './update-comment-bounty'
|
||||
export * from './add-subsidy'
|
||||
|
||||
import { health } from './health'
|
||||
import { transact } from './transact'
|
||||
|
@ -56,16 +64,20 @@ import { cancelbet } from './cancel-bet'
|
|||
import { sellbet } from './sell-bet'
|
||||
import { sellshares } from './sell-shares'
|
||||
import { claimmanalink } from './claim-manalink'
|
||||
import { createmarket } from './create-contract'
|
||||
import { addliquidity } from './add-liquidity'
|
||||
import { withdrawliquidity } from './withdraw-liquidity'
|
||||
import { createmarket } from './create-market'
|
||||
import { createcomment } from './create-comment'
|
||||
import { addcommentbounty, awardcommentbounty } from './update-comment-bounty'
|
||||
import { creategroup } from './create-group'
|
||||
import { resolvemarket } from './resolve-market'
|
||||
import { closemarket } from './close-market'
|
||||
import { unsubscribe } from './unsubscribe'
|
||||
import { stripewebhook, createcheckoutsession } from './stripe'
|
||||
import { getcurrentuser } from './get-current-user'
|
||||
import { acceptchallenge } from './accept-challenge'
|
||||
import { getcustomtoken } from './get-custom-token'
|
||||
import { createpost } from './create-post'
|
||||
import { savetwitchcredentials } from './save-twitch-credentials'
|
||||
import { updatemetrics } from './update-metrics'
|
||||
import { addsubsidy } from './add-subsidy'
|
||||
|
||||
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
||||
return onRequest(opts, handler as any)
|
||||
|
@ -81,16 +93,21 @@ const sellBetFunction = toCloudFunction(sellbet)
|
|||
const sellSharesFunction = toCloudFunction(sellshares)
|
||||
const claimManalinkFunction = toCloudFunction(claimmanalink)
|
||||
const createMarketFunction = toCloudFunction(createmarket)
|
||||
const addLiquidityFunction = toCloudFunction(addliquidity)
|
||||
const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity)
|
||||
const addSubsidyFunction = toCloudFunction(addsubsidy)
|
||||
const addCommentBounty = toCloudFunction(addcommentbounty)
|
||||
const createCommentFunction = toCloudFunction(createcomment)
|
||||
const awardCommentBounty = toCloudFunction(awardcommentbounty)
|
||||
const createGroupFunction = toCloudFunction(creategroup)
|
||||
const resolveMarketFunction = toCloudFunction(resolvemarket)
|
||||
const closeMarketFunction = toCloudFunction(closemarket)
|
||||
const unsubscribeFunction = toCloudFunction(unsubscribe)
|
||||
const stripeWebhookFunction = toCloudFunction(stripewebhook)
|
||||
const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
|
||||
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
||||
const acceptChallenge = toCloudFunction(acceptchallenge)
|
||||
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
||||
const createPostFunction = toCloudFunction(createpost)
|
||||
const saveTwitchCredentials = toCloudFunction(savetwitchcredentials)
|
||||
const updateMetricsFunction = toCloudFunction(updatemetrics)
|
||||
|
||||
export {
|
||||
healthFunction as health,
|
||||
|
@ -104,14 +121,19 @@ export {
|
|||
sellSharesFunction as sellshares,
|
||||
claimManalinkFunction as claimmanalink,
|
||||
createMarketFunction as createmarket,
|
||||
addLiquidityFunction as addliquidity,
|
||||
withdrawLiquidityFunction as withdrawliquidity,
|
||||
addSubsidyFunction as addsubsidy,
|
||||
createGroupFunction as creategroup,
|
||||
resolveMarketFunction as resolvemarket,
|
||||
closeMarketFunction as closemarket,
|
||||
unsubscribeFunction as unsubscribe,
|
||||
stripeWebhookFunction as stripewebhook,
|
||||
createCheckoutSessionFunction as createcheckoutsession,
|
||||
getCurrentUserFunction as getcurrentuser,
|
||||
acceptChallenge as acceptchallenge,
|
||||
getCustomTokenFunction as getcustomtoken,
|
||||
createPostFunction as createpost,
|
||||
saveTwitchCredentials as savetwitchcredentials,
|
||||
createCommentFunction as createcomment,
|
||||
addCommentBounty as addcommentbounty,
|
||||
awardCommentBounty as awardcommentbounty,
|
||||
updateMetricsFunction as updatemetrics,
|
||||
}
|
||||
|
|
|
@ -3,9 +3,10 @@ import * as admin from 'firebase-admin'
|
|||
|
||||
import { Contract } from '../../common/contract'
|
||||
import { getPrivateUser, getUserByUsername } from './utils'
|
||||
import { sendMarketCloseEmail } from './emails'
|
||||
import { createNotification } from './create-notification'
|
||||
import { createMarketClosedNotification } from './create-notification'
|
||||
import { DAY_MS } from '../../common/util/time'
|
||||
|
||||
const SEND_NOTIFICATIONS_EVERY_DAYS = 5
|
||||
export const marketCloseNotifications = functions
|
||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||
.pubsub.schedule('every 1 hours')
|
||||
|
@ -15,31 +16,31 @@ export const marketCloseNotifications = functions
|
|||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function sendMarketCloseEmails() {
|
||||
export async function sendMarketCloseEmails() {
|
||||
const contracts = await firestore.runTransaction(async (transaction) => {
|
||||
const snap = await transaction.get(
|
||||
firestore.collection('contracts').where('isResolved', '!=', true)
|
||||
)
|
||||
const contracts = snap.docs.map((doc) => doc.data() as Contract)
|
||||
const now = Date.now()
|
||||
const closeContracts = contracts.filter(
|
||||
(contract) =>
|
||||
contract.closeTime &&
|
||||
contract.closeTime < now &&
|
||||
shouldSendFirstOrFollowUpCloseNotification(contract)
|
||||
)
|
||||
|
||||
return snap.docs
|
||||
.map((doc) => {
|
||||
const contract = doc.data() as Contract
|
||||
|
||||
if (
|
||||
contract.resolution ||
|
||||
(contract.closeEmailsSent ?? 0) >= 1 ||
|
||||
contract.closeTime === undefined ||
|
||||
(contract.closeTime ?? 0) > Date.now()
|
||||
await Promise.all(
|
||||
closeContracts.map(async (contract) => {
|
||||
await transaction.update(
|
||||
firestore.collection('contracts').doc(contract.id),
|
||||
{
|
||||
closeEmailsSent: admin.firestore.FieldValue.increment(1),
|
||||
}
|
||||
)
|
||||
return undefined
|
||||
|
||||
transaction.update(doc.ref, {
|
||||
closeEmailsSent: (contract.closeEmailsSent ?? 0) + 1,
|
||||
})
|
||||
|
||||
return contract
|
||||
})
|
||||
.filter((x) => !!x) as Contract[]
|
||||
)
|
||||
return closeContracts
|
||||
})
|
||||
|
||||
for (const contract of contracts) {
|
||||
|
@ -56,15 +57,40 @@ async function sendMarketCloseEmails() {
|
|||
const privateUser = await getPrivateUser(user.id)
|
||||
if (!privateUser) continue
|
||||
|
||||
await sendMarketCloseEmail(user, privateUser, contract)
|
||||
await createNotification(
|
||||
contract.id,
|
||||
'contract',
|
||||
'closed',
|
||||
await createMarketClosedNotification(
|
||||
contract,
|
||||
user,
|
||||
'closed' + contract.id.slice(6, contract.id.length),
|
||||
contract.closeTime?.toString() ?? new Date().toString(),
|
||||
{ contract }
|
||||
privateUser,
|
||||
contract.id + '-closed-at-' + contract.closeTime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// The downside of this approach is if this function goes down for the entire
|
||||
// day of a multiple of the time period after the market has closed, it won't
|
||||
// keep sending them notifications bc when it comes back online the time period will have passed
|
||||
function shouldSendFirstOrFollowUpCloseNotification(contract: Contract) {
|
||||
if (!contract.closeEmailsSent || contract.closeEmailsSent === 0) return true
|
||||
const { closedMultipleOfNDaysAgo, fullTimePeriodsSinceClose } =
|
||||
marketClosedMultipleOfNDaysAgo(contract)
|
||||
return (
|
||||
contract.closeEmailsSent > 0 &&
|
||||
closedMultipleOfNDaysAgo &&
|
||||
contract.closeEmailsSent === fullTimePeriodsSinceClose
|
||||
)
|
||||
}
|
||||
|
||||
function marketClosedMultipleOfNDaysAgo(contract: Contract) {
|
||||
const now = Date.now()
|
||||
const closeTime = contract.closeTime
|
||||
if (!closeTime)
|
||||
return { closedMultipleOfNDaysAgo: false, fullTimePeriodsSinceClose: 0 }
|
||||
const daysSinceClose = Math.floor((now - closeTime) / DAY_MS)
|
||||
return {
|
||||
closedMultipleOfNDaysAgo:
|
||||
daysSinceClose % SEND_NOTIFICATIONS_EVERY_DAYS == 0,
|
||||
fullTimePeriodsSinceClose: Math.floor(
|
||||
daysSinceClose / SEND_NOTIFICATIONS_EVERY_DAYS
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user