Compare commits
594 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c96de053e | ||
|
|
d336ed9d3d | ||
|
|
05f95531e7 | ||
|
|
78cb58329e | ||
|
|
0e561f80ef | ||
|
|
51e3bee414 | ||
|
|
57fc9e1eb7 | ||
|
|
f6660bef03 | ||
|
|
ae0bedf24b | ||
|
|
206c999835 | ||
|
|
3bd0b1c9be | ||
|
|
3cc7cecf6a | ||
|
|
9620365349 | ||
|
|
8e4e8a90ab | ||
|
|
d8e43f0834 | ||
|
|
82f27186cf | ||
|
|
876d3a1cc3 | ||
|
|
712f82cb1a | ||
|
|
682215a574 | ||
|
|
395b390df6 | ||
|
|
cee3390b71 | ||
|
|
418ee08b00 | ||
|
|
ebfcd8fbbe | ||
|
|
c27e70b87c | ||
|
|
b4f3555600 | ||
|
|
0e87c38548 | ||
|
|
245d0eed06 | ||
|
|
46c8caa09c | ||
|
|
aa056adaf9 | ||
|
|
ce4df4f79e | ||
|
|
769a7ab9b5 | ||
|
|
5cbc56cf65 | ||
|
|
098c7b9cbe | ||
|
|
a0d880cf81 | ||
|
|
443a00a777 | ||
|
|
9eaa22f644 | ||
|
|
1c92482855 | ||
|
|
274d79dfde | ||
|
|
c2ca9ab93e | ||
|
|
0fbc826581 | ||
|
|
e9f784b2bc | ||
|
|
f9a9469523 | ||
|
|
b995f4d671 | ||
|
|
3f62bcf2f0 | ||
|
|
bd30721989 | ||
|
|
802d56231a | ||
|
|
9536df2ff1 | ||
|
|
ff3ed31dd7 | ||
|
|
719f82c7c4 | ||
|
|
0937e35095 | ||
|
|
3f9b436c86 | ||
|
|
40afc7388a | ||
|
|
eff6484a10 | ||
|
|
635875345e | ||
|
|
b670affd4c | ||
|
|
6798bf3cf1 | ||
|
|
8734ee5986 | ||
|
|
226fc441a7 | ||
|
|
36242d14b0 | ||
|
|
b02352e8bf | ||
|
|
819e2f5712 | ||
|
|
5357c58ac9 | ||
|
|
aada666c1a | ||
|
|
86fc15907a | ||
|
|
c5f2df4bc0 | ||
|
|
71d3661b22 | ||
|
|
740db0fe16 | ||
|
|
ed6afb50de | ||
|
|
9d439e7f62 | ||
|
|
6b60855362 | ||
|
|
53b53c2207 | ||
|
|
2818167131 | ||
|
|
c2440d1080 | ||
|
|
f556f745fb | ||
|
|
7f597c0fab | ||
|
|
cfaf5d79c1 | ||
|
|
e9fbf61375 | ||
|
|
749c9b40ea | ||
|
|
b553a6fcd8 | ||
|
|
ad1e2c60a1 | ||
|
|
7001452f89 | ||
|
|
5fee3c6fdd | ||
|
|
40b1e40721 | ||
|
|
aee4df10a8 | ||
|
|
82a194987a | ||
|
|
1882b14690 | ||
|
|
d56030a920 | ||
|
|
b75438bc8b | ||
|
|
9f44e48cf1 | ||
|
|
48855f487f | ||
|
|
f09ded1a3f | ||
|
|
e16348e2e0 | ||
|
|
45c3e1dc78 | ||
|
|
24ddace1eb | ||
|
|
b38ce038e6 | ||
|
|
04bf075826 | ||
|
|
27ec2e018c | ||
|
|
3ffe7693cf | ||
|
|
2c7381ff47 | ||
|
|
6e4746a7fe | ||
|
|
48379c66ce | ||
|
|
1b7f0d11fd | ||
|
|
db1d264ad3 | ||
|
|
fd24cb890a | ||
|
|
932c4482d2 | ||
|
|
2d8dac5c15 | ||
|
|
0e3d188a9a | ||
|
|
1be62f0bec | ||
|
|
e6519a7dd1 | ||
|
|
3ced65b8a4 | ||
|
|
bab8506919 | ||
|
|
4a79280edc | ||
|
|
53f0ca9b7e | ||
|
|
0381200fec | ||
|
|
040ebb5475 | ||
|
|
0752314d87 | ||
|
|
9db5fd104a | ||
|
|
b9db195017 | ||
|
|
b6bdbcf587 | ||
|
|
cc325b2fbe | ||
|
|
2694379c97 | ||
|
|
82aaf0784a | ||
|
|
55a509cad8 | ||
|
|
463dd6fdfb | ||
|
|
3ad4fb6846 | ||
|
|
1c76277c46 | ||
|
|
f90f81b3d9 | ||
|
|
81accd1864 | ||
|
|
524ce49fcb | ||
|
|
cbec108172 | ||
|
|
3a4dc91e49 | ||
|
|
af80079957 | ||
|
|
c7a8e9e5a7 | ||
|
|
e6362212c6 | ||
|
|
d63bdf1887 | ||
|
|
3bb55c6b5d | ||
|
|
71212f7e32 | ||
|
|
731ff4a485 | ||
|
|
519ba8963a | ||
|
|
ec784ccb36 | ||
|
|
20d8404717 | ||
|
|
809841df0a | ||
|
|
2dc5a6efb4 | ||
|
|
7cd72a706d | ||
|
|
4f8956f79a | ||
|
|
beef7f2d86 | ||
|
|
b667662b49 | ||
|
|
ed87df6269 | ||
|
|
622c72d4a8 | ||
|
|
df6c44d2c4 | ||
|
|
59e7a2867a | ||
|
|
0a31428d7a | ||
|
|
fbc7e64032 | ||
|
|
4a8fb760d2 | ||
|
|
15c510b742 | ||
|
|
e81b0a4d1f | ||
|
|
b9fc65ec9a | ||
|
|
d73c368ee4 | ||
|
|
5e1c430657 | ||
|
|
d4f19e42f8 | ||
|
|
60593df077 | ||
|
|
aeef2c2356 | ||
|
|
54880d201a | ||
|
|
0f6581bf47 | ||
|
|
37c3330897 | ||
|
|
7a981923f6 | ||
|
|
9672c2db9a | ||
|
|
13f57d8d73 | ||
|
|
43f4d0f1b0 | ||
|
|
19c83c92ab | ||
|
|
91c2013522 | ||
|
|
cf09e80934 | ||
|
|
ad9879b4f9 | ||
|
|
c528c6169e | ||
|
|
33c93b5ded | ||
|
|
5ba046f111 | ||
|
|
5414a7c7ef | ||
|
|
22fd0cc7bb | ||
|
|
784fbfe7b1 | ||
|
|
f47d00be37 | ||
|
|
2eb145ab80 | ||
|
|
adaa7f1a0d | ||
|
|
2506c95816 | ||
|
|
6303293037 | ||
|
|
185cb52501 | ||
|
|
92cf5b42c0 | ||
|
|
ea45289460 | ||
|
|
7f81073037 | ||
|
|
ac9a6f0398 | ||
|
|
172160deb5 | ||
|
|
7fc005024d | ||
|
|
05ba0e4a66 | ||
|
|
5c53d8237d | ||
|
|
6aaf0db78d | ||
|
|
1c3ce8fdb7 | ||
|
|
3e05db4b87 | ||
|
|
a9b2179eab | ||
|
|
0fffff66dc | ||
|
|
1316a45c7a | ||
|
|
dcf7d29357 | ||
|
|
184d515e16 | ||
|
|
86e4dd5153 | ||
|
|
e4d103b015 | ||
|
|
f71c781530 | ||
|
|
736e395cd7 | ||
|
|
8021135879 | ||
|
|
acdeb402ec | ||
|
|
84f5d8f94d | ||
|
|
bb28235bee | ||
|
|
f69226c1d1 | ||
|
|
35ca1b974e | ||
|
|
fdb9d8a8c9 | ||
|
|
16cefb9cdb | ||
|
|
63bf1c0ac8 | ||
|
|
44e3b87ae1 | ||
|
|
a3fbab9163 | ||
|
|
1ac1002c37 | ||
|
|
08e0fb7f4b | ||
|
|
24d62d9594 | ||
|
|
28ea390863 | ||
|
|
4ba0d8ffc0 | ||
|
|
7306423192 | ||
|
|
147a9bcbb2 | ||
|
|
652a8305c8 | ||
|
|
80a77dd567 | ||
|
|
5951d0c40c | ||
|
|
60acc689ee | ||
|
|
cd520a9e2c | ||
|
|
b6e388c68c | ||
|
|
45a138dec1 | ||
|
|
26d2ab53e8 | ||
|
|
e50ecc8309 | ||
|
|
406d7fe964 | ||
|
|
366f4d98ef | ||
|
|
4eda18a4d5 | ||
|
|
ea66b8b959 | ||
|
|
1eab4291f6 | ||
|
|
bed144b994 | ||
|
|
dac334f975 | ||
|
|
5478cb0c7b | ||
|
|
b47cebd40b | ||
|
|
bb51be545b | ||
|
|
ce8105bf08 | ||
|
|
59db4c5b02 | ||
|
|
1f6f1f1dbd | ||
|
|
1c142bb71f | ||
|
|
3e67982bb4 | ||
|
|
bb79c73416 | ||
|
|
b1da7d87f9 | ||
|
|
a79324f23c | ||
|
|
9008db5166 | ||
|
|
bcc57e4612 | ||
|
|
8c7315760a | ||
|
|
290083b414 | ||
|
|
4bc778ff3e | ||
|
|
49ae6ef6bd | ||
|
|
3dab1da5ea | ||
|
|
6ec9bb7362 | ||
|
|
31d0c3c9b3 | ||
|
|
88b7acf3ae | ||
|
|
877e000da6 | ||
|
|
3f05a88409 | ||
|
|
03ab5c80a8 | ||
|
|
4faaa60aea | ||
|
|
3a94a99293 | ||
|
|
e893bc2685 | ||
|
|
cccf6ac680 | ||
|
|
ea5af12b1d | ||
|
|
bf1f613a4f | ||
|
|
49f1acb52a | ||
|
|
a97005e2e1 | ||
|
|
966b02b2de | ||
|
|
024bbce9a4 | ||
|
|
c960c14170 | ||
|
|
39295e9a33 | ||
|
|
0feddebcc3 | ||
|
|
5e4108fbdc | ||
|
|
38cce46c7b | ||
|
|
69ae42f285 | ||
|
|
ef9619b1bd | ||
|
|
041f3d5ba2 | ||
|
|
666bb79833 | ||
|
|
2736d94432 | ||
|
|
57b098f309 | ||
|
|
c802132bd5 | ||
|
|
485764fe38 | ||
|
|
51dc10b467 | ||
|
|
7cf768b8dd | ||
|
|
59b5e81d16 | ||
|
|
8fc98abb1a | ||
|
|
def4ab3bc0 | ||
|
|
bb6de9056b | ||
|
|
cc52da9fc4 | ||
|
|
b98e62cac8 | ||
|
|
6cd1f04584 | ||
|
|
8ffe4e991b | ||
|
|
2b85852a48 | ||
|
|
94015b279c | ||
|
|
1291887049 | ||
|
|
d4a26a5610 | ||
|
|
1249371397 | ||
|
|
2ce7b08244 | ||
|
|
156254afa9 | ||
|
|
e31f4e549b | ||
|
|
f47e1f16d7 | ||
|
|
9954f9502d | ||
|
|
9fbdc3cab1 | ||
|
|
e04be0711f | ||
|
|
5c3caa7143 | ||
|
|
1ea724609f | ||
|
|
2b6948db94 | ||
|
|
0b8026e6fe | ||
|
|
b14831be12 | ||
|
|
b12a61e2fb | ||
|
|
6f6c9d7174 | ||
|
|
89c3ec7526 | ||
|
|
06c81d9304 | ||
|
|
15c59e8149 | ||
|
|
f6b66c759a | ||
|
|
b1c99d4b37 | ||
|
|
add6df46e6 | ||
|
|
b2dfb29f51 | ||
|
|
097ef6a3df | ||
|
|
984cf81911 | ||
|
|
08501234a8 | ||
|
|
3afe7fe9f2 | ||
|
|
bd573724b9 | ||
|
|
684258dc17 | ||
|
|
a53e773e5d | ||
|
|
5307d113a8 | ||
|
|
9e7d59f1fe | ||
|
|
2ccd228f23 | ||
|
|
a726f67453 | ||
|
|
c9b4016b78 | ||
|
|
a7135c9ab1 | ||
|
|
a1b3776686 | ||
|
|
dc2225a2bc | ||
|
|
41571169c6 | ||
|
|
7506dbb29c | ||
|
|
4f25c4a2bb | ||
|
|
fa4e75b777 | ||
|
|
f845695f6e | ||
|
|
dbb2f7f6a2 | ||
|
|
264f145708 | ||
|
|
bb2677ab1b | ||
|
|
29e46455c1 | ||
|
|
cfc9748036 | ||
|
|
715555f340 | ||
|
|
6a1f17ab5a | ||
|
|
cd43e0beaa | ||
|
|
24d00f7c39 | ||
|
|
b8573930b9 | ||
|
|
2c1e608f84 | ||
|
|
f90066263e | ||
|
|
1daf15df9f | ||
|
|
708a813f2a | ||
|
|
18b50283d3 | ||
|
|
d9ff7b15a1 | ||
|
|
cc5309b18f | ||
|
|
a060bf1079 | ||
|
|
1ffb9421b2 | ||
|
|
7978ed97a9 | ||
|
|
b4b6c347e3 | ||
|
|
83cb750b2f | ||
|
|
fc0bf229a7 | ||
|
|
b55680dbce | ||
|
|
136e68201f | ||
|
|
b4bb3d72d5 | ||
|
|
3f812f4aee | ||
|
|
02883aca13 | ||
|
|
ace81ecc84 | ||
|
|
0aafcdf885 | ||
|
|
f55062f9a9 | ||
|
|
1fde2504b4 | ||
|
|
6a3abbfb56 | ||
|
|
9ced714718 | ||
|
|
8ed6481f16 | ||
|
|
d160d1a929 | ||
|
|
1092bc2c58 | ||
|
|
a2e56aaabd | ||
|
|
7ec6a965d3 | ||
|
|
b58d4b72ab | ||
|
|
a2d98607ea | ||
|
|
27d9152949 | ||
|
|
3cb069283c | ||
|
|
4d084d0055 | ||
|
|
4b39dfbd69 | ||
|
|
6c5c1c0032 | ||
|
|
22112c763c | ||
|
|
b230cb62f4 | ||
|
|
9f71e384b2 | ||
|
|
8592a4a54d | ||
|
|
ccc94fdfc2 | ||
|
|
4537a4dc94 | ||
|
|
db0cb41b61 | ||
|
|
4ced70f737 | ||
|
|
9262fec093 | ||
|
|
ff3bcc4693 | ||
|
|
34a22ef9f0 | ||
|
|
5e86cf7b29 | ||
|
|
83172dc5ea | ||
|
|
0ca68bb140 | ||
|
|
430356da9a | ||
|
|
1c50460160 | ||
|
|
2ef9e9b61c | ||
|
|
b906cddc6a | ||
|
|
65fe8d4168 | ||
|
|
8956fba629 | ||
|
|
04884fe1b0 | ||
|
|
ef28667d13 | ||
|
|
0ced5f2402 | ||
|
|
252bf32d2f | ||
|
|
01e87657c6 | ||
|
|
55bf30c0e0 | ||
|
|
95f61487e8 | ||
|
|
3ed3ed4994 | ||
|
|
3a0c4ad4db | ||
|
|
803eb46e5f | ||
|
|
a3f0c54f66 | ||
|
|
52796fcc55 | ||
|
|
ca02a6b56a | ||
|
|
b722a20d96 | ||
|
|
9b5261aaeb | ||
|
|
0efb7f51a4 | ||
|
|
bbe42b81de | ||
|
|
e9f469d399 | ||
|
|
4c58258f01 | ||
|
|
f6e6db6430 | ||
|
|
dab445f237 | ||
|
|
da90738ba1 | ||
|
|
e4dbef80eb | ||
|
|
e169c9d900 | ||
|
|
c8a17f5fe9 | ||
|
|
552a38994b | ||
|
|
b313ac9843 | ||
|
|
88aec2c894 | ||
|
|
86677ed193 | ||
|
|
accf0905f8 | ||
|
|
ef5065de48 | ||
|
|
bcb45cded6 | ||
|
|
dfe50b4bee | ||
|
|
90f72e05b9 | ||
|
|
3a0f6920d0 | ||
|
|
07eb9eb9a2 | ||
|
|
a9e8e8b275 | ||
|
|
4708d3d3d7 | ||
|
|
27acddc0e7 | ||
|
|
102a935ff8 | ||
|
|
614c15243e | ||
|
|
082db21308 | ||
|
|
0f75c6a52b | ||
|
|
1c2ed1d9e4 | ||
|
|
0e956fb7c4 | ||
|
|
b49dd03e25 | ||
|
|
82211a888c | ||
|
|
5dd8cbbdc9 | ||
|
|
5640c115de | ||
|
|
7b8114b401 | ||
|
|
8e36361866 | ||
|
|
0a04cff7eb | ||
|
|
d3dd1d4ebd | ||
|
|
50f2e2770a | ||
|
|
830cd07f38 | ||
|
|
326ae58f04 | ||
|
|
25f5a7178f | ||
|
|
e5591e8f06 | ||
|
|
1c4a383a49 | ||
|
|
469a006088 | ||
|
|
c673c9e458 | ||
|
|
dd957d07e4 | ||
|
|
3837ce24ac | ||
|
|
4b87e3d9b8 | ||
|
|
d6759b86e3 | ||
|
|
df55456409 | ||
|
|
f290787b99 | ||
|
|
2f84507a23 | ||
|
|
96c58361a5 | ||
|
|
011136d0e9 | ||
|
|
054d1de88a | ||
|
|
ebab00d7bd | ||
|
|
01dc10d4f3 | ||
|
|
fb3abf275e | ||
|
|
f6ad694200 | ||
|
|
a3d72a4bbc | ||
|
|
1bb750f136 | ||
|
|
5315272694 | ||
|
|
43b9e398b5 | ||
|
|
1247989cf4 | ||
|
|
45a5ca3b88 | ||
|
|
54b4567a81 | ||
|
|
fc45a504fb | ||
|
|
052cc69414 | ||
|
|
5caaaf1b1f | ||
|
|
c0163767ed | ||
|
|
342b0c81f6 | ||
|
|
cb21750b87 | ||
|
|
2e8ef480f9 | ||
|
|
f27bba9ffa | ||
|
|
6528fd136e | ||
|
|
eb802266e1 | ||
|
|
5bec0b78da | ||
|
|
4894460821 | ||
|
|
f3889e326e | ||
|
|
2d61c72588 | ||
|
|
1a634c350a | ||
|
|
35b83ab842 | ||
|
|
c0d42ade6f | ||
|
|
0e2344ba85 | ||
|
|
309d03b5e7 | ||
|
|
8d32e315ed | ||
|
|
1c8b3f0339 | ||
|
|
fc83659041 | ||
|
|
e8f5a0e8c8 | ||
|
|
9e213fc396 | ||
|
|
f10ba73529 | ||
|
|
c2a83cabaa | ||
|
|
7a3c51bc90 | ||
|
|
e69a7c2712 | ||
|
|
d149512d93 | ||
|
|
5826ed15c8 | ||
|
|
3db9e1b9a4 | ||
|
|
79a72d6fe2 | ||
|
|
f3bfd4ad41 | ||
|
|
45acf421b3 | ||
|
|
2c5ea67ada | ||
|
|
2e141ac94c | ||
|
|
8b16f454ca | ||
|
|
094664dc7a | ||
|
|
62a6ef00da | ||
|
|
3a739476ca | ||
|
|
04e4335cc2 | ||
|
|
4235b57cd8 | ||
|
|
cb362f1b2c | ||
|
|
a4d61d8eaa | ||
|
|
310623b948 | ||
|
|
b7303fb9c0 | ||
|
|
4fbb8e9117 | ||
|
|
f0502bfc33 | ||
|
|
98f4af55c9 | ||
|
|
08b8bdd769 | ||
|
|
1283199d0d | ||
|
|
c1fff5ea49 | ||
|
|
94adc449a1 | ||
|
|
4b24e5f754 | ||
|
|
e0a531e538 | ||
|
|
8cef1ca0f7 | ||
|
|
088f1d9ae4 | ||
|
|
89a7d98b41 | ||
|
|
0076fdc65b | ||
|
|
48b4a533c3 | ||
|
|
0b66fde26c | ||
|
|
5788385951 | ||
|
|
0891e67528 | ||
|
|
b319acd9c4 | ||
|
|
05977f950b | ||
|
|
5ac36d4156 | ||
|
|
fb998706c2 | ||
|
|
c45492c890 | ||
|
|
41219b435f | ||
|
|
e1321843de | ||
|
|
0baa8a1fff | ||
|
|
f2a3b66357 | ||
|
|
f2af46037e | ||
|
|
20a06c9b5a | ||
|
|
8ef54d41b6 | ||
|
|
1cdddd1321 | ||
|
|
f834af69fe | ||
|
|
08cd4bec41 | ||
|
|
63ac64f470 | ||
|
|
72686f1e32 | ||
|
|
c07359362f | ||
|
|
ed58c39bdc | ||
|
|
5d2254be27 | ||
|
|
83f4b53f55 | ||
|
|
4b5e2f7f16 | ||
|
|
0c5d915638 | ||
|
|
a03231d356 | ||
|
|
72936322b3 | ||
|
|
1bc6eb9a23 | ||
|
|
8954aa792a | ||
|
|
379437b720 | ||
|
|
02c0dee089 | ||
|
|
c55e1af101 | ||
|
|
87cbd1ab38 | ||
|
|
1090e97bb5 | ||
|
|
921c6b105f | ||
|
|
bc08e9263d | ||
|
|
8c8300a5de | ||
|
|
3f169adcf2 | ||
|
|
774368f325 | ||
|
|
f83f15d37a | ||
|
|
d9f1402576 | ||
|
|
d81cb28010 | ||
|
|
b57a5d7ddb |
3
.env
3
.env
@@ -5,9 +5,12 @@ REACT_APP_AWS_API_REGION="us-east-2"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
|
||||
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
|
||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://magical-alien-tab.quiknode.pro/669e87e569a8277d3fbd9e202f9df93189f19f4c"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
|
||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
|
||||
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
|
||||
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
|
||||
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
|
||||
REACT_APP_UNISWAP_API_URL="https://api.uniswap.org/v2"
|
||||
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"
|
||||
|
||||
@@ -12,4 +12,5 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
||||
REACT_APP_SENTRY_ENABLED=true
|
||||
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
|
||||
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
|
||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
||||
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://ultra-blue-flower.quiknode.pro/770b22d5f362c537bc8fe19b034c45b22958f880"
|
||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap"
|
||||
|
||||
40
.eslintrc.js
40
.eslintrc.js
@@ -1,5 +1,6 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const { node: restrictedImports } = require('@uniswap/eslint-config/restrictedImports')
|
||||
require('@uniswap/eslint-config/load')
|
||||
|
||||
const rulesDirPlugin = require('eslint-plugin-rulesdir')
|
||||
@@ -13,7 +14,6 @@ module.exports = {
|
||||
files: ['**/*'],
|
||||
rules: {
|
||||
'multiline-comment-style': ['error', 'separate-lines'],
|
||||
'rulesdir/enforce-retry-on-import': 'error',
|
||||
'rulesdir/no-undefined-or': 'error',
|
||||
},
|
||||
},
|
||||
@@ -28,6 +28,20 @@ module.exports = {
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
...restrictedImports,
|
||||
paths: [
|
||||
...restrictedImports.paths,
|
||||
{
|
||||
name: '@uniswap/smart-order-router',
|
||||
message: 'Only import types, unless you are in the client-side SOR, to preserve lazy-loading.',
|
||||
allowTypeImports: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'import/no-restricted-paths': [
|
||||
'error',
|
||||
{
|
||||
@@ -56,6 +70,30 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: ':matches(ExportAllDeclaration)',
|
||||
message: 'Barrel exports bloat the bundle size by preventing tree-shaking.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
excludedFiles: ['src/analytics/*'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@uniswap/analytics',
|
||||
message: `Do not import from '@uniswap/analytics' directly. Use 'analytics' instead.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
@uniswap/web-admins
|
||||
* @uniswap/web-reviewers
|
||||
|
||||
50
.github/actions/setup/action.yml
vendored
50
.github/actions/setup/action.yml
vendored
@@ -8,9 +8,9 @@ runs:
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
# cache is intentionally omitted, as it is faster with yarn v1 to cache node_modules.
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
@@ -19,13 +19,13 @@ runs:
|
||||
path: |
|
||||
node_modules
|
||||
!node_modules/.cache
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('yarn.lock') }}
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
shell: bash
|
||||
|
||||
# Validators compile quickly, so caching can be omitted.
|
||||
- run: yarn ajv
|
||||
|
||||
# Run patch-package to apply patches to dependencies.
|
||||
- run: yarn patch-package
|
||||
shell: bash
|
||||
|
||||
# Contracts are compiled from source. If source hasn't changed, the contracts do not need to be re-compiled.
|
||||
@@ -40,36 +40,10 @@ runs:
|
||||
run: yarn contracts
|
||||
shell: bash
|
||||
|
||||
# GraphQL is generated from schema. The schema is always fetched, but if unchanged, graphql does not need to be re-generated.
|
||||
- run: yarn graphql:fetch
|
||||
shell: bash
|
||||
- uses: actions/cache@v3
|
||||
id: graphql-cache
|
||||
with:
|
||||
path: src/graphql/**/__generated__
|
||||
key: ${{ runner.os }}-graphql-${{ hashFiles('src/graphql/**/schema.graphql') }}
|
||||
- if: steps.graphql-cache.outputs.cache-hit != 'true'
|
||||
run: yarn graphql:generate
|
||||
shell: bash
|
||||
|
||||
# Messages are extracted from source.
|
||||
# A record of source file content hashes and catalogs is maintained in node_modules/.cache/lingui.
|
||||
# Messages are always extracted, but extraction may short-circuit from the custom extractor's cache.
|
||||
- uses: actions/cache@v3
|
||||
id: i18n-extract-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-i18n-extract-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-i18n-extract-
|
||||
- run: yarn i18n:extract
|
||||
shell: bash
|
||||
|
||||
# Translations are compiled from messages. If messages haven't changed, the translations do not need to be re-compiled.
|
||||
- uses: actions/cache@v3
|
||||
id: i18n-compile-cache
|
||||
with:
|
||||
path: src/locales/*.js
|
||||
key: ${{ runner.os }}-i18n-compile-${{ hashFiles('src/locales/*.po') }}
|
||||
- if: steps.i18n-compile-cache.outputs.cache-hit !='true'
|
||||
run: yarn i18n:compile
|
||||
# These operations cannot be cached, so they are run concurrently
|
||||
# - ajv: Validators compile quickly, so caching can be omitted.
|
||||
# - graphql: GraphQL is generated from schema and client-side graphql queries. The schema is always fetched and
|
||||
# changes to client-side queries are hard to detect, so it is always re-generated.
|
||||
# - i18n: Messages are extracted from source and compiled. No caching extractor is available (out-of-the-box).
|
||||
- run: yarn concurrently --max-processes=100% npm:ajv npm:graphql npm:i18n
|
||||
shell: bash
|
||||
|
||||
51
.github/workflows/1-main-to-staging.yml
vendored
51
.github/workflows/1-main-to-staging.yml
vendored
@@ -14,19 +14,60 @@ jobs:
|
||||
environment:
|
||||
name: push/staging
|
||||
steps:
|
||||
- name: Check test status
|
||||
uses: actions/github-script@v6.4.1
|
||||
with:
|
||||
script: |
|
||||
const statuses = await github.rest.repos.listCommitStatusesForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: context.sha
|
||||
})
|
||||
const status = statuses.data.find(status => status.context === 'Test / promotion')?.state || 'missing'
|
||||
core.info('Status: ' + status)
|
||||
if (status !== 'success') {
|
||||
core.setFailed('"Test / promotion" must be successful before pushing')
|
||||
}
|
||||
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
|
||||
ref: main
|
||||
|
||||
# The source file must exist for the corresponding translation messages to be downloaded.
|
||||
- run: touch src/locales/en-US.po
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@3133cc916c35590475cf6705f482fb653d8e36e9
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
project_id: 458284
|
||||
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN_SECRET }}
|
||||
source: 'src/locales/en-US.po'
|
||||
translation: 'src/locales/%locale%.po'
|
||||
localization_branch_name: main
|
||||
create_pull_request: false
|
||||
push_translations: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Git config
|
||||
run: |
|
||||
git config user.name "UL Service Account"
|
||||
git config user.email "hello-happy-puppy@users.noreply.github.com"
|
||||
- name: Add CODEOWNERS file
|
||||
git config user.name 'UL Service Account'
|
||||
git config user.email 'hello-happy-puppy@users.noreply.github.com'
|
||||
|
||||
- name: Add translations
|
||||
run: |
|
||||
echo "@uniswap/web-admins" > CODEOWNERS
|
||||
rm src/locales/en-US.po
|
||||
git add -f src/locales/*.po
|
||||
git commit -m 'ci(t9n): download translations from crowdin'
|
||||
|
||||
- name: Add CODEOWNERS
|
||||
run: |
|
||||
echo '* @uniswap/web-admins' > CODEOWNERS
|
||||
git add CODEOWNERS
|
||||
git commit -m "ci: add global CODEOWNERS"
|
||||
git commit -m 'ci: add global CODEOWNERS'
|
||||
|
||||
- name: Git push
|
||||
run: |
|
||||
git push origin main:releases/staging --force
|
||||
|
||||
19
.github/workflows/2-deploy-to-staging.yml
vendored
19
.github/workflows/2-deploy-to-staging.yml
vendored
@@ -10,27 +10,23 @@ jobs:
|
||||
environment:
|
||||
name: deploy/staging
|
||||
steps:
|
||||
- name: Send Slack message that deploy is starting
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Staging deploy started for branch: ${{ github.ref_name }}"
|
||||
"text": "Deploy _started_ for ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
- run: yarn build
|
||||
env:
|
||||
REACT_APP_STAGING: 1
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Update Cloudflare Pages deployment
|
||||
id: pages-deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
@@ -42,18 +38,19 @@ jobs:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
|
||||
branch: main
|
||||
- name: Send Slack message about deployment outcome
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
|
||||
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
if: always()
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Staging deploy **${{ steps.pages-deployment.outcome }}** for: ${{ github.ref_name }}"
|
||||
"text": "Deploy *${{ steps.pages-deployment.outcome }}* for ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
|
||||
continue-on-error: true
|
||||
|
||||
15
.github/workflows/3-staging-to-prod.yml
vendored
15
.github/workflows/3-staging-to-prod.yml
vendored
@@ -14,6 +14,21 @@ jobs:
|
||||
environment:
|
||||
name: push/prod
|
||||
steps:
|
||||
- name: Check test status
|
||||
uses: actions/github-script@v6.4.1
|
||||
with:
|
||||
script: |
|
||||
const statuses = await github.rest.repos.listCommitStatusesForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: context.sha
|
||||
})
|
||||
const status = statuses.data.find(status => status.context === 'Test / promotion')?.state || 'missing'
|
||||
core.info('Status: ' + status)
|
||||
if (status !== 'success') {
|
||||
core.setFailed('"Test / promotion" must be successful before pushing')
|
||||
}
|
||||
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
|
||||
|
||||
21
.github/workflows/4-deploy-to-prod.yml
vendored
21
.github/workflows/4-deploy-to-prod.yml
vendored
@@ -10,21 +10,21 @@ jobs:
|
||||
environment:
|
||||
name: deploy/prod
|
||||
steps:
|
||||
- name: Send Slack message that build is starting
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Production deploy started for branch: ${{ github.ref_name }}"
|
||||
"text": "Deploy _started_ for ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
- run: yarn build
|
||||
|
||||
- name: Bump and tag
|
||||
id: github-tag-action
|
||||
uses: mathieudutour/github-tag-action@d745f2e74aaf1ee82e747b181f7a0967978abee0
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
with:
|
||||
cidv0: ${{ steps.pinata.outputs.hash }}
|
||||
|
||||
- name: Release
|
||||
- name: Publish release
|
||||
uses: actions/create-release@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -74,11 +74,6 @@ jobs:
|
||||
|
||||
${{ steps.github-tag-action.outputs.changelog }}
|
||||
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Update Cloudflare Pages deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
id: pages-deployment
|
||||
@@ -91,18 +86,18 @@ jobs:
|
||||
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
|
||||
branch: main
|
||||
|
||||
- name: Send Slack message about deployment outcome
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
if: always()
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Production deploy **${{ steps.pages-deployment.outcome }}** for: ${{ github.ref_name }}"
|
||||
"text": "Deploy *${{ steps.pages-deployment.outcome }}* for ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
|
||||
continue-on-error: true
|
||||
|
||||
33
.github/workflows/crowdin-sync.yaml
vendored
33
.github/workflows/crowdin-sync.yaml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Crowdin Download
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Download translations every hour.
|
||||
# This is not done as part of the build so that builds remain reproducible.
|
||||
- cron: '0 * * * *'
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
download-translations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn i18n:extract
|
||||
|
||||
- name: Download Crowdin translations
|
||||
uses: crowdin/github-action@1.4.9
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
project_id: 458284
|
||||
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN_SECRET }}
|
||||
source: 'src/locales/en-US.po'
|
||||
translation: 'src/locales/%locale%.po'
|
||||
create_pull_request: true
|
||||
pull_request_title: 'chore(i18n): new Crowdin translations'
|
||||
localization_branch_name: l10n_crowdin
|
||||
commit_message: 'chore(i18n): synchronize translations from crowdin [skip ci]'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
2
.github/workflows/crowdin.yaml
vendored
2
.github/workflows/crowdin.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- run: yarn i18n:extract
|
||||
|
||||
- name: Upload Crowdin sources
|
||||
uses: crowdin/github-action@1.1.0
|
||||
uses: crowdin/github-action@3133cc916c35590475cf6705f482fb653d8e36e9
|
||||
with:
|
||||
upload_sources: true
|
||||
download_translations: false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Slack notifications for releases/* merges
|
||||
name: Slack notification on pushes to releases/*
|
||||
|
||||
# This CI job will push notifications to Slack whenever code is merged into any releases/* branch
|
||||
#
|
||||
@@ -25,7 +25,6 @@ on:
|
||||
|
||||
jobs:
|
||||
notify-slack:
|
||||
name: 'Emit Slack notification(s)'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: notify/releases
|
||||
@@ -45,9 +44,7 @@ jobs:
|
||||
| awk '{print substr($0,0,3000);}' \
|
||||
> /tmp/parsed_github_context
|
||||
echo "SLACK_COMMITS=$(cat /tmp/parsed_github_context)" >> "$GITHUB_OUTPUT"
|
||||
- name: Send custom JSON data to Slack workflow
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
|
||||
125
.github/workflows/release.yaml
vendored
125
.github/workflows/release.yaml
vendored
@@ -1,125 +0,0 @@
|
||||
name: Release
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 16 * * 1-4' # every day 16:00 UTC Monday-Thursday
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
wait-on-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: unit-tests
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: unit-tests
|
||||
- id: cypress-tests
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: cypress-tests
|
||||
- if: steps.unit-tests.outputs.conclusion != 'success' || steps.cypress-tests.outputs.conclusion != 'success'
|
||||
run: exit 1
|
||||
|
||||
tag:
|
||||
needs: wait-on-tests
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
new_tag: ${{ steps.github-tag-action.outputs.new_tag }}
|
||||
changelog: ${{ steps.github-tag-action.outputs.changelog }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Bump and tag
|
||||
id: github-tag-action
|
||||
uses: mathieudutour/github-tag-action@v6.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_branches: .*
|
||||
default_bump: patch
|
||||
|
||||
release:
|
||||
needs: tag
|
||||
if: ${{ needs.tag.outputs.new_tag != null }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn build
|
||||
|
||||
- name: Pin to IPFS
|
||||
id: pinata
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
|
||||
with:
|
||||
pin-name: Uniswap ${{ needs.tag.outputs.new_tag }}
|
||||
path: './build'
|
||||
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
||||
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
||||
|
||||
- name: Pin to Crust
|
||||
uses: crustio/ipfs-crust-action@v2.0.3
|
||||
continue-on-error: true
|
||||
timeout-minutes: 2
|
||||
with:
|
||||
cid: ${{ steps.pinata.outputs.hash }}
|
||||
seeds: ${{ secrets.CRUST_SEEDS }}
|
||||
|
||||
- name: Convert CIDv0 to CIDv1
|
||||
id: convert-cidv0
|
||||
uses: uniswap/convert-cidv0-cidv1@v1.0.0
|
||||
with:
|
||||
cidv0: ${{ steps.pinata.outputs.hash }}
|
||||
|
||||
- name: Release
|
||||
uses: actions/create-release@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ needs.tag.outputs.new_tag }}
|
||||
release_name: Release ${{ needs.tag.outputs.new_tag }}
|
||||
body: |
|
||||
IPFS hash of the deployment:
|
||||
- CIDv0: `${{ steps.pinata.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert-cidv0.outputs.cidv1 }}`
|
||||
|
||||
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
You can also access the Uniswap Interface directly from an IPFS gateway.
|
||||
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
|
||||
**You should always use an IPFS gateway that enforces origin separation**, or our alias to the latest release at [app.uniswap.org](https://app.uniswap.org).
|
||||
Your Uniswap settings are never remembered across different URLs.
|
||||
|
||||
IPFS gateways:
|
||||
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
|
||||
- [ipfs://${{ steps.pinata.outputs.hash }}/](ipfs://${{ steps.pinata.outputs.hash }}/)
|
||||
|
||||
${{ needs.tag.outputs.changelog }}
|
||||
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Update Cloudflare Pages deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: build
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
with:
|
||||
environment: production
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
24
.github/workflows/semgrep.yml
vendored
Normal file
24
.github/workflows/semgrep.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Semgrep
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
pull_request: {}
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/semgrep.yml
|
||||
schedule:
|
||||
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
|
||||
- cron: '2 11 * * *'
|
||||
jobs:
|
||||
semgrep:
|
||||
name: semgrep/ci
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
||||
container:
|
||||
image: returntocorp/semgrep
|
||||
if: (github.actor != 'dependabot[bot]')
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: semgrep ci
|
||||
172
.github/workflows/test.yml
vendored
172
.github/workflows/test.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Test
|
||||
|
||||
# Many build steps have their own caches, so each job has its own cache to improve subsequent build times.
|
||||
# Build tools are configured to cache cache to node_modules/.cache, so this is cached independently of node_modules.
|
||||
# Build tools are configured to cache to node_modules/.cache, so they are cached independently of node_modules.
|
||||
# Caches are saved every run (by keying on github.run_id), and the most recent available cache is loaded.
|
||||
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
|
||||
|
||||
@@ -9,8 +9,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- releases/staging
|
||||
pull_request:
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -20,11 +20,10 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: eslint-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-
|
||||
key: ${{ runner.os }}-eslint-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-eslint-
|
||||
- run: yarn lint
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
@@ -38,11 +37,10 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: tsc-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-
|
||||
key: ${{ runner.os }}-tsc-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-tsc-
|
||||
- run: yarn typecheck
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
@@ -68,17 +66,15 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: jest-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-
|
||||
key: ${{ runner.os }}-jest-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-jest-
|
||||
- run: yarn test --coverage --maxWorkers=100%
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
flags: unit-tests
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
@@ -86,26 +82,40 @@ jobs:
|
||||
name: Unit tests
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
build-e2e:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: build-e2e-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-build-e2e-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-build-e2e-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn build:e2e
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
- uses: actions/upload-artifact@v2
|
||||
path: node_modules/.swc
|
||||
key: ${{ runner.os }}-swc-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-swc-
|
||||
- run: yarn build
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-e2e
|
||||
name: build
|
||||
path: build
|
||||
if-no-files-found: error
|
||||
|
||||
cypress-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-cypress-tsc-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cypress-tsc-
|
||||
- run: yarn typecheck:cypress
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cypress typecheck
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
# Allows for parallel re-runs of cypress tests without re-building.
|
||||
cypress-rerun:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -113,9 +123,8 @@ jobs:
|
||||
- run: exit 0
|
||||
|
||||
cypress-test-matrix:
|
||||
needs: [build-e2e, cypress-rerun]
|
||||
needs: [build, cypress-rerun]
|
||||
runs-on: ubuntu-latest
|
||||
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -124,7 +133,6 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: cypress-cache
|
||||
with:
|
||||
path: /root/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-${{ hashFiles('**/node_modules/cypress/package.json') }}
|
||||
@@ -134,11 +142,10 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-e2e
|
||||
name: build
|
||||
path: build
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: hardhat-cache
|
||||
- uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: cache
|
||||
key: ${{ runner.os }}-hardhat-${{ hashFiles('hardhat.config.js') }}-${{ github.run_id }}
|
||||
@@ -151,8 +158,9 @@ jobs:
|
||||
parallel: true
|
||||
start: yarn serve
|
||||
wait-on: 'http://localhost:3000'
|
||||
browser: chrome
|
||||
browser: electron
|
||||
group: e2e
|
||||
spec: ${{ github.ref_name == 'releases/staging' && 'cypress/{e2e,staging}/**/*.test.ts' || 'cypress/e2e/**/*.test.ts' }}
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -166,25 +174,105 @@ jobs:
|
||||
COMMIT_INFO_TIMESTAMP: ${{ github.event.pull_request.updated_at || github.event.head_commit.timestamp }}
|
||||
CYPRESS_PULL_REQUEST_ID: ${{ github.event.pull_request.number }}
|
||||
CYPRESS_PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }}
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
flags: e2e-tests
|
||||
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cypress tests
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
|
||||
cypress-tests:
|
||||
if: always()
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: hardhat-cache
|
||||
path: cache
|
||||
|
||||
hardhat-cache:
|
||||
needs: [cypress-test-matrix]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: needs.cypress-test-matrix.result != 'success'
|
||||
run: exit 1
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: hardhat-cache
|
||||
path: cache
|
||||
- uses: actions/cache/save@v3
|
||||
with:
|
||||
path: cache
|
||||
key: ${{ runner.os }}-hardhat-${{ hashFiles('hardhat.config.js') }}-${{ github.run_id }}
|
||||
|
||||
cloud-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-cloud-tsc-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cloud-tsc-
|
||||
- run: yarn typecheck:cloud
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cloud typecheck
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
cloud-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-cloud-jest-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cloud-jest-
|
||||
# Ignore start:cloud output so it doesn't flood the test output.
|
||||
# Only use 1 worker for testing, as the other is used to run start:cloud (the proxy server under test).
|
||||
- run: yarn start-server-and-test 'yarn start:cloud >/dev/null' 3000 'yarn test:cloud --coverage --maxWorkers=1'
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
flags: cloud-tests
|
||||
|
||||
pre:
|
||||
if: ${{ github.ref_name == 'main' || github.ref_name == 'releases/staging' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6.4.1
|
||||
with:
|
||||
script: |
|
||||
github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: 'pending',
|
||||
context: 'Test / promotion',
|
||||
description: 'Running tests...',
|
||||
target_url: 'https://github.com/Uniswap/interface/actions/runs/' + context.runId
|
||||
})
|
||||
|
||||
post:
|
||||
if: ${{ github.ref_name == 'main' || github.ref_name == 'releases/staging' }}
|
||||
needs: [pre, lint, typecheck, deps-tests, unit-tests, cypress-test-matrix, cloud-tests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6.4.1
|
||||
with:
|
||||
script: |
|
||||
github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: ${{ env.STATUS }} ? 'success' : 'failure',
|
||||
context: 'Test / promotion',
|
||||
description: ${{ env.STATUS }} ? 'All tests passed' : 'One or more tests failed and are blocking promotion',
|
||||
target_url: 'https://github.com/Uniswap/interface/actions/runs/' + context.runId
|
||||
})
|
||||
env:
|
||||
STATUS: |
|
||||
${{ needs.lint.result == 'success' }} &&
|
||||
${{ needs.typecheck.result == 'success' }} &&
|
||||
${{ needs.deps-tests.result == 'success' }} &&
|
||||
${{ needs.unit-tests.result == 'success' }} &&
|
||||
${{ needs.cypress-test-matrix.result == 'success' }} &&
|
||||
${{ needs.cloud-tests.result == 'success' }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -5,8 +5,7 @@
|
||||
/src/types/v3
|
||||
/src/abis/types
|
||||
/src/locales/**/*.js
|
||||
/src/locales/**/en-US.po
|
||||
/src/locales/**/pseudo.po
|
||||
/src/locales/**/*.po
|
||||
|
||||
# generated files
|
||||
/src/**/__generated__
|
||||
@@ -20,6 +19,8 @@ schema.graphql
|
||||
# testing
|
||||
/coverage
|
||||
/cache
|
||||
/functions/coverage
|
||||
/.swc
|
||||
|
||||
# builds
|
||||
/build
|
||||
@@ -47,7 +48,10 @@ notes.txt
|
||||
|
||||
package-lock.json
|
||||
|
||||
cypress/downloads
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
||||
.vercel
|
||||
|
||||
.wrangler
|
||||
|
||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
25
.snyk
25
.snyk
@@ -1,25 +0,0 @@
|
||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.25.0
|
||||
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
|
||||
ignore:
|
||||
SNYK-JS-OPENZEPPELINCONTRACTS-2964946:
|
||||
- '*':
|
||||
reason: None Given
|
||||
expires: 2099-01-01T00:00:00.000Z
|
||||
created: 2022-12-08T16:25:57.347Z
|
||||
SNYK-JS-OPENZEPPELINCONTRACTS-2958047:
|
||||
- '*':
|
||||
reason: None Given
|
||||
expires: 2099-01-01T00:00:00.000Z
|
||||
created: 2022-12-08T16:26:09.720Z
|
||||
SNYK-JS-OPENZEPPELINCONTRACTS-2958050:
|
||||
- '*':
|
||||
reason: None Given
|
||||
expires: 2099-01-01T00:00:00.000Z
|
||||
created: 2022-12-08T16:26:17.702Z
|
||||
SNYK-JS-OPENZEPPELINCONTRACTS-2965580:
|
||||
- '*':
|
||||
reason: None Given
|
||||
expires: 2099-01-01T00:00:00.000Z
|
||||
created: 2022-12-08T16:26:34.283Z
|
||||
patch: {}
|
||||
36
.swcrc
Normal file
36
.swcrc
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
// has to duplicate from package.json, see swc issue: https://swc.rs/docs/configuration/compilation#env
|
||||
// this breaks jest because jest is setting target for some reason
|
||||
// "env": {
|
||||
// "targets": "> 0.5%, not dead"
|
||||
// },
|
||||
"jsc": {
|
||||
// without this swc breaks WalletConnect class super() call
|
||||
"target": "es2020",
|
||||
"keepClassNames": true,
|
||||
"experimental": {
|
||||
"plugins": [
|
||||
[
|
||||
"@lingui/swc-plugin",
|
||||
{}
|
||||
],
|
||||
[
|
||||
"@swc/plugin-styled-components",
|
||||
{
|
||||
"displayName": true
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @uniswap/web-admins
|
||||
@@ -69,10 +69,10 @@ Other things to note:
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
- Swap on Uniswap V2: <https://app.uniswap.org/#/swap?use=v2>
|
||||
- View V2 liquidity: <https://app.uniswap.org/#/pools/v2>
|
||||
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
|
||||
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>
|
||||
- Swap on Uniswap V2: <https://app.uniswap.org/swap?use=v2>
|
||||
- View V2 liquidity: <https://app.uniswap.org/pools/v2>
|
||||
- Add V2 liquidity: <https://app.uniswap.org/add/v2>
|
||||
- Migrate V2 liquidity to V3: <https://app.uniswap.org/migrate/v2>
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
module.exports = {
|
||||
styledComponents: {
|
||||
fileName: isDev,
|
||||
displayName: isDev,
|
||||
},
|
||||
}
|
||||
26
codecov.yml
26
codecov.yml
@@ -6,8 +6,15 @@ ignore:
|
||||
- "**/instrumented/**/*"
|
||||
- "**/styles/**/*"
|
||||
- "styles/**/*"
|
||||
- "**/styled.tsx"
|
||||
- "**/constants/**/*"
|
||||
- "constants/**/*"
|
||||
- "src/dev/*"
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
|
||||
flag_management:
|
||||
default_rules:
|
||||
@@ -15,11 +22,22 @@ flag_management:
|
||||
- type: project
|
||||
target: auto
|
||||
threshold: 1%
|
||||
# Adjust the base when removing code to avoid penalizing tech debt payback / dead code removal.
|
||||
removed_code_behavior: adjust_base
|
||||
if_ci_failed: error
|
||||
- type: patch
|
||||
target: 80%
|
||||
target: 50%
|
||||
individual_flags:
|
||||
- name: e2e-tests
|
||||
- name: unit-tests
|
||||
- name: cloud-tests
|
||||
statuses:
|
||||
- type: patch
|
||||
target: 0%
|
||||
- type: project
|
||||
target: 80%
|
||||
|
||||
comment:
|
||||
layout: flags
|
||||
hide_comment_details: false
|
||||
|
||||
github_checks:
|
||||
# Turn off GitHub Check annotations, as they make it more difficult to review code.
|
||||
annotations: false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
overrideExisting: true
|
||||
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
|
||||
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap'
|
||||
generates:
|
||||
./src/graphql/thegraph/schema/schema.graphql:
|
||||
plugins:
|
||||
|
||||
111
craco.config.cjs
111
craco.config.cjs
@@ -5,11 +5,14 @@ const { execSync } = require('child_process')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const path = require('path')
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
||||
const { DefinePlugin, IgnorePlugin, ProvidePlugin } = require('webpack')
|
||||
const { IgnorePlugin, ProvidePlugin } = require('webpack')
|
||||
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
|
||||
|
||||
const commitHash = execSync('git rev-parse HEAD').toString().trim()
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
process.env.REACT_APP_GIT_COMMIT_HASH = commitHash
|
||||
|
||||
// Linting and type checking are only necessary as part of development and testing.
|
||||
// Omit them from production builds, as they slow down the feedback loop.
|
||||
const shouldLintOrTypeCheck = !isProduction
|
||||
@@ -20,32 +23,6 @@ function getCacheDirectory(cacheName) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
babel: {
|
||||
plugins: [
|
||||
'@vanilla-extract/babel-plugin',
|
||||
...(process.env.REACT_APP_ADD_COVERAGE_INSTRUMENTATION
|
||||
? [
|
||||
[
|
||||
'istanbul',
|
||||
{
|
||||
all: true,
|
||||
include: ['src/**/*.tsx', 'src/**/*.ts'],
|
||||
exclude: [
|
||||
'src/**/*.css',
|
||||
'src/**/*.css.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.test.tsx',
|
||||
'src/**/*.spec.ts',
|
||||
'src/**/*.spec.tsx',
|
||||
'src/**/graphql/**/*',
|
||||
'src/**/*.d.ts',
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
eslint: {
|
||||
enable: shouldLintOrTypeCheck,
|
||||
pluginOptions(eslintConfig) {
|
||||
@@ -68,16 +45,20 @@ module.exports = {
|
||||
configure(jestConfig) {
|
||||
return Object.assign(jestConfig, {
|
||||
cacheDirectory: getCacheDirectory('jest'),
|
||||
transform: Object.assign(jestConfig.transform, {
|
||||
transform: {
|
||||
...Object.entries(jestConfig.transform).reduce((transform, [key, value]) => {
|
||||
if (value.match(/babel/)) return transform
|
||||
return { ...transform, [key]: value }
|
||||
}, {}),
|
||||
// Transform vanilla-extract using its own transformer.
|
||||
// See https://sandroroth.com/blog/vanilla-extract-cra#jest-transform.
|
||||
'\\.css\\.ts$': '@vanilla-extract/jest-transform',
|
||||
}),
|
||||
// Use @uniswap/conedison's build directly, as jest does not support its exports.
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||
'\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
// Use d3-arrays's build directly, as jest does not support its exports.
|
||||
transformIgnorePatterns: ['d3-array'],
|
||||
moduleNameMapper: {
|
||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
|
||||
'd3-array': 'd3-array/dist/d3-array.min.js',
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -87,25 +68,24 @@ module.exports = {
|
||||
// Webpack 5 does not polyfill node globals, so we do so for those necessary:
|
||||
new ProvidePlugin({
|
||||
// - react-markdown requires process.cwd
|
||||
process: 'process/browser',
|
||||
process: 'process/browser.js',
|
||||
}),
|
||||
new VanillaExtractPlugin(),
|
||||
new RetryChunkLoadPlugin({
|
||||
cacheBust: `function() {
|
||||
return 'cache-bust=' + Date.now();
|
||||
}`,
|
||||
// Retries with exponential backoff (500ms, 1000ms, 2000ms).
|
||||
retryDelay: `function(retryAttempt) {
|
||||
return 2 ** (retryAttempt - 1) * 500;
|
||||
}`,
|
||||
maxRetries: 3,
|
||||
}),
|
||||
// vanilla-extract has poor performance on M1 machines with 'debug' identifiers, so we use 'short' instead.
|
||||
// See https://vanilla-extract.style/documentation/integrations/webpack/#identifiers for docs.
|
||||
// See https://github.com/vanilla-extract-css/vanilla-extract/issues/771#issuecomment-1249524366.
|
||||
new VanillaExtractPlugin({ identifiers: 'short' }),
|
||||
],
|
||||
configure: (webpackConfig) => {
|
||||
// Configure webpack plugins:
|
||||
webpackConfig.plugins = webpackConfig.plugins
|
||||
.map((plugin) => {
|
||||
// Extend process.env with dynamic values (eg commit hash).
|
||||
// This will make dynamic values available to JavaScript only, not to interpolated HTML (ie index.html).
|
||||
if (plugin instanceof DefinePlugin) {
|
||||
Object.assign(plugin.definitions['process.env'], {
|
||||
REACT_APP_GIT_COMMIT_HASH: JSON.stringify(commitHash),
|
||||
})
|
||||
}
|
||||
|
||||
// CSS ordering is mitigated through scoping / naming conventions, so we can ignore order warnings.
|
||||
// See https://webpack.js.org/plugins/mini-css-extract-plugin/#remove-order-warnings.
|
||||
if (plugin instanceof MiniCssExtractPlugin) {
|
||||
@@ -150,19 +130,32 @@ module.exports = {
|
||||
},
|
||||
})
|
||||
|
||||
// Retain source maps for node_modules packages:
|
||||
webpackConfig.module.rules[0] = {
|
||||
...webpackConfig.module.rules[0],
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
|
||||
// Configure webpack transpilation (create-react-app specifies transpilation rules in a oneOf):
|
||||
webpackConfig.module.rules[1].oneOf = webpackConfig.module.rules[1].oneOf.map((rule) => {
|
||||
// The fallback rule (eg for dependencies).
|
||||
if (rule.loader && rule.loader.match(/babel-loader/) && !rule.include) {
|
||||
// Allow not-fully-specified modules so that legacy packages are still able to build.
|
||||
rule.resolve = { fullySpecified: false }
|
||||
|
||||
// The class properties transform is required for @uniswap/analytics to build.
|
||||
rule.options.plugins.push('@babel/plugin-proposal-class-properties')
|
||||
if (rule.loader && rule.loader.match(/babel-loader/)) {
|
||||
rule.loader = 'swc-loader'
|
||||
delete rule.options
|
||||
}
|
||||
return rule
|
||||
})
|
||||
|
||||
// Run terser compression on node_modules before tree-shaking, so that tree-shaking is more effective.
|
||||
// This works by eliminating dead code, so that webpack can identify unused imports and tree-shake them;
|
||||
// it is only necessary for node_modules - it is done through linting for our own source code -
|
||||
// see https://medium.com/engineering-housing/dead-code-elimination-and-tree-shaking-at-housing-part-1-307a94b30f23#7e03:
|
||||
webpackConfig.module.rules.push({
|
||||
enforce: 'post',
|
||||
test: /node_modules.*\.(js)$/,
|
||||
loader: path.join(__dirname, 'scripts/terser-loader.js'),
|
||||
options: { compress: true, mangle: false },
|
||||
})
|
||||
|
||||
// Configure webpack optimization:
|
||||
webpackConfig.optimization = Object.assign(
|
||||
webpackConfig.optimization,
|
||||
@@ -181,16 +174,8 @@ module.exports = {
|
||||
: {}
|
||||
)
|
||||
|
||||
// Configure webpack caching:
|
||||
webpackConfig.cache = Object.assign(webpackConfig.cache, {
|
||||
cacheDirectory: getCacheDirectory('webpack'),
|
||||
})
|
||||
|
||||
// Ignore failed source mappings to avoid spamming the console.
|
||||
// Source mappings for a package will fail if the package does not provide them, but the build will still succeed,
|
||||
// so it is unnecessary (and bothersome) to log it. This should be turned off when debugging missing sourcemaps.
|
||||
// See https://webpack.js.org/loaders/source-map-loader#ignoring-warnings.
|
||||
webpackConfig.ignoreWarnings = [/Failed to parse source map/]
|
||||
// Configure webpack resolution. webpackConfig.cache is unused with swc-loader, but the resolver can still cache:
|
||||
webpackConfig.resolve = Object.assign(webpackConfig.resolve, { unsafeCache: true })
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
|
||||
@@ -1,37 +1,19 @@
|
||||
import codeCoverageTask from '@cypress/code-coverage/task'
|
||||
import { defineConfig } from 'cypress'
|
||||
import { setupHardhatEvents } from 'cypress-hardhat'
|
||||
import { unlinkSync } from 'fs'
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'yp82ef',
|
||||
defaultCommandTimeout: 24000, // 2x average block time
|
||||
chromeWebSecurity: false,
|
||||
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
|
||||
retries: { runMode: 2 },
|
||||
retries: { runMode: process.env.CYPRESS_RETRIES ? +process.env.CYPRESS_RETRIES : 1 },
|
||||
video: false, // GH provides 2 CPUs, and cypress video eats one up, see https://github.com/cypress-io/cypress/issues/20468#issuecomment-1307608025
|
||||
e2e: {
|
||||
async setupNodeEvents(on, config) {
|
||||
await setupHardhatEvents(on, config)
|
||||
codeCoverageTask(on, config)
|
||||
|
||||
// Delete recorded videos for specs that passed without flakes.
|
||||
on('after:spec', async (spec, results) => {
|
||||
if (results && results.video) {
|
||||
// If there were no failures (including flakes), delete the recorded video.
|
||||
if (!results.tests?.some((test) => test.attempts.some((attempt) => attempt?.state === 'failed'))) {
|
||||
unlinkSync(results.video)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...config,
|
||||
// Only enable Chrome.
|
||||
// Electron (the default) has issues injecting window.ethereum before pageload, so it is not viable.
|
||||
browsers: config.browsers.filter(({ name }) => name === 'chrome'),
|
||||
}
|
||||
return config
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
specPattern: 'cypress/{e2e,staging}/**/*.test.ts',
|
||||
},
|
||||
})
|
||||
|
||||
201
cypress/README.md
Normal file
201
cypress/README.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# e2e testing with Cypress
|
||||
|
||||
End-to-end tests are run through [Cypress](https://docs.cypress.io/api/table-of-contents/), which runs tests in a real browser. Cypress is a little different than other testing frameworks, and e2e tests are a little different than unit tests, so this directory has its own set of patterns, idioms, and best practices. Not only that, but we're testing against a forked blockchain, not just against typical Web APIs, so we have unique flows that you may not have seen elsewhere.
|
||||
|
||||
## Running your first e2e tests
|
||||
|
||||
Cypress tests run against a local server, so you'll need to run the application locally at the same time. The fastest way to run e2e tests is to use your dev server: `yarn start`.
|
||||
|
||||
Open cypress at the same time with `yarn cypress:open`. You should do this from another window or tab, so that you can continue to see any typechecking/linting warnings from `yarn start`.
|
||||
|
||||
Cypress opens its own instance of Chrome, with a list of "E2E specs" for you to select. When you're developing locally, you usually only want to run one spec file at a time. Select your spec by clicking on the filename and it will run.
|
||||
|
||||
## Glossary
|
||||
|
||||
#### spec
|
||||
Cypress considers each file a separate spec, or collection of tests.
|
||||
Specs are always run as a whole through `yarn cypress:open` or on the same machine through CI.
|
||||
|
||||
#### Thenable
|
||||
Cypress queues commands to run in the browser using [Thenables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables), not Promises.
|
||||
For this reason, you should not use `async/await` syntax in Cypress unless it is wholly-contained in a `cy.then` function argument.
|
||||
|
||||
## Writing your first e2e test
|
||||
|
||||
_For an excellent treatment on tests, check out the [Cypress Fundamentals](https://learn.cypress.io/cypress-fundamentals/how-to-write-a-test) course._
|
||||
_While some of that will be paraphrased here, this should be sufficient to get you started:_
|
||||
|
||||
### What is a test?
|
||||
|
||||
Cypress tests are just like any other test: you should set up an initial state, execute an action, and verify the action's consequence. This is codified in the AAA (Arrange-Act-Assert) pattern, and you'll see this in most of our tests. In _our_ case, it plays out as:
|
||||
|
||||
1. Arrange: Visit a page, eg `cy.visit('/swap')`, and set up the state, on the blockchain and the page.
|
||||
2. Act: Initiate your action under test, eg `initiateSwap()`
|
||||
3. Assert: Verify that the action has occured, eg `// Verify swap has occured`
|
||||
|
||||
You'll usually see the setup, followed by a newline, followed by assertions with comments stating what they are asserting.
|
||||
Because Cypress tests are translated into user actions, it may be hard to follow the action being described. You should use comments liberally to describe what you are doing and what you intend to test, to make tests easier to read and maintain in the future.
|
||||
|
||||
### Thinking about tests: queuing up a sequence of commands
|
||||
|
||||
Cypress uses `Thenable`s to achieve "command chaining". A test is described as a series of commands, which are only executed once the previous command in the chain has executed.
|
||||
|
||||
```
|
||||
cy.visit('/swap')
|
||||
cy.contains('Select token').click()
|
||||
cy.contains('DAI').click()
|
||||
```
|
||||
|
||||
In this example, `cy.contains('Select token').click()` is queued up right away (all the code is synchronous), but it will not execute until `/swap` has loaded (all the commands are chained); and `click()` will not execute until `Select token` has been found.
|
||||
|
||||
This becomes more relevant as you work with data on the blockchain, as you'll need to load it at the correct time, _after_ it's been modified by the application:
|
||||
|
||||
```
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// wait for the transaction to be executed
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// BAD: This will get the balance _before_ the other queued actions have executed.
|
||||
const balance = await hardhat.getBalance(hardhat.wallet, USDC_MAINNET)
|
||||
cy.wrap(balance).should('deep.equal', expectedBalance)
|
||||
})
|
||||
```
|
||||
|
||||
```
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// wait for the transaction to be executed
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// GOOD: cy.then chains the command so that it runs _after_ executing the swap
|
||||
cy.hardhat()
|
||||
.then((hardhat) => hardhat.getBalance(hardhat.wallet, USDC_MAINNET))
|
||||
.should('deep.equal', expectedBalance)
|
||||
})
|
||||
```
|
||||
|
||||
### Working with the blockchain (ie hardhat)
|
||||
|
||||
Our tests use a local hardhat node to simulate blockchain transactions. This can be accessed with `cy.hardhat().then((hardhat) => ...)`.
|
||||
|
||||
By default, automining is turned on, so that any transaction that you send to the blockchain is mined immediately. If you want to assert on intermediate states (between sending a transaction and mining it), you can turn off automining: `cy.hardhat({ automine: false })`.
|
||||
|
||||
The hardhat integration has built-in utilities to let you modify and assert on balances, approvals, and permits, and should be fully typed. Check it out at [Uniswap/cypress-hardhat](https://github.com/Uniswap/cypress-hardhat).
|
||||
|
||||
### Asserting on wallet methods
|
||||
|
||||
Wallet methods to hardhat are all aliased. If you'd like to assert that a method was sent to the wallet, you can do so using the method name, prefixed with `@`:
|
||||
|
||||
```
|
||||
// Asserts that `eth_sendRawTransaction` was sent to the wallet.
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
```
|
||||
|
||||
Sometimes, you may want a method to _fail_. In this case, you can stub it, but you should disable logging to avoid spamming the test:
|
||||
|
||||
```
|
||||
// Stub calls to eth_signTypedData_v4 and fail them
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Note the closure to keep signTypedDataStub in scope. Using closures instead of variables (eg let) helps prevent misuse of chaining.
|
||||
const signTypedDataStub = cy.stub(hardhat.provider, 'send').log(false)
|
||||
signTypedDataStub.withArgs('eth_signTypedData_v4).rejects(USER_REJECTION)
|
||||
signTypedDataStub.callThrough() // allws other methods to call through to hardhat
|
||||
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Verify the call occured
|
||||
// Note the call to cy.wrap to correctly queue the chained command. Without this, the test would occur before the stub is called.
|
||||
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
|
||||
|
||||
// Restore the stub
|
||||
// note the call to cy.then to correctly queue the chained command. Without this, the stub would be restored immediately.
|
||||
cy.then(() => permitApprovalStub.restore())
|
||||
})
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
<!-- Best practices should all be labeled using H3, with the rationale italicized at the end of the section. -->
|
||||
<!-- Best practice 🤣 is to also include an example before your rationale. -->
|
||||
|
||||
### Spec / test grouping
|
||||
|
||||
Each spec should be specific to one route, _not_ one functional behavior.
|
||||
For example, `token-details.test.ts` is separated from `swap.test.ts`.
|
||||
|
||||
If a route has different functional behaviors, that route should become a directory name, and its spec should be split.
|
||||
For example, `swap.test.ts` may be split into `swap/swap.test.ts`, `swap/wrap.test.ts`, `swap/permit2.test.ts`.
|
||||
|
||||
_This prevents specs from growing too large, which is important because they are always run as a whole locally and on the same machine through CI. If a spec grows too large, it will have a longer local feedback loop, and it will become the bottleneck for CI test runtime._
|
||||
|
||||
_Similarly, avoid actions outside the scope of your spec, as it will cause total testing time to increase._
|
||||
|
||||
### Use closures instead of variables
|
||||
|
||||
Avoid usage of `let`, instead assigning a constant. In practice, this means using closures for your variables:
|
||||
|
||||
```javascript
|
||||
let badVariable
|
||||
|
||||
cy.hardhat({ automine: false })
|
||||
.then((hardhat) => cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address)))
|
||||
.then((initialBalance) => {
|
||||
// Do not assign to a variable outside of your closure!
|
||||
badVariable = initialBalance // <-- bad!
|
||||
|
||||
// Use initial balance here, within the closure.
|
||||
})
|
||||
|
||||
cy.get('.class-name').then((el) => {
|
||||
// Do not use badVariable here! It may have changed value due to the queued async nature of Cypress.
|
||||
expect(el).should('contain', badVariable) // <-- bad!
|
||||
})
|
||||
```
|
||||
|
||||
_This prevents misuse of a not-yet-initialized variable, or a variable that has changed as the test progresses._
|
||||
|
||||
### Prefer selecting elements using on-screen text over data-testid attributes
|
||||
|
||||
When selecting components (eg with `cy.get`), prefer defining your selector with visible UI. Sometimes this is not possible (eg if the text is duplicated on-screen), and you'll need to add a `data-testid` property.
|
||||
|
||||
_Defining tests using visual fields helps ensure that we don't break them. `data-testid` may select an element that is only selectable programmatically, and should be used only when necessary, as its use may cover up UI breakages._
|
||||
|
||||
_You'll still want to use `data-testid` in cases where the text is rendered in multiple containers and you need to select the correct one, or where the component doesn't render predictable text output._
|
||||
|
||||
### Avoid branching logic
|
||||
|
||||
Do not write tests that rely on if-statements or conditionals. Do not create helper methods which do more than one thing, and rely on branching logic to apply to different but similar situations.
|
||||
|
||||
_Tests should be readable and simple. Branching logic makes it harder to reason about tests, and may hide otherwise flaky or ill-defined behaviors._
|
||||
|
||||
_Similarly, you should avoid complicated for-loops. Sometimes, for simple repetition, for-loops are ok._
|
||||
|
||||
### Avoid spamming the console
|
||||
|
||||
It is ok to include logging while you are developing a test, but that logging should be removed if it is not needed to debug (potential) errors.
|
||||
|
||||
For example, stubbing a wallet method will result in dumping a hex string (the calldata) to the log. Instead, suppress logging from methods which you know will flood the log.
|
||||
|
||||
```javascript
|
||||
cy.stub(hardhat.wallet, 'sendTransaction')
|
||||
.log(false) // <-- suppresses logs from this stub
|
||||
.rejects(new Error('user cancelled'))
|
||||
```
|
||||
|
||||
_Unnecessary logs it makes it harder to reason about a test overall._
|
||||
|
||||
### Name helper methods using transitive verbs
|
||||
|
||||
Name helper methods using "action verbs": `expectsThisToHappen`, not `expectThisToHappen`; `selectsToken(token: string)`, not `selectAToken(token: string)`.
|
||||
|
||||
_This makes your tests read more naturally, and makes it easier to follow given existing `should` syntax._
|
||||
@@ -4,61 +4,103 @@ import { aliasQuery, hasQuery } from '../utils/graphql-test-utils'
|
||||
|
||||
describe('Add Liquidity', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req) => {
|
||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3?source=uniswap', (req) => {
|
||||
aliasQuery(req, 'feeTierDistribution')
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/500')
|
||||
it('loads the token pair', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.contains('0.05% fee tier')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/add/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
it('does not crash if token is duplicated', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it.skip('token not in storage is loaded', () => {
|
||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it.skip('single token can be selected', () => {
|
||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
||||
it('single token can be selected', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it.skip('loads fee tier distribution', () => {
|
||||
it('loads fee tier distribution', () => {
|
||||
cy.fixture('feeTierDistribution.json').then((feeTierDistribution) => {
|
||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req: CyHttpMessages.IncomingHttpRequest) => {
|
||||
if (hasQuery(req, 'FeeTierDistributionQuery')) {
|
||||
req.alias = 'FeeTierDistributionQuery'
|
||||
cy.intercept(
|
||||
'POST',
|
||||
'/subgraphs/name/uniswap/uniswap-v3?source=uniswap',
|
||||
(req: CyHttpMessages.IncomingHttpRequest) => {
|
||||
if (hasQuery(req, 'FeeTierDistribution')) {
|
||||
req.alias = 'FeeTierDistribution'
|
||||
|
||||
req.reply({
|
||||
body: {
|
||||
data: {
|
||||
...feeTierDistribution,
|
||||
req.reply({
|
||||
body: {
|
||||
data: {
|
||||
...feeTierDistribution,
|
||||
},
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
'access-control-allow-origin': '*',
|
||||
},
|
||||
})
|
||||
headers: {
|
||||
'access-control-allow-origin': '*',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
|
||||
cy.wait('@FeeTierDistribution')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.30% fee tier')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40% select')
|
||||
})
|
||||
})
|
||||
|
||||
cy.wait('@FeeTierDistributionQuery')
|
||||
it('disables increment and decrement until initial prices are inputted', () => {
|
||||
// ETH / BITCOIN pool (0.05% tier not created)
|
||||
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
|
||||
// Set starting price in order to enable price range step counters
|
||||
cy.get('.start-price-input').type('1000')
|
||||
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.3% fee tier')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40%')
|
||||
// Min Price increment / decrement buttons should be disabled
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
|
||||
// Enter min price, which should enable the buttons
|
||||
cy.get('.rate-input-0').eq(0).type('900').blur()
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('not.be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('not.be.disabled')
|
||||
|
||||
// Repeat for Max Price step counter
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
|
||||
// Enter max price, which should enable the buttons
|
||||
cy.get('.rate-input-0').eq(1).type('1100').blur()
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('not.be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('allows full range selection on new pool creation', () => {
|
||||
// ETH / BITCOIN pool (0.05% tier not created)
|
||||
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
|
||||
// Set starting price in order to enable price range step counters
|
||||
cy.get('.start-price-input').type('1000')
|
||||
cy.get('[data-testid="set-full-range"]').click()
|
||||
// Check that the min price is 0 and the max price is infinity
|
||||
cy.get('.rate-input-0').eq(0).should('have.value', '0')
|
||||
cy.get('.rate-input-0').eq(1).should('have.value', '∞')
|
||||
// Increment and decrement buttons are disabled when full range is selected
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
|
||||
// Check that url params were added
|
||||
cy.url().then((url) => {
|
||||
const params = new URLSearchParams(url)
|
||||
const minPrice = params.get('minPrice')
|
||||
const maxPrice = params.get('maxPrice')
|
||||
// Note: although 0 and ∞ displayed, actual values in query are ticks at limit
|
||||
return minPrice && maxPrice && parseFloat(minPrice) < parseFloat(maxPrice)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
import { CONNECTED_WALLET_USER_STATE, DISCONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
|
||||
describe('Landing Page', () => {
|
||||
it('shows landing page when no user state exists', () => {
|
||||
cy.visit('/', { userState: {} })
|
||||
cy.visit('/', { userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
cy.get(getTestSelector('landing-page'))
|
||||
cy.screenshot()
|
||||
})
|
||||
@@ -39,4 +39,50 @@ describe('Landing Page', () => {
|
||||
cy.get(getTestSelector('pool-nav-link')).last().click()
|
||||
cy.url().should('include', '/pools')
|
||||
})
|
||||
|
||||
it('does not render landing page when / path is blocked', () => {
|
||||
cy.intercept('/', (req) => {
|
||||
req.reply((res) => {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(res.body, 'text/html')
|
||||
const meta = document.createElement('meta')
|
||||
meta.setAttribute('property', 'x:blocked-paths')
|
||||
meta.setAttribute('content', '/,/buy')
|
||||
doc.head.appendChild(meta)
|
||||
|
||||
res.body = doc.documentElement.outerHTML
|
||||
})
|
||||
})
|
||||
cy.visit('/', { userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
|
||||
cy.get(getTestSelector('landing-page')).should('not.exist')
|
||||
cy.get(getTestSelector('buy-fiat-button')).should('not.exist')
|
||||
cy.url().should('include', '/swap')
|
||||
})
|
||||
|
||||
it('does not render uk compliance banner in US', () => {
|
||||
cy.visit('/swap')
|
||||
cy.contains('UK disclaimer').should('not.exist')
|
||||
})
|
||||
|
||||
it('renders uk compliance banner in uk', () => {
|
||||
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const requestBody = JSON.stringify(req.body)
|
||||
const byteSize = new Blob([requestBody]).size
|
||||
req.alias = 'amplitude'
|
||||
req.reply(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
server_upload_time: Date.now(),
|
||||
payload_size_bytes: byteSize,
|
||||
events_ingested: req.body.events.length,
|
||||
}),
|
||||
{
|
||||
'origin-country': 'GB',
|
||||
}
|
||||
)
|
||||
})
|
||||
cy.visit('/swap')
|
||||
cy.contains('UK disclaimer')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
describe('Link', () => {
|
||||
it('should update route', () => {
|
||||
cy.viewport(2000, 1600)
|
||||
cy.visit('/')
|
||||
cy.visit('/swap')
|
||||
cy.contains('Pool').click()
|
||||
cy.get('[data-cy="join-pool-button"]').should('exist')
|
||||
})
|
||||
|
||||
@@ -2,30 +2,71 @@ import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Mini Portfolio account drawer', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(/api.uniswap.org\/v1\/graphql/, cy.spy().as('gqlSpy'))
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
const portfolioSpy = cy.spy().as('portfolioSpy')
|
||||
cy.intercept(/api.uniswap.org\/v1\/graphql/, (req) => {
|
||||
if (req.body.operationName === 'PortfolioBalances') {
|
||||
portfolioSpy(req)
|
||||
}
|
||||
})
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('fetches balances when account button is first hovered', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@gqlSpy').should('not.have.been.called')
|
||||
cy.get('@portfolioSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
it('should not re-fetch balances on second hover', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@portfolioSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
|
||||
// Balances should not be refetched upon second hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
it('should not re-fetch balances when the account drawer is opened', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@portfolioSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
|
||||
// Balances should not be refetched upon opening drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
// Balances should not be refetched upon closing & reopening drawer
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
it('fetches account information', () => {
|
||||
// Open the mini portfolio
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/tokens.json' })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
|
||||
// Verify that wallet state loads correctly
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Tokens')
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('Hidden (197)')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/nfts.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('I Got Plenty')
|
||||
|
||||
// Skip this for now, someone sent test account an NFT on block 17445713 that causes this test to fail
|
||||
// cy.intercept(/graphql/, { fixture: 'mini-portfolio/pools.json' })
|
||||
// cy.get(getTestSelector('mini-portfolio-navbar')).contains('Pools').click()
|
||||
// cy.get(getTestSelector('mini-portfolio-page')).contains('No pools yet')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/full_activity.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('Contract Interaction')
|
||||
})
|
||||
|
||||
it('refetches balances when account changes', () => {
|
||||
@@ -55,4 +96,36 @@ describe('Mini Portfolio account drawer', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fetches ENS name', () => {
|
||||
cy.hardhat().then(() => {
|
||||
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
|
||||
const haydenENS = 'hayden.eth'
|
||||
|
||||
// Opens the account drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// Simulate wallet changing to Hayden's account
|
||||
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
|
||||
|
||||
// Hayden's ENS name should be shown
|
||||
cy.contains(haydenENS).should('exist')
|
||||
|
||||
// Close account drawer
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
|
||||
// Switch chain to Polygon
|
||||
cy.get(getTestSelector('chain-selector')).eq(1).click()
|
||||
cy.contains('Polygon').click()
|
||||
|
||||
//Reopen account drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// Simulate wallet changing to Hayden's account
|
||||
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
|
||||
|
||||
// Hayden's ENS name should be shown
|
||||
cy.contains(haydenENS).should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,9 +94,7 @@ describe('mini-portfolio activity history', () => {
|
||||
})
|
||||
|
||||
it('should deduplicate activity history by nonce', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' }).hardhat({
|
||||
automine: false,
|
||||
})
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`).hardhat({ automine: false })
|
||||
|
||||
// Input swap info.
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
@@ -108,7 +106,7 @@ describe('mini-portfolio activity history', () => {
|
||||
|
||||
// Check activity history tab.
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-activity')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
|
||||
// Assert that the local pending transaction is replaced by a remote transaction with the same nonce.
|
||||
cy.contains('Swapping').should('not.exist')
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
||||
|
||||
describe('Testing nfts', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('should load nft leaderboard', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('nft-nav')).first().click()
|
||||
cy.get(getTestSelector('nft-nav')).first().should('exist')
|
||||
cy.get(getTestSelector('nft-nav')).first().click()
|
||||
@@ -16,7 +12,7 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should load pudgy penguin collection page', () => {
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-collection-asset')).should('exist')
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
@@ -24,13 +20,13 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should be able to navigate to activity', () => {
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-activity')).first().click()
|
||||
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
||||
})
|
||||
|
||||
it('should go to the details page', () => {
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||
cy.get(getTestSelector('nft-collection-asset')).first().click()
|
||||
@@ -41,7 +37,10 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should toggle buy now on details page', () => {
|
||||
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
|
||||
cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||
cy.get(getTestSelector('nft-collection-asset')).first().click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('exist')
|
||||
cy.get(getTestSelector('nft-details-description')).click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
||||
@@ -49,15 +48,11 @@ describe('Testing nfts', () => {
|
||||
cy.get(getTestSelector('nft-bag')).should('exist')
|
||||
})
|
||||
|
||||
it('should navigate to the owned nfts page', () => {
|
||||
it('should navigate to and from the owned nfts page', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('nft-view-self-nfts')).click()
|
||||
})
|
||||
|
||||
it('should close the sidebar when navigating to NFT details', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-nfts')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
|
||||
cy.contains('Buy crypto').should('not.be.visible')
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,216 +1,277 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { MaxUint160, MaxUint256 } from '@uniswap/permit2-sdk'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
|
||||
import { DAI, USDC_MAINNET } from '../../src/constants/tokens'
|
||||
import { DAI, USDC_MAINNET, USDT } from '../../src/constants/tokens'
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
/** Initiates a swap. */
|
||||
function initiateSwap() {
|
||||
// The swap button is re-rendered once enable, so we must wait until the original button is not disabled to re-select the appropriate button.
|
||||
// The swap button is re-rendered once enabled, so we must wait until the original button is not disabled to re-select the appropriate button.
|
||||
cy.get('#swap-button').should('not.be.disabled')
|
||||
// Completes the swap.
|
||||
cy.get('#swap-button').click()
|
||||
cy.get(getTestSelector('confirm-swap-button')).click()
|
||||
cy.contains('Confirm swap').click()
|
||||
}
|
||||
|
||||
describe('Permit2', () => {
|
||||
// The same tokens & swap-amount combination is used for all permit2 tests.
|
||||
const INPUT_TOKEN = DAI
|
||||
const OUTPUT_TOKEN = USDC_MAINNET
|
||||
const TEST_BALANCE_INCREMENT = 0.01
|
||||
|
||||
beforeEach(() => {
|
||||
// Sets up a swap between INPUT_TOKEN and OUTPUT_TOKEN.
|
||||
cy.visit(`/swap/?inputCurrency=${INPUT_TOKEN.address}&outputCurrency=${OUTPUT_TOKEN.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
})
|
||||
cy.get('#swap-currency-input .token-amount-input').type(TEST_BALANCE_INCREMENT.toString())
|
||||
})
|
||||
function setupInputs(inputToken: Token, outputToken: Token) {
|
||||
// Sets up a swap between inputToken and outputToken.
|
||||
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`)
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01')
|
||||
}
|
||||
|
||||
/** Asserts permit2 has a max approval for spend of the input token on-chain. */
|
||||
function expectTokenAllowanceForPermit2ToBeMax() {
|
||||
function expectTokenAllowanceForPermit2ToBeMax(inputToken: Token) {
|
||||
// check token approval
|
||||
return cy
|
||||
.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }))
|
||||
.should('deep.equal', MaxUint256)
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: inputToken }))
|
||||
.then((allowance) => {
|
||||
Cypress.log({ name: `Token allowance: ${allowance.toString()}` })
|
||||
cy.wrap(allowance).should('deep.equal', MaxUint256)
|
||||
})
|
||||
}
|
||||
|
||||
/** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */
|
||||
function expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime: number) {
|
||||
return cy
|
||||
.hardhat()
|
||||
.then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN }))
|
||||
function expectPermit2AllowanceForUniversalRouterToBeMax(inputToken: Token) {
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getPermit2Allowance({ owner: wallet, token: inputToken }))
|
||||
.then((allowance) => {
|
||||
cy.wrap(MaxUint160.eq(allowance.amount)).should('eq', true)
|
||||
Cypress.log({ name: `Permit2 allowance: ${allowance.amount.toString()}` })
|
||||
cy.wrap(allowance.amount).should('deep.equal', MaxUint160)
|
||||
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
|
||||
const expected = Math.floor((approvalTime + 2_592_000_000) / 1000)
|
||||
const THIRTY_DAYS_SECONDS = 2_592_000
|
||||
const expected = Math.floor(Date.now() / 1000 + THIRTY_DAYS_SECONDS)
|
||||
cy.wrap(allowance.expiration).should('be.closeTo', expected, 40)
|
||||
})
|
||||
}
|
||||
|
||||
it('swaps when user has already approved token and permit2', () => {
|
||||
cy.hardhat().then(({ approval, wallet }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
|
||||
beforeEach(() =>
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(DAI, 1e18))
|
||||
await hardhat.mine()
|
||||
})
|
||||
)
|
||||
|
||||
describe('approval process (with intermediate screens)', () => {
|
||||
// Turn off automine so that intermediate screens are available to assert on.
|
||||
beforeEach(() => cy.hardhat({ automine: false }))
|
||||
|
||||
it('swaps after completing full permit2 approval process', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
initiateSwap()
|
||||
|
||||
// verify that the modal retains its state when the window loses focus
|
||||
cy.window().trigger('blur')
|
||||
|
||||
// Verify token approval
|
||||
cy.contains('Enable spending DAI on Uniswap')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax(DAI)
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.contains('Allow DAI to be used for swapping')
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.contains('Swap submitted')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
|
||||
it('swaps with existing permit approval and missing token approval', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI })
|
||||
await hardhat.mine()
|
||||
})
|
||||
initiateSwap()
|
||||
|
||||
// Verify token approval
|
||||
cy.contains('Enable spending DAI on Uniswap')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax(DAI)
|
||||
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
/**
|
||||
* On mainnet, you have to revoke USDT approval before increasing it.
|
||||
* From the token contract:
|
||||
* To change the approve amount you first have to reduce the addresses`
|
||||
* allowance to zero by calling `approve(_spender, 0)` if it is not
|
||||
* already 0 to mitigate the race condition described here:
|
||||
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
|
||||
*/
|
||||
it('swaps USDT with existing permit, and existing but insufficient token approval', () => {
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6))
|
||||
await hardhat.mine()
|
||||
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDT }, 1e6)
|
||||
await hardhat.mine()
|
||||
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDT })
|
||||
await hardhat.mine()
|
||||
})
|
||||
setupInputs(USDT, USDC_MAINNET)
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('2')
|
||||
initiateSwap()
|
||||
|
||||
// Verify allowance revocation
|
||||
cy.contains('Reset USDT')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: USDT }))
|
||||
.should('deep.equal', BigNumber.from(0))
|
||||
|
||||
// Verify token approval
|
||||
cy.contains('Enable spending USDT on Uniswap')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax(USDT)
|
||||
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
it('swaps USDT with existing permit, and existing and sufficient token approval', () => {
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6))
|
||||
await hardhat.mine()
|
||||
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDT }, 1e6)
|
||||
await hardhat.mine()
|
||||
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDT })
|
||||
await hardhat.mine()
|
||||
})
|
||||
setupInputs(USDT, USDC_MAINNET)
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
initiateSwap()
|
||||
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
initiateSwap()
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
// Verifies that there is a successful swap notification.
|
||||
cy.contains('Swapped').should('exist')
|
||||
})
|
||||
|
||||
it('swaps after completing full permit2 approval process', () => {
|
||||
cy.hardhat().then(({ provider }) => {
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
})
|
||||
it('swaps when user has already approved token and permit2', () => {
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }),
|
||||
approval.setPermit2Allowance({ owner: wallet, token: DAI }),
|
||||
])
|
||||
)
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
initiateSwap()
|
||||
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
||||
cy.contains('Approved').should('exist')
|
||||
|
||||
cy.contains('Allow DAI to be used for swapping').should('exist')
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectTokenAllowanceForPermit2ToBeMax()
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
})
|
||||
// Verify transaction
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
it('swaps after handling user rejection of both approval and signature', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
const USER_REJECTION = { code: 4001 }
|
||||
cy.hardhat().then((hardhat) => {
|
||||
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction')
|
||||
tokenApprovalStub.rejects(USER_REJECTION) // reject token approval
|
||||
const permitApprovalStub = cy.stub(hardhat.provider, 'send')
|
||||
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // reject permit approval
|
||||
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
|
||||
|
||||
// Reject token approval
|
||||
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction').log(false)
|
||||
tokenApprovalStub.rejects(USER_REJECTION) // rejects token approval
|
||||
initiateSwap()
|
||||
|
||||
// tokenApprovalStub should reject here, and the modal should revert to the review state.
|
||||
cy.contains('Review swap').should('be.visible')
|
||||
|
||||
cy.then(() => {
|
||||
// The user is now allowing approval, but the permit2 signature will be rejected by the user (permitApprovalStub).
|
||||
tokenApprovalStub.restore() // allow token approval
|
||||
})
|
||||
|
||||
cy.get(getTestSelector('confirm-swap-button')).click()
|
||||
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
||||
cy.contains('Approved').should('exist')
|
||||
|
||||
// permitApprovalStub should reject here, and the modal should revert to the review state.
|
||||
// Verify token approval rejection
|
||||
cy.wrap(tokenApprovalStub).should('be.calledOnce')
|
||||
cy.contains('Review swap')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
permitApprovalStub.restore() // allow permit approval
|
||||
})
|
||||
|
||||
cy.get(getTestSelector('confirm-swap-button')).click()
|
||||
// Allow token approval
|
||||
cy.then(() => tokenApprovalStub.restore())
|
||||
|
||||
// The swap should now be able to proceed, as the permit2 signature will be accepted by the user.
|
||||
const approvalTime = Date.now()
|
||||
// Reject permit2 approval
|
||||
const permitApprovalStub = cy.stub(hardhat.provider, 'send').log(false)
|
||||
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // rejects permit approval
|
||||
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
// Verify token approval
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax(DAI)
|
||||
|
||||
expectTokenAllowanceForPermit2ToBeMax()
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
})
|
||||
})
|
||||
// Verify permit2 approval rejection
|
||||
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
|
||||
cy.contains('Review swap')
|
||||
|
||||
it('swaps with existing token approval and missing permit approval', () => {
|
||||
cy.hardhat().then(({ approval, wallet, provider }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
})
|
||||
cy.then(() => initiateSwap())
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
// Allow permit2 approval
|
||||
cy.then(() => permitApprovalStub.restore())
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
})
|
||||
})
|
||||
|
||||
it('swaps with existing permit approval and missing token approval', () => {
|
||||
cy.hardhat().then(({ approval, wallet }) => approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }))
|
||||
cy.then(() => {
|
||||
initiateSwap()
|
||||
})
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
})
|
||||
})
|
||||
|
||||
it('prompts signature when existing permit approval is expired', () => {
|
||||
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
|
||||
|
||||
cy.hardhat().then(({ approval, wallet, provider }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, expiredAllowance)
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
})
|
||||
cy.then(() => {
|
||||
initiateSwap()
|
||||
})
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
})
|
||||
})
|
||||
|
||||
it('prompts signature when existing permit approval amount is too low', () => {
|
||||
const smallAllowance = { amount: 1 }
|
||||
|
||||
cy.hardhat().then(({ approval, wallet, provider }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, smallAllowance)
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
initiateSwap()
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
// Verify permit2 approval
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
})
|
||||
|
||||
it('prompts token approval when existing approval amount is too low', () => {
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => {
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }, 1)
|
||||
})
|
||||
.then(() => {
|
||||
initiateSwap()
|
||||
const approvalTime = Date.now()
|
||||
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setPermit2Allowance({ owner: wallet, token: DAI }),
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }, 1),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
// Verify token approval
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
})
|
||||
it('prompts signature when existing permit approval is expired', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }),
|
||||
approval.setPermit2Allowance({ owner: wallet, token: DAI }, expiredAllowance),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
|
||||
it('prompts signature when existing permit approval amount is too low', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
const smallAllowance = { amount: 1 }
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }),
|
||||
approval.setPermit2Allowance({ owner: wallet, token: DAI }, smallAllowance),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
import { ChainId, MaxUint256, UNI_ADDRESSES } from '@uniswap/sdk-core'
|
||||
|
||||
const UNI_MAINNET = UNI_ADDRESSES[ChainId.MAINNET]
|
||||
|
||||
describe('Remove Liquidity', () => {
|
||||
it('eth remove', () => {
|
||||
cy.visit('/remove/v2/ETH/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
it('loads the token pair in v2', () => {
|
||||
cy.visit(`/remove/v2/ETH/${UNI_MAINNET}`)
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('eth remove swap order', () => {
|
||||
cy.visit('/remove/v2/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'UNI')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
|
||||
it('loads the token pair in v3', () => {
|
||||
cy.visit(`/remove/1`)
|
||||
cy.get('#remove-liquidity-tokens').should('contain.text', 'UNI/ETH')
|
||||
|
||||
cy.get('#remove-pooled-tokena-symbol').should('contain.text', 'Pooled UNI')
|
||||
cy.get('#remove-pooled-tokenb-symbol').should('contain.text', 'Pooled ETH')
|
||||
})
|
||||
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||
})
|
||||
it('should redirect to error pages if pool does not exist', () => {
|
||||
// Duplicate-token v2 pools redirect to position unavailable
|
||||
cy.visit(`/remove/v2/ETH/ETH`)
|
||||
cy.contains('Position unavailable')
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
|
||||
// Single-token pools don't exist
|
||||
cy.visit('/remove/v2/ETH')
|
||||
cy.url().should('match', /\/not-found/)
|
||||
|
||||
// Nonexistent v3 pool
|
||||
cy.visit(`/remove/${MaxUint256}`)
|
||||
cy.contains('Position unavailable')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,68 +1,84 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
import { InterfaceSectionName } from '@uniswap/analytics-events'
|
||||
import { CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { FeatureFlag } from 'featureFlags'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
|
||||
import { DAI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
|
||||
describe('Swap errors', () => {
|
||||
it('wallet rejection', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Stub the wallet to reject any transaction.
|
||||
cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled'))
|
||||
|
||||
// Attempt to swap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
|
||||
cy.contains('Review swap').should('exist')
|
||||
cy.get('body').click('topRight')
|
||||
cy.contains('Review swap').should('not.exist')
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_estimateGas')
|
||||
|
||||
// Verify rejection
|
||||
cy.contains('Review swap')
|
||||
cy.contains('Confirm swap')
|
||||
})
|
||||
})
|
||||
|
||||
it('transaction past deadline', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Set deadline to minimum. (1 minute)
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('transaction-deadline-settings')).click()
|
||||
cy.get(getTestSelector('deadline-input')).clear().type('1') // 1 minute
|
||||
|
||||
// Click outside of modal to dismiss it.
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('deadline-input')).should('not.exist')
|
||||
|
||||
// Attempt to swap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Swap submitted')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine(1, /* 10 minutes */ 1000 * 60 * 10)) // mines past the deadline
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
// Remove the transaction from the mempool, so that it doesn't fail but it is past the deadline.
|
||||
// This should result in it being removed from pending transactions, without a failure notificiation.
|
||||
const transactions = await hardhat.send('eth_pendingTransactions', [])
|
||||
await hardhat.send('hardhat_dropTransaction', [transactions[0].hash])
|
||||
// Mine past the deadline
|
||||
await hardhat.mine(1, DEFAULT_DEADLINE_FROM_NOW + 1)
|
||||
})
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction did not occur
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
|
||||
|
||||
// Verify the balance is unchanged.
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should('have.text', `Balance: ${initialBalance}`)
|
||||
cy.get(getTestSelector('popups')).should('not.contain', 'Swap failed')
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${initialBalance}`)
|
||||
getBalance(USDC_MAINNET).should('eq', initialBalance)
|
||||
})
|
||||
})
|
||||
|
||||
it('slippage failure', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
|
||||
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
|
||||
})
|
||||
cy.hardhat({ automine: false }).then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 500e6))
|
||||
await hardhat.mine()
|
||||
await Promise.all([
|
||||
hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDC_MAINNET }),
|
||||
hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDC_MAINNET }),
|
||||
])
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
getBalance(DAI).then((initialBalance) => {
|
||||
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
|
||||
cy.hardhat().then((hardhat) => {
|
||||
const send = cy.stub(hardhat.provider, 'send').log(false)
|
||||
@@ -74,43 +90,49 @@ describe('Swap errors', () => {
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('max-slippage-settings')).click()
|
||||
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
|
||||
|
||||
// Click outside of modal to dismiss it.
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('toggle-uniswap-x-button')).click() // turn off uniswapx
|
||||
cy.get('body').click('topRight') // close modal
|
||||
cy.get(getTestSelector('slippage-input')).should('not.exist')
|
||||
|
||||
// Swap 2 times.
|
||||
const AMOUNT_TO_SWAP = 200
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.clear()
|
||||
.type(AMOUNT_TO_SWAP.toString())
|
||||
.should('have.value', AMOUNT_TO_SWAP.toString())
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.clear()
|
||||
.type(AMOUNT_TO_SWAP.toString())
|
||||
.should('have.value', AMOUNT_TO_SWAP.toString())
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
// Submit 2 transactions
|
||||
for (let i = 0; i < 2; i++) {
|
||||
cy.get('#swap-currency-input .token-amount-input').type('200').should('have.value', '200')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Swap submitted')
|
||||
if (i === 0) {
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
}
|
||||
}
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
|
||||
|
||||
// Mine transactions
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
cy.contains('Swap failed')
|
||||
|
||||
// Verify only 1 transaction occurred
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
|
||||
|
||||
// Assert that the transactions were unsuccessful by checking on-chain balance.
|
||||
getBalance(UNI_MAINNET).should('equal', initialBalance)
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
cy.get(getTestSelector('popups')).contains('Swap failed')
|
||||
getBalance(DAI).should('be.closeTo', initialBalance + 200, 1)
|
||||
})
|
||||
})
|
||||
|
||||
it('insufficient liquidity', () => {
|
||||
// The API response is too variable so stubbing a 404.
|
||||
cy.intercept('POST', 'https://api.uniswap.org/v2/quote', {
|
||||
statusCode: 404,
|
||||
fixture: 'insufficientLiquidity.json',
|
||||
})
|
||||
|
||||
cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
cy.get('#swap-currency-output .token-amount-input').type('100000000000000').should('have.value', '100000000000000') // 100 trillion
|
||||
cy.contains('Insufficient liquidity for this trade.')
|
||||
cy.get('#swap-button').should('not.exist')
|
||||
cy.get(getTestSelector(`fiat-value-${InterfaceSectionName.CURRENCY_OUTPUT_PANEL}`)).contains('-')
|
||||
})
|
||||
})
|
||||
|
||||
149
cypress/e2e/swap/fees.test.ts
Normal file
149
cypress/e2e/swap/fees.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { FeatureFlag } from 'featureFlags'
|
||||
|
||||
import { USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
describe.skip('Swap with fees', () => {
|
||||
describe('Classic swaps', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap', { featureFlags: [{ name: FeatureFlag.feesEnabled, value: true }] })
|
||||
|
||||
// Store trade quote into alias
|
||||
cy.intercept({ url: 'https://api.uniswap.org/v2/quote' }, (req) => {
|
||||
// Avoid tracking stablecoin pricing fetches
|
||||
if (JSON.parse(req.body).intent !== 'pricing') req.alias = 'quoteFetch'
|
||||
})
|
||||
})
|
||||
|
||||
it('displays $0 fee on swaps without fees', () => {
|
||||
// Set up a stablecoin <> stablecoin swap (no fees)
|
||||
cy.get('#swap-currency-input .open-currency-select-button').click()
|
||||
cy.contains('DAI').click()
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('USDC').click()
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1')
|
||||
|
||||
// Verify 0 fee UI is displayed
|
||||
cy.get(getTestSelector('swap-details-header-row')).click()
|
||||
cy.contains('Fee')
|
||||
cy.contains('$0')
|
||||
})
|
||||
|
||||
it('swaps ETH for USDC exact-out with swap fee', () => {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Set up swap
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('USDC').click()
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1')
|
||||
|
||||
cy.wait('@quoteFetch')
|
||||
.its('response.body')
|
||||
.then(({ quote: { portionBips, portionRecipient, portionAmount } }) => {
|
||||
// Fees are generally expected to always be enabled for ETH -> USDC swaps
|
||||
// If the routing api does not include a fee, end the test early rather than manually update routes and hardcode fee vars
|
||||
if (portionRecipient) return
|
||||
|
||||
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((initialRecipientBalance) => {
|
||||
const feeCurrencyAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, portionAmount)
|
||||
|
||||
// Initiate transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Review swap')
|
||||
|
||||
// Verify fee percentage and amount is displayed
|
||||
cy.contains(`Fee (${portionBips / 100}%)`)
|
||||
|
||||
// Confirm transaction
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
|
||||
// Verify the post-fee output is the expected exact-out amount
|
||||
const finalBalance = initialBalance + 1
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(USDC_MAINNET).should('eq', finalBalance)
|
||||
|
||||
// Verify fee recipient received fee
|
||||
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((finalRecipientBalance) => {
|
||||
const expectedFinalRecipientBalance = initialRecipientBalance.add(feeCurrencyAmount)
|
||||
cy.then(() => finalRecipientBalance.equalTo(expectedFinalRecipientBalance)).should('be.true')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('swaps ETH for USDC exact-in with swap fee', () => {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Set up swap
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('USDC').click()
|
||||
cy.get('#swap-currency-input .token-amount-input').type('.01')
|
||||
|
||||
cy.wait('@quoteFetch')
|
||||
.its('response.body')
|
||||
.then(({ quote: { portionBips, portionRecipient } }) => {
|
||||
// Fees are generally expected to always be enabled for ETH -> USDC swaps
|
||||
// If the routing api does not include a fee, end the test early rather than manually update routes and hardcode fee vars
|
||||
if (portionRecipient) return
|
||||
|
||||
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((initialRecipientBalance) => {
|
||||
// Initiate transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Review swap')
|
||||
|
||||
// Verify fee percentage and amount is displayed
|
||||
cy.contains(`Fee (${portionBips / 100}%)`)
|
||||
|
||||
// Confirm transaction
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
|
||||
// Verify fee recipient received fee
|
||||
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((finalRecipientBalance) => {
|
||||
cy.then(() => finalRecipientBalance.greaterThan(initialRecipientBalance)).should('be.true')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UniswapX swaps', () => {
|
||||
it('displays UniswapX fee in UI', () => {
|
||||
cy.visit('/swap', {
|
||||
featureFlags: [
|
||||
{ name: FeatureFlag.feesEnabled, value: true },
|
||||
{ name: FeatureFlag.uniswapXDefaultEnabled, value: true },
|
||||
],
|
||||
})
|
||||
|
||||
// Intercept the trade quote
|
||||
cy.intercept({ url: 'https://api.uniswap.org/v2/quote' }, (req) => {
|
||||
// Avoid intercepting stablecoin pricing fetches
|
||||
if (JSON.parse(req.body).intent !== 'pricing') {
|
||||
req.reply({ fixture: 'uniswapx/feeQuote.json' })
|
||||
}
|
||||
})
|
||||
|
||||
// Setup swap
|
||||
cy.get('#swap-currency-input .open-currency-select-button').click()
|
||||
cy.contains('USDC').click()
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('ETH').click()
|
||||
cy.get('#swap-currency-input .token-amount-input').type('200')
|
||||
|
||||
// Verify fee UI is displayed
|
||||
cy.get(getTestSelector('swap-details-header-row')).click()
|
||||
cy.contains('Fee (0.15%)')
|
||||
})
|
||||
})
|
||||
})
|
||||
76
cypress/e2e/swap/logging.test.ts
Normal file
76
cypress/e2e/swap/logging.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
|
||||
import { USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('swap flow logging', () => {
|
||||
it('completes two swaps and verifies the TTS logging for the first, plus all intermediate steps along the way', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.hardhat()
|
||||
|
||||
// First swap in the session:
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Verify first swap action
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_FIRST_ACTION).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'time_to_first_swap_action')
|
||||
cy.wrap(event.event_properties.time_to_first_swap_action).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_first_swap_action).should('be.gte', 0)
|
||||
})
|
||||
|
||||
// Verify Swap Quote
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_FETCH).then((event: any) => {
|
||||
// Price quotes don't include these values, so we only verify the types if they exist
|
||||
if (event.event_properties.time_to_first_quote_request) {
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request).should('be.gte', 0)
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request_since_first_input).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request_since_first_input).should('be.gte', 0)
|
||||
}
|
||||
})
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
|
||||
// Verify logging
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'time_to_swap')
|
||||
cy.wrap(event.event_properties.time_to_swap).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_swap).should('be.gte', 0)
|
||||
cy.wrap(event.event_properties).should('have.property', 'time_to_swap_since_first_input')
|
||||
cy.wrap(event.event_properties.time_to_swap_since_first_input).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_swap_since_first_input).should('be.gte', 0)
|
||||
})
|
||||
|
||||
// Second swap in the session:
|
||||
// Enter amount to swap (different from first trade, to trigger a new quote request)
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('10').should('have.value', '10')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Verify second Swap Quote
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_FETCH).then((event: any) => {
|
||||
// Price quotes don't include these values, so we only verify the types if they exist
|
||||
if (event.event_properties.time_to_first_quote_request) {
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request).should('be.undefined')
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request_since_first_input).should('be.undefined')
|
||||
}
|
||||
})
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('not.have.property', 'time_to_swap')
|
||||
cy.wrap(event.event_properties).should('not.have.property', 'time_to_swap_since_first_input')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,10 +5,30 @@ describe('Swap settings', () => {
|
||||
cy.visit('/swap')
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.get(getTestSelector('mobile-settings-menu')).should('not.exist')
|
||||
cy.contains('Max. slippage').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.contains('Auto Router API').should('exist')
|
||||
cy.contains('UniswapX').should('exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Settings').should('not.exist')
|
||||
})
|
||||
|
||||
it('should open the mobile settings menu', () => {
|
||||
// Set viewport to iPhone 6
|
||||
cy.viewport('iphone-6')
|
||||
cy.visit('/swap')
|
||||
|
||||
// Click the button to open the settings dialog
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click({ waitForAnimations: true })
|
||||
|
||||
// Verify the mobile settings menu and its contents
|
||||
cy.get(getTestSelector('mobile-settings-menu'))
|
||||
.should('exist')
|
||||
.within(() => {
|
||||
cy.contains('Max. slippage').should('exist')
|
||||
cy.contains('UniswapX').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.get(getTestSelector('mobile-settings-close')).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
const UNI_MAINNET = UNI[ChainId.MAINNET]
|
||||
|
||||
describe('Swap', () => {
|
||||
describe('Swap on main page', () => {
|
||||
@@ -37,33 +38,60 @@ describe('Swap', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
})
|
||||
|
||||
it('resets the dependent input when the independent input is cleared', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`)
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('have.value', '')
|
||||
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value', '')
|
||||
cy.get('#swap-currency-input .token-amount-input').clear()
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
|
||||
cy.window().trigger('blur')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
})
|
||||
|
||||
it('swaps ETH for USDC', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.visit('/swap')
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Select USDC
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
|
||||
cy.contains('USDC').click()
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
cy.get(getTestSelector('token-search-input')).type(USDC_MAINNET.address)
|
||||
cy.get(getTestSelector('common-base-USDC')).click()
|
||||
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Verify logging
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_RECEIVED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'quote_latency_milliseconds')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.gte', 0)
|
||||
})
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Review swap')
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Swap submitted')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.contains('Swap submitted').should('not.exist')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swapped')
|
||||
|
||||
// Verify the balance is updated.
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialBalance + 1}`
|
||||
)
|
||||
getBalance(USDC_MAINNET).should('eq', initialBalance + 1)
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
const finalBalance = initialBalance + 1
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(USDC_MAINNET).should('eq', finalBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
430
cypress/e2e/swap/uniswapx.test.ts
Normal file
430
cypress/e2e/swap/uniswapx.test.ts
Normal file
@@ -0,0 +1,430 @@
|
||||
import { ChainId, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { CyHttpMessages } from 'cypress/types/net-stubbing'
|
||||
import { FeatureFlag } from 'featureFlags'
|
||||
|
||||
import { DAI, nativeOnChain, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
const QuoteWhereUniswapXIsBetter = 'uniswapx/quote1.json'
|
||||
const QuoteWithEthInput = 'uniswapx/quote2.json'
|
||||
|
||||
const QuoteEndpoint = 'https://api.uniswap.org/v2/quote'
|
||||
const OrderSubmissionEndpoint = 'https://api.uniswap.org/v2/order'
|
||||
const OrderStatusEndpoint =
|
||||
'https://api.uniswap.org/v2/orders?swapper=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266&orderHashes=0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19'
|
||||
|
||||
/**
|
||||
* Stubs quote to return a quote for non-price requests
|
||||
* Price quotes are blocked with 409, as the backend would not accept them regardless
|
||||
*/
|
||||
function stubNonPriceQuoteWith(fixture: string) {
|
||||
cy.intercept(QuoteEndpoint, (req: CyHttpMessages.IncomingHttpRequest) => {
|
||||
let body = req.body
|
||||
if (typeof body === 'string') {
|
||||
body = JSON.parse(body)
|
||||
}
|
||||
if (body.intent === 'pricing') {
|
||||
req.reply({ statusCode: 409 })
|
||||
} else {
|
||||
req.reply({ fixture })
|
||||
}
|
||||
}).as('quote')
|
||||
}
|
||||
|
||||
/** Stubs the provider to return a tx receipt corresponding to the mock filled uniswapx order's txHash */
|
||||
function stubSwapTxReceipt() {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
cy.fixture('uniswapx/fillTransactionReceipt.json').then((mockTxReceipt) => {
|
||||
const getTransactionReceiptStub = cy.stub(hardhat.provider, 'getTransactionReceipt').log(false)
|
||||
getTransactionReceiptStub.withArgs(mockTxReceipt.transactionHash).resolves(mockTxReceipt)
|
||||
getTransactionReceiptStub.callThrough()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: FIX THESE TESTS where we should NOT stub for pricing requests
|
||||
describe.skip('UniswapX Toggle', () => {
|
||||
beforeEach(() => {
|
||||
stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter)
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
|
||||
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
|
||||
})
|
||||
})
|
||||
|
||||
it('displays uniswapx ui when setting is on', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.wait('@quote')
|
||||
|
||||
// UniswapX UI should not be visible
|
||||
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('not.exist')
|
||||
|
||||
// Opt-in to UniswapX
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// UniswapX UI should be visible
|
||||
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('exist')
|
||||
})
|
||||
|
||||
it('prompts opt-in if UniswapX is better', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.wait('@quote')
|
||||
|
||||
// UniswapX should not display in gas estimate row before opt-in
|
||||
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('not.exist')
|
||||
|
||||
// UniswapX mustache should be visible
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Opt-in dialog should now be hidden
|
||||
cy.contains('Try it now').should('not.be.visible')
|
||||
|
||||
// UniswapX should display in gas estimate row
|
||||
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('exist')
|
||||
|
||||
// Opt-in dialog should not reappear if user manually toggles UniswapX off
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('toggle-uniswap-x-button')).click()
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Try it now').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('UniswapX Orders', () => {
|
||||
beforeEach(() => {
|
||||
stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter)
|
||||
cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' })
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/openStatusResponse.json' })
|
||||
|
||||
stubSwapTxReceipt()
|
||||
|
||||
cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8)))
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
|
||||
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
|
||||
})
|
||||
})
|
||||
|
||||
it('can swap exact-in trades using uniswapX', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.wait('@quote')
|
||||
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap submitted')
|
||||
cy.contains('Learn more about swapping with UniswapX')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
// Verify swap success
|
||||
cy.contains('Swapped')
|
||||
})
|
||||
|
||||
it('can swap exact-out trades using uniswapX', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('300')
|
||||
cy.wait('@quote')
|
||||
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap submitted')
|
||||
cy.contains('Learn more about swapping with UniswapX')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
// Verify swap success
|
||||
cy.contains('Swapped')
|
||||
})
|
||||
|
||||
it('renders proper view if uniswapx order expires', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.wait('@quote')
|
||||
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Return expired order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/expiredStatusResponse.json' })
|
||||
|
||||
// Verify swap failure message
|
||||
cy.contains('Swap expired')
|
||||
})
|
||||
|
||||
it('renders proper view if uniswapx order has insufficient funds', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.wait('@quote')
|
||||
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Return insufficient_funds order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/insufficientFundsStatusResponse.json' })
|
||||
|
||||
// Verify swap failure message
|
||||
cy.contains('Insufficient funds')
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('UniswapX Eth Input', () => {
|
||||
beforeEach(() => {
|
||||
stubNonPriceQuoteWith(QuoteWithEthInput)
|
||||
cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' })
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/openStatusResponse.json' })
|
||||
|
||||
// Turn off automine so that intermediate screens are available to assert on.
|
||||
cy.hardhat({ automine: false }).then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(nativeOnChain(ChainId.MAINNET), 2e18))
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
stubSwapTxReceipt()
|
||||
|
||||
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`, {
|
||||
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
|
||||
})
|
||||
})
|
||||
|
||||
it('can swap using uniswapX with ETH as input', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('1')
|
||||
|
||||
cy.wait('@quote')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Prompt ETH wrap to use for order
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.contains('Wrap ETH')
|
||||
|
||||
// Wrap ETH
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.contains('Pending...')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Wrapped')
|
||||
|
||||
// Approve WETH spend
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
|
||||
// Verify signed order submission
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap submitted')
|
||||
cy.contains('Learn more about swapping with UniswapX')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
// Verify swap success
|
||||
cy.contains('Swapped')
|
||||
})
|
||||
|
||||
it('keeps ETH as the input currency before wrap completes', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('1')
|
||||
cy.wait('@quote')
|
||||
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Prompt ETH wrap and confirm
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
|
||||
// Close review modal before wrap is confirmed on chain
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
// Confirm ETH is still the input token before wrap succeeds
|
||||
cy.contains('ETH')
|
||||
})
|
||||
|
||||
it('switches swap input to WETH after wrap', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('1')
|
||||
cy.wait('@quote')
|
||||
|
||||
// Prompt ETH wrap and confirm
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
|
||||
// Confirm wrap is successful and WETH is now input token
|
||||
cy.contains('Wrapped')
|
||||
cy.contains('WETH')
|
||||
|
||||
// Approve WETH spend
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap submitted')
|
||||
cy.contains('Learn more about swapping with UniswapX')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
// Verify swap success
|
||||
cy.contains('Swapped')
|
||||
|
||||
// Close modal
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
// The input currency should now be WETH
|
||||
cy.contains('WETH')
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('UniswapX activity history', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(QuoteEndpoint, { fixture: QuoteWhereUniswapXIsBetter })
|
||||
cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' })
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/openStatusResponse.json' })
|
||||
|
||||
stubSwapTxReceipt()
|
||||
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))
|
||||
})
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
|
||||
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
|
||||
})
|
||||
})
|
||||
|
||||
it('can view UniswapX order status progress in activity', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// Open mini portfolio and navigate to activity history
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/empty_activity.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
|
||||
// Open pending order modal
|
||||
cy.contains('Swapping').click()
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Swapping')
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Learn more about swapping with UniswapX')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Swapped')
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('View on Explorer')
|
||||
})
|
||||
|
||||
it('can view UniswapX order status progress in activity upon expiry', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// Open mini portfolio and navigate to activity history
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/empty_activity.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
|
||||
// Open pending order modal
|
||||
cy.contains('Swapping').click()
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Swapping')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/expiredStatusResponse.json' })
|
||||
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Swap expired')
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('learn more')
|
||||
})
|
||||
|
||||
it('deduplicates remote vs local uniswapx orders', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
cy.contains('Swapped')
|
||||
|
||||
// Open mini portfolio
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
cy.fixture('mini-portfolio/uniswapx_activity.json').then((uniswapXActivity) => {
|
||||
// Replace fixture's timestamp with current time
|
||||
uniswapXActivity.data.portfolios[0].assetActivities[0].timestamp = Date.now() / 1000
|
||||
cy.intercept(/graphql/, uniswapXActivity)
|
||||
})
|
||||
|
||||
// Open activity history
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
|
||||
// Ensure gql and local order have been deduped, such that there is one swap activity listed
|
||||
cy.get(getTestSelector('activity-content')).contains('Swapped').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('balances should refetch after uniswapx swap', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
const gqlSpy = cy.spy().as('gqlSpy')
|
||||
cy.intercept(/graphql/, (req) => {
|
||||
// Spy on request frequency
|
||||
req.on('response', gqlSpy)
|
||||
// Reply with a fixture to speed up test
|
||||
req.reply({
|
||||
fixture: 'mini-portfolio/tokens.json',
|
||||
})
|
||||
})
|
||||
|
||||
// Expect balances to fetch upon opening mini portfolio
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Expect balances to refetch after approval
|
||||
cy.get('@gqlSpy').should('have.been.calledTwice')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
// Expect balances to refetch after swap
|
||||
cy.get('@gqlSpy').should('have.been.calledThrice')
|
||||
})
|
||||
})
|
||||
@@ -1,18 +1,16 @@
|
||||
import { CurrencyAmount, SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { ChainId, CurrencyAmount, WETH9 } from '@uniswap/sdk-core'
|
||||
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const WETH = WETH9[SupportedChainId.MAINNET]
|
||||
const WETH = WETH9[ChainId.MAINNET]
|
||||
|
||||
describe('Swap wrap', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`, { ethereum: 'hardhat' }).hardhat({
|
||||
automine: false,
|
||||
})
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`).hardhat({ automine: false })
|
||||
})
|
||||
|
||||
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01').should('have.value', '0.01')
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
|
||||
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('0.02').should('have.value', '0.02')
|
||||
@@ -20,31 +18,28 @@ describe('Swap wrap', () => {
|
||||
})
|
||||
|
||||
it('should be able to wrap ETH', () => {
|
||||
getBalance(WETH).then((initialWethBalance) => {
|
||||
getBalance(WETH).then((initialBalance) => {
|
||||
cy.contains('Enter ETH amount')
|
||||
|
||||
// Enter the amount to wrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
|
||||
// This also ensures we don't click "Wrap" before the UI has caught up.
|
||||
// Enter amount to wrap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1)
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
|
||||
|
||||
// Click the wrap button.
|
||||
// Submit transaction
|
||||
cy.contains('Wrap').click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Wrapped')
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('1.00 ETH for 1.00 WETH')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-output').should('contain', `Balance: ${initialWethBalance + 1}`)
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
getBalance(WETH).should('equal', initialWethBalance + 1)
|
||||
cy.get(getTestSelector('popups')).contains('Wrapped')
|
||||
const finalBalance = initialBalance + 1
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(WETH).should('equal', finalBalance)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,33 +49,30 @@ describe('Swap wrap', () => {
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
getBalance(WETH).then((initialWethBalance) => {
|
||||
// Swap input/output to unwrap WETH.
|
||||
getBalance(WETH).then((initialBalance) => {
|
||||
// Swap input/output to unwrap WETH
|
||||
cy.get(getTestSelector('swap-currency-button')).click()
|
||||
cy.contains('Enter WETH amount')
|
||||
|
||||
// Enter the amount to unwrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
|
||||
// This also ensures we don't click "Wrap" before the UI has caught up.
|
||||
// Enter the amount to unwrap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1)
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
|
||||
|
||||
// Click the unwrap button.
|
||||
// Submit transaction
|
||||
cy.contains('Unwrap').click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Unwrapped')
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('1.00 WETH for 1.00 ETH')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-input').should('contain', `Balance: ${initialWethBalance - 1}`)
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
getBalance(WETH).should('equal', initialWethBalance - 1)
|
||||
cy.get(getTestSelector('popups')).contains('Unwrapped')
|
||||
const finalBalance = initialBalance - 1
|
||||
cy.get('#swap-currency-input').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(WETH).should('equal', finalBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { ChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeatureFlag } from 'featureFlags'
|
||||
|
||||
import { UNI } from '../../src/constants/tokens'
|
||||
import { ARB, UNI } from '../../src/constants/tokens'
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
const UNI_MAINNET = UNI[ChainId.MAINNET]
|
||||
|
||||
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
|
||||
|
||||
@@ -14,8 +15,9 @@ describe('Token details', () => {
|
||||
|
||||
it('Uniswap token should have all information populated', () => {
|
||||
// Uniswap token
|
||||
cy.visit(`/tokens/ethereum/${UNI_ADDRESS}`)
|
||||
|
||||
cy.visit(`/tokens/ethereum/${UNI_ADDRESS}`, {
|
||||
featureFlags: [{ name: FeatureFlag.infoTDP, value: false }],
|
||||
})
|
||||
// Price chart should be filled in
|
||||
cy.get('[data-cy="chart-header"]').should('include.text', '$')
|
||||
cy.get('[data-cy="price-chart"]').should('exist')
|
||||
@@ -47,41 +49,50 @@ describe('Token details', () => {
|
||||
cy.contains(UNI_ADDRESS).should('exist')
|
||||
})
|
||||
|
||||
it('token with warning and low trading volume should have all information populated', () => {
|
||||
// Shiba predator token, low trading volume and also has warning modal
|
||||
cy.visit('/tokens/ethereum/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||
it('Uniswap token should have correct stats boxes if infoTDP flag on', () => {
|
||||
// Uniswap token
|
||||
cy.visit(`/tokens/ethereum/${UNI_ADDRESS}`, {
|
||||
featureFlags: [{ name: FeatureFlag.infoTDP, value: true }],
|
||||
})
|
||||
|
||||
// Should have missing price chart when price unavailable (expected for this token)
|
||||
if (cy.get('[data-cy="chart-header"]').contains('Price Unavailable')) {
|
||||
cy.get('[data-cy="missing-chart"]').should('exist')
|
||||
}
|
||||
// Stats should have: TVL, 24H Volume, 52W low, 52W high
|
||||
// Stats should have: TVL, FDV, market cap, 24H volume
|
||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||
cy.get('[data-cy="tvl"]').should('exist')
|
||||
cy.get('[data-cy="volume-24h"]').should('exist')
|
||||
cy.get('[data-cy="52w-low"]').should('exist')
|
||||
cy.get('[data-cy="52w-high"]').should('exist')
|
||||
cy.get('[data-cy="tvl"]').should('include.text', '$')
|
||||
cy.get('[data-cy="fdv"]').should('include.text', '$')
|
||||
cy.get('[data-cy="market-cap"]').should('include.text', '$')
|
||||
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
|
||||
})
|
||||
})
|
||||
|
||||
it('token with warning and low trading volume should have all information populated', () => {
|
||||
// Null token created for this test, 0 trading volume and has warning modal
|
||||
cy.visit('/tokens/ethereum/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||
|
||||
// Should have missing price chart when price unavailable (expected for this token)
|
||||
if (cy.get('[data-cy="chart-header"]').contains('Price unavailable')) {
|
||||
cy.get('[data-cy="missing-chart"]').should('exist')
|
||||
}
|
||||
|
||||
// Stats should not exist
|
||||
cy.get(getTestSelector('token-details-stats')).should('not.exist')
|
||||
|
||||
// About section should have description of token
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.contains('QOM is the Shiba Predator').should('exist')
|
||||
cy.contains('No token information available').should('exist')
|
||||
|
||||
// Links section should link out to Etherscan, More analytics, Website, Twitter
|
||||
// Links section should link out to Etherscan, More analytics
|
||||
cy.get('[data-cy="resources-container"]').within(() => {
|
||||
cy.contains('Etherscan')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', 'etherscan.io/address/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||
.and('include', 'etherscan.io/address/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||
cy.contains('More analytics')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', 'info.uniswap.org/#/tokens/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||
cy.contains('Website').should('have.attr', 'href').and('include', 'qom')
|
||||
cy.contains('Twitter').should('have.attr', 'href').and('include', 'twitter.com/ShibaPredator1')
|
||||
.and('include', 'info.uniswap.org/#/tokens/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||
})
|
||||
|
||||
// Contract address should be displayed
|
||||
cy.contains('0xa71d0588EAf47f12B13cF8eC750430d21DF04974').should('exist')
|
||||
cy.contains('0x1eFBB78C8b917f67986BcE54cE575069c0143681').should('exist')
|
||||
|
||||
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
|
||||
cy.get('[data-cy="token-safety-message"]')
|
||||
@@ -93,9 +104,7 @@ describe('Token details', () => {
|
||||
beforeEach(() => {
|
||||
// On mobile widths, we just link back to /swap instead of rendering the swap component.
|
||||
cy.viewport(1200, 800)
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
}).then(() => {
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`).then(() => {
|
||||
cy.wait('@eth_blockNumber')
|
||||
cy.scrollTo('top')
|
||||
})
|
||||
@@ -110,12 +119,12 @@ describe('Token details', () => {
|
||||
|
||||
it('should automatically navigate to the new TDP', () => {
|
||||
cy.get(`#swap-currency-output .open-currency-select-button`).click()
|
||||
cy.contains('WETH').click()
|
||||
cy.get('[data-reach-dialog-content]').contains('WETH').click()
|
||||
cy.url().should('include', `${WETH9[1].address}`)
|
||||
cy.url().should('not.include', `${UNI_MAINNET.address}`)
|
||||
})
|
||||
|
||||
it.only('should not share swap state with the main swap page', () => {
|
||||
it('should not share swap state with the main swap page', () => {
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||
cy.get(`#swap-currency-input .open-currency-select-button`).click()
|
||||
cy.contains('WETH').click()
|
||||
@@ -145,12 +154,13 @@ describe('Token details', () => {
|
||||
})
|
||||
|
||||
it('should show a L2 token even if the user is connected to a different network', () => {
|
||||
cy.visit('/tokens', { ethereum: 'hardhat' })
|
||||
cy.visit('/tokens')
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
|
||||
cy.get(getTestSelector('token-table-row-ARB')).click()
|
||||
cy.get(getTestSelector(`token-table-row-${ARB.address.toLowerCase()}`)).click()
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'ARB')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).should('be.disabled')
|
||||
cy.contains('Connect to Arbitrum').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,83 +1,30 @@
|
||||
describe.skip('Token explore filter', () => {
|
||||
before(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('should filter correctly by uni search term', () => {
|
||||
describe('Token explore filter', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/tokens')
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
const filteredByUni = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('uni'))
|
||||
cy.wrap(filteredByUni).as('filteredByUni')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||
.clear()
|
||||
.type('uni')
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||
cy.get('@filteredByUni').then((filteredByUni) => {
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
expect(tokenNames.length).to.equal(filteredByUni.length)
|
||||
tokenNames.forEach((tokenName) => {
|
||||
expect(filteredByUni).to.include(tokenName)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function aliasFilteredTokens(filter: string) {
|
||||
cy.get('[data-cy="token-name"]').then((tokens) => {
|
||||
cy.wrap(Array.from(tokens).filter((token) => token.innerText.toLowerCase().includes(filter))).as('filteredTokens')
|
||||
})
|
||||
}
|
||||
|
||||
function searchFor(filter: string) {
|
||||
cy.get('[data-cy="explore-tokens-search-input"]').clear().type(filter).type('{enter}')
|
||||
// wait for it to finish the filtered render
|
||||
cy.get('[data-cy="token-name"]').first().contains(filter, {
|
||||
matchCase: false,
|
||||
})
|
||||
}
|
||||
|
||||
it('should filter correctly by dao search term', () => {
|
||||
cy.visit('/tokens')
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
const filteredByDao = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('dao'))
|
||||
cy.wrap(filteredByDao).as('filteredByDao')
|
||||
})
|
||||
aliasFilteredTokens('dao')
|
||||
searchFor('dao')
|
||||
|
||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||
.clear()
|
||||
.type('dao')
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||
cy.get('@filteredByDao').then((filteredByDao) => {
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
expect(tokenNames.length).to.equal(filteredByDao.length)
|
||||
tokenNames.forEach((tokenName) => {
|
||||
expect(filteredByDao).to.include(tokenName)
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('@filteredTokens').then((filteredTokens) => {
|
||||
cy.get('[data-cy="token-name"]').then((tokens) => {
|
||||
cy.wrap(Array.from(tokens)).should('deep.equal', Array.from(filteredTokens))
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter correctly by ax search term', () => {
|
||||
cy.visit('/tokens')
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
const filteredByAx = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('ax'))
|
||||
cy.wrap(filteredByAx).as('filteredByAx')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||
.clear()
|
||||
.type('ax')
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||
cy.get('@filteredByAx').then((filteredByAx) => {
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
expect(tokenNames.length).to.equal(filteredByAx.length)
|
||||
tokenNames.forEach((tokenName) => {
|
||||
expect(filteredByAx).to.include(tokenName)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,11 +10,11 @@ describe('Token explore', () => {
|
||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.greaterThan', 0)
|
||||
// check sorted svg icon is present in volume cell, since tokens are sorted by volume by default
|
||||
cy.get(getTestSelector('header-row')).find(getTestSelector('volume-cell')).find('svg').should('exist')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('name-cell')).should('include.text', 'Ether')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('volume-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('price-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('tvl-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-ETH'))
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('name-cell')).should('include.text', 'Ether')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('volume-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('price-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('tvl-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE'))
|
||||
.find(getTestSelector('percent-change-cell'))
|
||||
.should('include.text', '%')
|
||||
cy.get(getTestSelector('header-row')).find(getTestSelector('price-cell')).click()
|
||||
@@ -24,24 +24,24 @@ describe('Token explore', () => {
|
||||
it('should update when time window toggled', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelector('time-selector')).should('contain', '1D')
|
||||
cy.get(getTestSelector('token-table-row-ETH'))
|
||||
cy.get(getTestSelector('token-table-row-NATIVE'))
|
||||
.find(getTestSelector('volume-cell'))
|
||||
.then(function ($elem) {
|
||||
cy.wrap($elem.text()).as('dailyEthVol')
|
||||
})
|
||||
cy.get(getTestSelector('time-selector')).click()
|
||||
cy.get(getTestSelector('1Y')).click()
|
||||
cy.get(getTestSelector('token-table-row-ETH'))
|
||||
cy.get(getTestSelector('token-table-row-NATIVE'))
|
||||
.find(getTestSelector('volume-cell'))
|
||||
.then(function ($elem) {
|
||||
cy.wrap($elem.text()).as('yearlyEthVol')
|
||||
})
|
||||
expect(cy.get('@dailyEthVol')).to.not.equal(cy.get('@yearlyEthVol'))
|
||||
cy.get('@dailyEthVol').should('not.equal', cy.get('@yearlyEthVol'))
|
||||
})
|
||||
|
||||
it('should navigate to token detail page when row clicked', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).click()
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).click()
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||
cy.get(getTestSelector('token-info-container')).should('exist')
|
||||
@@ -53,13 +53,13 @@ describe('Token explore', () => {
|
||||
it('should update when global network changed', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Ethereum')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).should('exist')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).should('exist')
|
||||
|
||||
// note: cannot switch global chain via UI because we cannot approve the network switch
|
||||
// in metamask modal using plain cypress. this is a workaround.
|
||||
cy.visit('/tokens/polygon')
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Polygon')
|
||||
cy.get(getTestSelector('token-table-row-MATIC')).should('exist')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('name-cell')).should('include.text', 'Matic')
|
||||
})
|
||||
|
||||
it('should update when token explore table network changed', () => {
|
||||
@@ -67,8 +67,6 @@ describe('Token explore', () => {
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-optimism')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
||||
cy.reload()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
||||
cy.get(getTestSelector('chain-selector-logo')).invoke('attr', 'alt').should('eq', 'Ethereum')
|
||||
cy.get(getTestSelector('chain-selector-logo')).find('title').should('include.text', 'Ethereum logo')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,64 +1,72 @@
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
import { UNI } from 'constants/tokens'
|
||||
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const UNI_ADDRESS = UNI[ChainId.MAINNET].address.toLowerCase()
|
||||
|
||||
describe('Universal search bar', () => {
|
||||
before(() => {
|
||||
function openSearch() {
|
||||
// can't just type "/" because on mobile it doesn't respond to that
|
||||
cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="magnifying-icon"]')
|
||||
.parent()
|
||||
.then(($navIcon) => {
|
||||
$navIcon.click()
|
||||
})
|
||||
})
|
||||
|
||||
it('should yield clickable result for regular token or nft collection search term', () => {
|
||||
// Search for uni token by name.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]')
|
||||
function getSearchBar() {
|
||||
return cy.get('[data-cy="search-bar-input"]').last()
|
||||
}
|
||||
|
||||
it('should yield clickable result that is then added to recent searches', () => {
|
||||
// Search for UNI token by name.
|
||||
openSearch()
|
||||
getSearchBar().clear().type('uni')
|
||||
|
||||
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||
.should('contain.text', 'Uniswap')
|
||||
.and('contain.text', 'UNI')
|
||||
.and('contain.text', '$')
|
||||
.and('contain.text', '%')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]').first().click()
|
||||
.click()
|
||||
cy.location('pathname').should('equal', '/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984')
|
||||
|
||||
cy.get('div').contains('Uniswap').should('exist')
|
||||
// Stats should have: TVL, 24H Volume, 52W low, 52W high.
|
||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||
cy.get('[data-cy="tvl"]').should('include.text', '$')
|
||||
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
|
||||
cy.get('[data-cy="52w-low"]').should('include.text', '$')
|
||||
cy.get('[data-cy="52w-high"]').should('include.text', '$')
|
||||
})
|
||||
|
||||
// About section should have description of token.
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.contains('UNI is the governance token for Uniswap').should('exist')
|
||||
})
|
||||
|
||||
it.skip('should show recent tokens and popular tokens with empty search term', () => {
|
||||
cy.get('[data-cy="magnifying-icon"]')
|
||||
.parent()
|
||||
.then(($navIcon) => {
|
||||
$navIcon.click()
|
||||
})
|
||||
// Recently searched UNI token should exist.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear()
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Recent searches')
|
||||
.find('[data-cy="searchbar-token-row-UNI"]')
|
||||
openSearch()
|
||||
cy.get(getTestSelector('searchbar-dropdown'))
|
||||
.contains(getTestSelector('searchbar-dropdown'), 'Recent searches')
|
||||
.find(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||
.should('exist')
|
||||
|
||||
// Most popular 3 tokens should be shown.
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Popular tokens')
|
||||
.find('[data-cy^="searchbar-token-row"]')
|
||||
.its('length')
|
||||
.should('be.eq', 3)
|
||||
})
|
||||
|
||||
it.skip('should show blocked badge when blocked token is searched for', () => {
|
||||
// Search for mTSLA, which is a blocked token.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
|
||||
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
|
||||
})
|
||||
it(
|
||||
'should go to the selected result when recent results are shown',
|
||||
// this test is experiencing flake despite being correct, i can see the right value in DOM
|
||||
// but for some reason cypress doesn't find it, so adding retries for now :/
|
||||
{
|
||||
// @ts-ignore see https://uniswapteam.slack.com/archives/C047U65H422/p1691455547556309
|
||||
// basically cypress has bad types due to overlap with jest and you just have to deal with it
|
||||
// i tried removing jest types but still happens
|
||||
retries: {
|
||||
runMode: 3,
|
||||
openMode: 3,
|
||||
},
|
||||
},
|
||||
() => {
|
||||
// Seed recent results with UNI.
|
||||
openSearch()
|
||||
getSearchBar().type('uni')
|
||||
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||
getSearchBar().clear().type('{esc}')
|
||||
|
||||
// Search a different token by name.
|
||||
openSearch()
|
||||
getSearchBar().type('eth')
|
||||
cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE'))
|
||||
|
||||
// Validate that we go to the searched/selected result.
|
||||
cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE')).click()
|
||||
cy.url().should('contain', 'tokens/ethereum/NATIVE')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
41
cypress/e2e/wallet-connection/connect.test.ts
Normal file
41
cypress/e2e/wallet-connection/connect.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getTestSelector } from '../../utils'
|
||||
import { DISCONNECTED_WALLET_USER_STATE } from '../../utils/user-state'
|
||||
|
||||
describe('disconnect wallet', () => {
|
||||
it('should clear state', () => {
|
||||
cy.visit('/swap')
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
|
||||
// Verify wallet is connected
|
||||
cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)))
|
||||
cy.contains('Balance:')
|
||||
|
||||
// Disconnect the wallet
|
||||
cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)).click())
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).contains('Disconnect')
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
|
||||
// Verify wallet has disconnected
|
||||
cy.contains('Connect a wallet').should('exist')
|
||||
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect')
|
||||
cy.contains('Connect wallet')
|
||||
|
||||
// Verify swap input is cleared
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('connect wallet', () => {
|
||||
it('should load state', () => {
|
||||
cy.visit('/swap', { userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
|
||||
// Connect the wallet
|
||||
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect').click()
|
||||
cy.contains('MetaMask').click()
|
||||
|
||||
// Verify wallet is connected
|
||||
cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)))
|
||||
cy.contains('Balance:')
|
||||
})
|
||||
})
|
||||
144
cypress/e2e/wallet-connection/switch-network.test.ts
Normal file
144
cypress/e2e/wallet-connection/switch-network.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { createDeferredPromise } from '../../../src/test-utils/promise'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
function waitsForActiveChain(chain: string) {
|
||||
cy.get(getTestSelector('chain-selector-logo')).find('title').should('include.text', `${chain} logo`)
|
||||
}
|
||||
|
||||
function switchChain(chain: string) {
|
||||
cy.get(getTestSelector('chain-selector')).eq(1).click()
|
||||
cy.contains(chain).click()
|
||||
}
|
||||
|
||||
describe('network switching', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
})
|
||||
|
||||
function rejectsNetworkSwitchWith(rejection: unknown) {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Reject network switch
|
||||
const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
|
||||
sendStub.withArgs('wallet_switchEthereumChain').rejects(rejection)
|
||||
sendStub.callThrough() // allows other calls to return non-stubbed values
|
||||
})
|
||||
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify rejected network switch
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Ethereum')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
}
|
||||
|
||||
it('should not display message on user rejection', () => {
|
||||
const USER_REJECTION = { code: 4001 }
|
||||
rejectsNetworkSwitchWith(USER_REJECTION)
|
||||
cy.get(getTestSelector('popups')).should('not.contain', 'Failed to switch networks')
|
||||
})
|
||||
|
||||
it('should display message on unknown error', () => {
|
||||
rejectsNetworkSwitchWith(new Error('Unknown error'))
|
||||
cy.get(getTestSelector('popups')).contains('Failed to switch networks')
|
||||
})
|
||||
|
||||
it('should add missing chain', () => {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// https://docs.metamask.io/guide/rpc-api.html#unrestricted-methods
|
||||
const CHAIN_NOT_ADDED = { code: 4902 } // missing message in useSelectChain
|
||||
|
||||
// Reject network switch with CHAIN_NOT_ADDED
|
||||
const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
|
||||
let added = false
|
||||
sendStub
|
||||
.withArgs('wallet_switchEthereumChain')
|
||||
.callsFake(() => (added ? Promise.resolve(null) : Promise.reject(CHAIN_NOT_ADDED)))
|
||||
sendStub.withArgs('wallet_addEthereumChain').callsFake(() => {
|
||||
added = true
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
sendStub.callThrough() // allows other calls to return non-stubbed values
|
||||
})
|
||||
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify the network was added
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_addEthereumChain', [
|
||||
{
|
||||
blockExplorerUrls: ['https://polygonscan.com/'],
|
||||
chainId: '0x89',
|
||||
chainName: 'Polygon',
|
||||
nativeCurrency: { name: 'Polygon Matic', symbol: 'MATIC', decimals: 18 },
|
||||
rpcUrls: ['https://polygon-rpc.com/'],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should not disconnect while switching', () => {
|
||||
const promise = createDeferredPromise()
|
||||
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Reject network switch with CHAIN_NOT_ADDED
|
||||
const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
|
||||
sendStub.withArgs('wallet_switchEthereumChain').returns(promise)
|
||||
sendStub.callThrough() // allows other calls to return non-stubbed values
|
||||
})
|
||||
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify there is no disconnection
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
|
||||
cy.contains('Connecting to Polygon')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('be.disabled')
|
||||
promise.resolve()
|
||||
})
|
||||
|
||||
it('should switch networks', () => {
|
||||
// Select an output currency
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('USDC').click()
|
||||
|
||||
// Populate input/output fields
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Switch network
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify network switch
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.url().should('contain', 'chain=polygon')
|
||||
|
||||
// Verify that the input/output fields were reset
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'MATIC')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
|
||||
})
|
||||
})
|
||||
|
||||
describe('network switching from URL param', () => {
|
||||
it('should switch network from URL param', () => {
|
||||
cy.visit('/swap?chain=polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
})
|
||||
|
||||
it('should be able to switch network after loading from URL param', () => {
|
||||
cy.visit('/swap?chain=polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
|
||||
// switching to another chain clears query param
|
||||
switchChain('Ethereum')
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Ethereum')
|
||||
cy.url().should('not.contain', 'chain=polygon')
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,10 @@
|
||||
import { FeatureFlag } from 'featureFlags'
|
||||
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Wallet Dropdown', () => {
|
||||
function itShouldChangeTheTheme() {
|
||||
it('should change the theme', () => {
|
||||
function itChangesTheme() {
|
||||
it('should change theme', () => {
|
||||
cy.get(getTestSelector('theme-lightmode')).click()
|
||||
|
||||
cy.get(getTestSelector('theme-lightmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
@@ -21,13 +23,21 @@ describe('Wallet Dropdown', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function itShouldChangeTheLanguage() {
|
||||
it('should select a language', () => {
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
|
||||
function itChangesLocale({ featureFlag = false }: { featureFlag?: boolean } = {}) {
|
||||
it('should change locale', () => {
|
||||
cy.contains('Uniswap available in: English').should('not.exist')
|
||||
|
||||
if (featureFlag) {
|
||||
cy.get(getTestSelector('language-settings-button')).click()
|
||||
}
|
||||
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
|
||||
cy.location('search').should('match', /\?lng=af-ZA$/)
|
||||
cy.contains('Uniswap available in: English')
|
||||
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
cy.location('search').should('match', /\?lng=en-US$/)
|
||||
cy.contains('Uniswap available in: English').should('not.exist')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,19 +47,76 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
itShouldChangeTheTheme()
|
||||
itShouldChangeTheLanguage()
|
||||
itChangesTheme()
|
||||
itChangesLocale()
|
||||
|
||||
it('should not show buy crypto button in uk', () => {
|
||||
cy.document().then((doc) => {
|
||||
const meta = document.createElement('meta')
|
||||
meta.setAttribute('property', 'x:blocked-paths')
|
||||
meta.setAttribute('content', '/,/nfts,/buy')
|
||||
doc.head.appendChild(meta)
|
||||
})
|
||||
cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('do not render buy button when /buy is blocked', () => {
|
||||
beforeEach(() => {
|
||||
cy.document().then((doc) => {
|
||||
const meta = document.createElement('meta')
|
||||
meta.setAttribute('property', 'x:blocked-paths')
|
||||
meta.setAttribute('content', '/buy')
|
||||
doc.head.appendChild(meta)
|
||||
})
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
|
||||
it('should not render buy button', () => {
|
||||
cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('should change locale with feature flag', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
itChangesLocale({ featureFlag: true })
|
||||
})
|
||||
|
||||
describe('testnet toggle', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
it('should toggle testnet visibility', () => {
|
||||
cy.get(getTestSelector('chain-selector')).last().click()
|
||||
cy.get(getTestSelector('chain-selector-options')).should('not.contain.text', 'Sepolia')
|
||||
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get('#testnets-toggle').click()
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
cy.get(getTestSelector('chain-selector')).last().click()
|
||||
cy.get(getTestSelector('chain-selector-options')).should('contain.text', 'Sepolia')
|
||||
})
|
||||
})
|
||||
|
||||
describe('disconnected', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
// click twice, first time to show confirmation, second to confirm
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect')
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
itShouldChangeTheTheme()
|
||||
itShouldChangeTheLanguage()
|
||||
itChangesTheme()
|
||||
itChangesLocale()
|
||||
})
|
||||
|
||||
describe('with color theme', () => {
|
||||
@@ -76,7 +143,7 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(152, 161, 192)')
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(155, 155, 155)')
|
||||
})
|
||||
|
||||
it('should properly use light system theme when auto theme setting is selected', () => {
|
||||
@@ -84,7 +151,7 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(125, 125, 125)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -105,4 +172,34 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('local currency', () => {
|
||||
it('loads local currency from the query param', () => {
|
||||
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.contains('USD')
|
||||
|
||||
cy.visit('/?cur=AUD', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.contains('AUD')
|
||||
})
|
||||
|
||||
it('loads local currency from menu', () => {
|
||||
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.contains('USD')
|
||||
|
||||
cy.get(getTestSelector('local-currency-settings-button')).click()
|
||||
cy.get(getTestSelector('wallet-local-currency-item')).contains('AUD').click({ force: true })
|
||||
cy.location('search').should('match', /\?cur=AUD$/)
|
||||
cy.contains('AUD')
|
||||
|
||||
cy.get(getTestSelector('wallet-local-currency-item')).contains('USD').click({ force: true })
|
||||
cy.location('search').should('match', /\?cur=USD$/)
|
||||
cy.contains('USD')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
5
cypress/fixtures/insufficientLiquidity.json
Normal file
5
cypress/fixtures/insufficientLiquidity.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"errorCode": "QUOTE_ERROR",
|
||||
"detail": "No quotes available",
|
||||
"id": "63363cc1-d474-4584-b386-7c356814b79f"
|
||||
}
|
||||
12
cypress/fixtures/mini-portfolio/empty_activity.json
Normal file
12
cypress/fixtures/mini-portfolio/empty_activity.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"data": {
|
||||
"portfolios": [
|
||||
{
|
||||
"id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==",
|
||||
"assetActivities": [],
|
||||
"__typename": "Portfolio"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
580
cypress/fixtures/mini-portfolio/full_activity.json
Normal file
580
cypress/fixtures/mini-portfolio/full_activity.json
Normal file
@@ -0,0 +1,580 @@
|
||||
{
|
||||
"data": {
|
||||
"portfolios": [
|
||||
{
|
||||
"id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==",
|
||||
"assetActivities": [
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnM09EQm1NamcwTURSak1qRXpPRGd6TVRVM00yRXdOakJtTVRaaE1UQTNaV0ZtTW1Jd01qazFZbUZqTmpjNU5tUm1ZamN5TW1WbVl6VmpPVE5tTmpRM1h6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEQXdNREF3TURBek1HWTBPV0ptTW1Vd01ESmxOakJqTm1Wa01UWTJNV1ppTWpNME5tUTRPREk9",
|
||||
"timestamp": 1684364195,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"__typename": "TransactionDetails",
|
||||
"id": "VHJhbnNhY3Rpb246MHg3ODBmMjg0MDRjMjEzODgzMTU3M2EwNjBmMTZhMTA3ZWFmMmIwMjk1YmFjNjc5NmRmYjcyMmVmYzVjOTNmNjQ3XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDAwMDAwMDAzMGY0OWJmMmUwMDJlNjBjNmVkMTY2MWZiMjM0NmQ4ODI=",
|
||||
"type": "UNKNOWN",
|
||||
"blockNumber": 17282434,
|
||||
"hash": "0x780f28404c2138831573a060f16a107eaf2b0295bac6796dfb722efc5c93f647",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0x000000030f49bf2e002e60c6ed1661fb2346d882",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 465,
|
||||
"assetChanges": []
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhobFl6QTROMkpoTjJJMk4yUTFPVEEwTVdNMU5XTXdObU16TkdNNVlXVmhPVEUxTkRreVpUYzRNRFl4WldRd016TTBNMlprWmprMU1qa3dPR1U0WTJSa1h6QjRaR0psWmpNM05HWmtaamhrTnpNMVpUYzFPRGxoT1dFNVpUSmpOV0V3T1RGbFlqSmtZbVUxTjE4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684364135,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"__typename": "TransactionDetails",
|
||||
"id": "VHJhbnNhY3Rpb246MHhlYzA4N2JhN2I2N2Q1OTA0MWM1NWMwNmMzNGM5YWVhOTE1NDkyZTc4MDYxZWQwMzM0M2ZkZjk1MjkwOGU4Y2RkXzB4ZGJlZjM3NGZkZjhkNzM1ZTc1ODlhOWE5ZTJjNWEwOTFlYjJkYmU1N18weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17282429,
|
||||
"hash": "0xec087ba7b67d59041c55c06c34c9aea915492e78061ed03343fdf952908e8cdd",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0xdbef374fdf8d735e7589a9a9e2c5a091eb2dbe57",
|
||||
"nonce": 66,
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGRiZWYzNzRmZGY4ZDczNWU3NTg5YTlhOWUyYzVhMDkxZWIyZGJlNTdfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4ZWMwODdiYTdiNjdkNTkwNDFjNTVjMDZjMzRjOWFlYTkxNTQ5MmU3ODA2MWVkMDMzNDNmZGY5NTI5MDhlOGNkZA==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.001",
|
||||
"sender": "0xdbef374fdf8d735e7589a9a9e2c5a091eb2dbe57",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjEuODI5NjcwMDAwMDAwMDAwMV9VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 1.8296700000000001,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhoaE9URXdPVFEwT1Rka01UVmpNelpsWWprd1pXUXpZVEkwWW1Wa09ESTBOalpqWmpKaU9URXpNV1l4WkRVMk1EUmlNelppWW1aallqRTBOMkUzTURnNFh6QjRaV1JoTldVeE9ERXhORFppTVdZNVlUZG1OREJtT0RWak1HUmhNek0wT1RNNE5ESXdaRFV4TkY4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684319903,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHhhOTEwOTQ0OTdkMTVjMzZlYjkwZWQzYTI0YmVkODI0NjZjZjJiOTEzMWYxZDU2MDRiMzZiYmZjYjE0N2E3MDg4XzB4ZWRhNWUxODExNDZiMWY5YTdmNDBmODVjMGRhMzM0OTM4NDIwZDUxNF8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17278819,
|
||||
"hash": "0xa91094497d15c36eb90ed3a24bed82466cf2b9131f1d5604b36bbfcb147a7088",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0xeda5e181146b1f9a7f40f85c0da334938420d514",
|
||||
"nonce": 5,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGVkYTVlMTgxMTQ2YjFmOWE3ZjQwZjg1YzBkYTMzNDkzODQyMGQ1MTRfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4YTkxMDk0NDk3ZDE1YzM2ZWI5MGVkM2EyNGJlZDgyNDY2Y2YyYjkxMzFmMWQ1NjA0YjM2YmJmY2IxNDdhNzA4OA==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.15",
|
||||
"sender": "0xeda5e181146b1f9a7f40f85c0da334938420d514",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjI3NC40NTA1X1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 274.4505,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnMFkyUm1Nell6T0dRME1ERXdOV1U1WkRZMVlUZGxObUV6WVdFMlpHTXpNREZpWVRNNVpHTXlNV1ppT0dGaE5USTBNVFppT1ROaE5tWXhOVEUwTWpReVh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VHUmxOR1F6WVRJME5XUXlZall4WW1WaE1tTmlaREl4TmpVNE1XVXlaR1ZrTmpWbFl6azFNRFE9",
|
||||
"timestamp": 1684319903,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg0Y2RmMzYzOGQ0MDEwNWU5ZDY1YTdlNmEzYWE2ZGMzMDFiYTM5ZGMyMWZiOGFhNTI0MTZiOTNhNmYxNTE0MjQyXzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weGRlNGQzYTI0NWQyYjYxYmVhMmNiZDIxNjU4MWUyZGVkNjVlYzk1MDQ=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17278819,
|
||||
"hash": "0x4cdf3638d40105e9d65a7e6a3aa6dc301ba39dc21fb8aa52416b93a6f1514242",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xde4d3a245d2b61bea2cbd216581e2ded65ec9504",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 464,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHhkZTRkM2EyNDVkMmI2MWJlYTJjYmQyMTY1ODFlMmRlZDY1ZWM5NTA0XzB4NGNkZjM2MzhkNDAxMDVlOWQ2NWE3ZTZhM2FhNmRjMzAxYmEzOWRjMjFmYjhhYTUyNDE2YjkzYTZmMTUxNDI0Mg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.00134999999999999",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0xde4d3a245d2b61bea2cbd216581e2ded65ec9504",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjIuNDcwMDU0NDk5OTk5OTgyX1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 2.470054499999982,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnM04yRXhPVGRoWmpjek9EUXpNRFk0WVRCaVlqUmlaV1V6WWpabFptWmxaakpsTkdZMFptTXlNR1UxWVRGbVltSTBOak14WXpoak1UQTROMk15WWpjM1h6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VHWTFZekZoTnpCbU5qY3pPV0k1TW1ZNU4yTmtOVE5qTXpFMk1ETTJNbU14TXpBMVpUa3hZVGc9",
|
||||
"timestamp": 1684202579,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg3N2ExOTdhZjczODQzMDY4YTBiYjRiZWUzYjZlZmZlZjJlNGY0ZmMyMGU1YTFmYmI0NjMxYzhjMTA4N2MyYjc3XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weGY1YzFhNzBmNjczOWI5MmY5N2NkNTNjMzE2MDM2MmMxMzA1ZTkxYTg=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17269191,
|
||||
"hash": "0x77a197af73843068a0bb4bee3b6effef2e4f4fc20e5a1fbb4631c8c1087c2b77",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf5c1a70f6739b92f97cd53c3160362c1305e91a8",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 463,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHhmNWMxYTcwZjY3MzliOTJmOTdjZDUzYzMxNjAzNjJjMTMwNWU5MWE4XzB4NzdhMTk3YWY3Mzg0MzA2OGEwYmI0YmVlM2I2ZWZmZWYyZTRmNGZjMjBlNWExZmJiNDYzMWM4YzEwODdjMmI3Nw==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.001216034894406018",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0xf5c1a70f6739b92f97cd53c3160362c1305e91a8",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjIuMjI0OTQyNTY1MjQ3ODU5X1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 2.224942565247859,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnMlpXSmtZbVJrTVRZMk0yVmxNV1ZrT0RVeE16TXlZelUyWmpkall6YzJaV1ZqTVROaE5qTm1PVEkxTldOa1ltWXlZVEUxWWpReFl6azBPVGhrWW1Wa1h6QjROREZpTXpBNU1qTTJZemczWWpGaVl6Wm1ZVGhsWWpnMk5UZ3pNMlUwTkRFMU9HWmhPVGt4WVY4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684202579,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg2ZWJkYmRkMTY2M2VlMWVkODUxMzMyYzU2ZjdjYzc2ZWVjMTNhNjNmOTI1NWNkYmYyYTE1YjQxYzk0OThkYmVkXzB4NDFiMzA5MjM2Yzg3YjFiYzZmYThlYjg2NTgzM2U0NDE1OGZhOTkxYV8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17269191,
|
||||
"hash": "0x6ebdbdd1663ee1ed851332c56f7cc76eec13a63f9255cdbf2a15b41c9498dbed",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0x41b309236c87b1bc6fa8eb865833e44158fa991a",
|
||||
"nonce": 111266,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDQxYjMwOTIzNmM4N2IxYmM2ZmE4ZWI4NjU4MzNlNDQxNThmYTk5MWFfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4NmViZGJkZDE2NjNlZTFlZDg1MTMzMmM1NmY3Y2M3NmVlYzEzYTYzZjkyNTVjZGJmMmExNWI0MWM5NDk4ZGJlZA==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.00275365",
|
||||
"sender": "0x41b309236c87b1bc6fa8eb865833e44158fa991a",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjUuMDM4MjcwNzk1NV9VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 5.0382707955,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNU5EUmlNR00wTVROa1l6QmpNekU0TUdFelkyTTNZakUyT1RCbVlqZzBNRFExWm1FME9UTXpObUV5WmprNE16VmpORFpqTURsak1UY3lObUUzTm1aalh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEWXlNakJsTURoak9XUTJNMkZpTjJKaE1tVTFOalk0TXpsbU5ESTVaV1ZsWm1VeE9UbGlOMlU9",
|
||||
"timestamp": 1684171943,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg5NDRiMGM0MTNkYzBjMzE4MGEzY2M3YjE2OTBmYjg0MDQ1ZmE0OTMzNmEyZjk4MzVjNDZjMDljMTcyNmE3NmZjXzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDYyMjBlMDhjOWQ2M2FiN2JhMmU1NjY4MzlmNDI5ZWVlZmUxOTliN2U=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17266680,
|
||||
"hash": "0x944b0c413dc0c3180a3cc7b1690fb84045fa49336a2f9835c46c09c1726a76fc",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 462,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHg2MjIwZTA4YzlkNjNhYjdiYTJlNTY2ODM5ZjQyOWVlZWZlMTk5YjdlXzB4OTQ0YjBjNDEzZGMwYzMxODBhM2NjN2IxNjkwZmI4NDA0NWZhNDkzMzZhMmY5ODM1YzQ2YzA5YzE3MjZhNzZmYw==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.003476850926189204",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjYuMzYxNDg5ODM0MTIwNjAxX1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 6.361489834120601,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhneE0yRTRNRGxsT1RZd05USmhOVGxrWlRjNU56WXhObVZrTlRjME1qTTVNakV3WkRJMVpUY3hNRGhqTkRjek9EbG1NbVJoTnpjeU5qTXhZbVZpTUdZMlh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEWXlNakJsTURoak9XUTJNMkZpTjJKaE1tVTFOalk0TXpsbU5ESTVaV1ZsWm1VeE9UbGlOMlU9",
|
||||
"timestamp": 1684171943,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHgxM2E4MDllOTYwNTJhNTlkZTc5NzYxNmVkNTc0MjM5MjEwZDI1ZTcxMDhjNDczODlmMmRhNzcyNjMxYmViMGY2XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDYyMjBlMDhjOWQ2M2FiN2JhMmU1NjY4MzlmNDI5ZWVlZmUxOTliN2U=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17266680,
|
||||
"hash": "0x13a809e96052a59de797616ed574239210d25e7108c47389f2da772631beb0f6",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 461,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHg2MjIwZTA4YzlkNjNhYjdiYTJlNTY2ODM5ZjQyOWVlZWZlMTk5YjdlXzB4MTNhODA5ZTk2MDUyYTU5ZGU3OTc2MTZlZDU3NDIzOTIxMGQyNWU3MTA4YzQ3Mzg5ZjJkYTc3MjYzMWJlYjBmNg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.000900000000000318",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjEuNjQ2NzAzMDAwMDAwNTgxOF9VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 1.6467030000005818,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhobFkyRTJNVEZrTlRVME1EZGxPVGt6WlRFM1lqWmtaVGhpWVRJMFlqWXlOREpqWVRSbFlXWTBORGN3TkRKbFpHRmtNRFE0TTJNNFptSTJabUU0WkRJNVh6QjROekU0WVRVeE5ESXhNR0kwTnpWaU9USXhOVGd6WldGaU5ERXlaV0ptTUdaaVlXUm1NMkl6T1Y4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684171931,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHhlY2E2MTFkNTU0MDdlOTkzZTE3YjZkZThiYTI0YjYyNDJjYTRlYWY0NDcwNDJlZGFkMDQ4M2M4ZmI2ZmE4ZDI5XzB4NzE4YTUxNDIxMGI0NzViOTIxNTgzZWFiNDEyZWJmMGZiYWRmM2IzOV8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17266679,
|
||||
"hash": "0xeca611d55407e993e17b6de8ba24b6242ca4eaf447042edad0483c8fb6fa8d29",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0x718a514210b475b921583eab412ebf0fbadf3b39",
|
||||
"nonce": 92,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDcxOGE1MTQyMTBiNDc1YjkyMTU4M2VhYjQxMmViZjBmYmFkZjNiMzlfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4ZWNhNjExZDU1NDA3ZTk5M2UxN2I2ZGU4YmEyNGI2MjQyY2E0ZWFmNDQ3MDQyZWRhZDA0ODNjOGZiNmZhOGQyOQ==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.01",
|
||||
"sender": "0x718a514210b475b921583eab412ebf0fbadf3b39",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjE4LjI5NjdfVVNE",
|
||||
"currency": "USD",
|
||||
"value": 18.2967,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnMllqTTJNelEwT1daaU1HWTROems0TkRnM1pqWmlOREkwTkRjMFkySXdNbVF5WlRVNE1EZ3dPVEpoWVRneE1EVm1ObUU0T1dOalpHTTBORGRsTURSa1h6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEQXdNREF3TURBek1HWTBPV0ptTW1Vd01ESmxOakJqTm1Wa01UWTJNV1ppTWpNME5tUTRPREk9",
|
||||
"timestamp": 1684085063,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg2YjM2MzQ0OWZiMGY4Nzk4NDg3ZjZiNDI0NDc0Y2IwMmQyZTU4MDgwOTJhYTgxMDVmNmE4OWNjZGM0NDdlMDRkXzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDAwMDAwMDAzMGY0OWJmMmUwMDJlNjBjNmVkMTY2MWZiMjM0NmQ4ODI=",
|
||||
"type": "UNKNOWN",
|
||||
"blockNumber": 17259555,
|
||||
"hash": "0x6b363449fb0f8798487f6b424474cb02d2e5808092aa8105f6a89ccdc447e04d",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0x000000030f49bf2e002e60c6ed1661fb2346d882",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 460,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": []
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNFlXRTVNVFJqTkRjeU5qWTNNVGxqWkRFeE1EYzNOMkprTnpZek0yVTFOV1kyWkdWbVpXRmpPVEV4TlRjd09EZzNZVEEyWXpNNE5UTmxaV0kyTldZeVh6QjRaR0V4TTJRMk5HVmpPVFZqWkRZM056VXlPVEZpTVdNek1qRXdNamN4TWpGaVpUSXdPV1JtTUY4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684085051,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg4YWE5MTRjNDcyNjY3MTljZDExMDc3N2JkNzYzM2U1NWY2ZGVmZWFjOTExNTcwODg3YTA2YzM4NTNlZWI2NWYyXzB4ZGExM2Q2NGVjOTVjZDY3NzUyOTFiMWMzMjEwMjcxMjFiZTIwOWRmMF8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17259554,
|
||||
"hash": "0x8aa914c47266719cd110777bd7633e55f6defeac911570887a06c3853eeb65f2",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0xda13d64ec95cd6775291b1c321027121be209df0",
|
||||
"nonce": 832,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGRhMTNkNjRlYzk1Y2Q2Nzc1MjkxYjFjMzIxMDI3MTIxYmUyMDlkZjBfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4OGFhOTE0YzQ3MjY2NzE5Y2QxMTA3NzdiZDc2MzNlNTVmNmRlZmVhYzkxMTU3MDg4N2EwNmMzODUzZWViNjVmMg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.00129866",
|
||||
"sender": "0xda13d64ec95cd6775291b1c321027121be209df0",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjIuMzc2MTE5MjQyMl9VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 2.3761192422,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnM00yTXdZMlJpTnpReU9UVTJZVFUxWXpZd016YzBOemd6TkRRNVpUSmpNbVZtTURnM1lqUTVPRFl4TVdGak5EZ3dZalJrTVRFMU1UbGhZemRpTXpZNVh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VHUXpaR1UwTkRneE5qTXlNakl5TURVME9UazJZVE0yTlRsaE5UTXlNR0k1TWpWbU5qUXhNR1k9",
|
||||
"timestamp": 1684006019,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg3M2MwY2RiNzQyOTU2YTU1YzYwMzc0NzgzNDQ5ZTJjMmVmMDg3YjQ5ODYxMWFjNDgwYjRkMTE1MTlhYzdiMzY5XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weGQzZGU0NDgxNjMyMjIyMDU0OTk2YTM2NTlhNTMyMGI5MjVmNjQxMGY=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17253116,
|
||||
"hash": "0x73c0cdb742956a55c60374783449e2c2ef087b498611ac480b4d11519ac7b369",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xd3de4481632222054996a3659a5320b925f6410f",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 459,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHhiZTgyODI1NjRlYzJiNzAwMDlmMmQ2ODk1NDAxMmViMDlmNDhiYzhkXzB4NzNjMGNkYjc0Mjk1NmE1NWM2MDM3NDc4MzQ0OWUyYzJlZjA4N2I0OTg2MTFhYzQ4MGI0ZDExNTE5YWM3YjM2OQ==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fMHhkM2RlNDQ4MTYzMjIyMjA1NDk5NmEzNjU5YTUzMjBiOTI1ZjY0MTBm",
|
||||
"name": "EL CHAPO",
|
||||
"symbol": "CHAPO",
|
||||
"address": "0xd3de4481632222054996a3659a5320b925f6410f",
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4ZDNkZTQ0ODE2MzIyMjIwNTQ5OTZhMzY1OWE1MzIwYjkyNWY2NDEwZg==",
|
||||
"isSpam": true,
|
||||
"logo": null,
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "ERC20",
|
||||
"quantity": "50000000000000.002683081102196736",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0xbe8282564ec2b70009f2d68954012eb09f48bc8d",
|
||||
"direction": "OUT",
|
||||
"transactedValue": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
}
|
||||
],
|
||||
"__typename": "Portfolio"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
1
cypress/fixtures/mini-portfolio/nfts.json
Normal file
1
cypress/fixtures/mini-portfolio/nfts.json
Normal file
File diff suppressed because one or more lines are too long
1
cypress/fixtures/mini-portfolio/pools.json
Normal file
1
cypress/fixtures/mini-portfolio/pools.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
cypress/fixtures/mini-portfolio/tokens.json
Normal file
1
cypress/fixtures/mini-portfolio/tokens.json
Normal file
File diff suppressed because one or more lines are too long
102
cypress/fixtures/mini-portfolio/uniswapx_activity.json
Normal file
102
cypress/fixtures/mini-portfolio/uniswapx_activity.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"data": {
|
||||
"portfolios": [
|
||||
{
|
||||
"id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==",
|
||||
"assetActivities": [
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNE9EZGpOemN5TlRRNU1qWTVNVEkwWVRkbVpUTXlNams1TjJJNU0yUTJabUV3TjJObE1UQXhOamxrTjJJd1pXUXhObUV6TldabU16SmtOMk13TWpBeVh6QjRaREkzTXpnek1EUTRaalF4WldZMlpXRXhaV1EzWWpBeFltVTVOemRqTjJVME1HSXdaRGswTmw4d2VEUTNZVFF5TVdKalpXTTJORE5oWWpSallURmpZamc0TmpOaU4yWm1PV0ppWm1SaU5HVmlNVE09",
|
||||
"timestamp": 1691001923,
|
||||
"type": "SWAP_ORDER",
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"__typename": "TransactionDetails",
|
||||
"id": "VHJhbnNhY3Rpb246MHg4ODdjNzcyNTQ5MjY5MTI0YTdmZTMyMjk5N2I5M2Q2ZmEwN2NlMTAxNjlkN2IwZWQxNmEzNWZmMzJkN2MwMjAyXzB4ZDI3MzgzMDQ4ZjQxZWY2ZWExZWQ3YjAxYmU5NzdjN2U0MGIwZDk0Nl8weDQ3YTQyMWJjZWM2NDNhYjRjYTFjYjg4NjNiN2ZmOWJiZmRiNGViMTM=",
|
||||
"type": "SWAP_ORDER",
|
||||
"from": "0xd27383048f41ef6ea1ed7b01be977c7e40b0d946",
|
||||
"to": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13",
|
||||
"hash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"nonce": 439,
|
||||
"status": "CONFIRMED"
|
||||
},
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDgwYmVjYjgwOGJmYWRlNDE0MzE4M2U1OGQxOGYyMDgwZTg0ZTU3YTFfMHg0N2E0MjFiY2VjNjQzYWI0Y2ExY2I4ODYzYjdmZjliYmZkYjRlYjEzXzB4ODg3Yzc3MjU0OTI2OTEyNGE3ZmUzMjI5OTdiOTNkNmZhMDdjZTEwMTY5ZDdiMGVkMTZhMzVmZjMyZDdjMDIwMg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fMHhhMGI4Njk5MWM2MjE4YjM2YzFkMTlkNGEyZTllYjBjZTM2MDZlYjQ4",
|
||||
"name": "USD Coin",
|
||||
"symbol": "USDC",
|
||||
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
"decimals": 6,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YTBiODY5OTFjNjIxOGIzNmMxZDE5ZDRhMmU5ZWIwY2UzNjA2ZWI0OA==",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL1VuaXN3YXAvYXNzZXRzL21hc3Rlci9ibG9ja2NoYWlucy9ldGhlcmV1bS9hc3NldHMvMHhBMGI4Njk5MWM2MjE4YjM2YzFkMTlENGEyZTlFYjBjRTM2MDZlQjQ4L2xvZ28ucG5n",
|
||||
"url": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "ERC20",
|
||||
"quantity": "300.0",
|
||||
"sender": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"recipient": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjMwMC4xNDkxNTIwOTE5NDE2M19VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 300.14915209194163,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDQ3YTQyMWJjZWM2NDNhYjRjYTFjYjg4NjNiN2ZmOWJiZmRiNGViMTNfMHg4MGJlY2I4MDhiZmFkZTQxNDMxODNlNThkMThmMjA4MGU4NGU1N2ExXzB4ODg3Yzc3MjU0OTI2OTEyNGE3ZmUzMjI5OTdiOTNkNmZhMDdjZTEwMTY5ZDdiMGVkMTZhMzVmZjMyZDdjMDIwMg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fMHg2YjE3NTQ3NGU4OTA5NGM0NGRhOThiOTU0ZWVkZWFjNDk1MjcxZDBm",
|
||||
"name": "Dai Stablecoin",
|
||||
"symbol": "DAI",
|
||||
"address": "0x6b175474e89094c44da98b954eedeac495271d0f",
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4NmIxNzU0NzRlODkwOTRjNDRkYTk4Yjk1NGVlZGVhYzQ5NTI3MWQwZg==",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL1VuaXN3YXAvYXNzZXRzL21hc3Rlci9ibG9ja2NoYWlucy9ldGhlcmV1bS9hc3NldHMvMHg2QjE3NTQ3NEU4OTA5NEM0NERhOThiOTU0RWVkZUFDNDk1MjcxZDBGL2xvZ28ucG5n",
|
||||
"url": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "ERC20",
|
||||
"quantity": "280.573117586837733376",
|
||||
"sender": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13",
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjI4MC42ODc3OTU0NTg2ODE4X1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 280.6877954586818,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
],
|
||||
"__typename": "AssetActivity"
|
||||
}
|
||||
],
|
||||
"__typename": "Portfolio"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
26
cypress/fixtures/uniswapx/expiredStatusResponse.json
Normal file
26
cypress/fixtures/uniswapx/expiredStatusResponse.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"orderStatus": "expired",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
562
cypress/fixtures/uniswapx/feeQuote.json
Normal file
562
cypress/fixtures/uniswapx/feeQuote.json
Normal file
@@ -0,0 +1,562 @@
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
|
||||
"nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633",
|
||||
"deadline": 1697481666,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1697481594,
|
||||
"decayEndTime": 1697481654,
|
||||
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "200000000",
|
||||
"endAmount": "200000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": "123803169993201727",
|
||||
"endAmount": "117908377342236273",
|
||||
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
|
||||
},
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": "185983730585681",
|
||||
"endAmount": "177128258400955",
|
||||
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327",
|
||||
"quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890",
|
||||
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
|
||||
"orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9",
|
||||
"startTimeBufferSecs": 45,
|
||||
"auctionPeriodSecs": 60,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
|
||||
},
|
||||
"deadline": 1697481666,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
|
||||
},
|
||||
"deadline": 1697481666,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1697481594,
|
||||
"decayEndTime": 1697481654,
|
||||
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x01b7d653c183183f"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x01a2e50b6386d671"
|
||||
},
|
||||
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
|
||||
},
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0xa926b6321051"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0xa118e2ebf2bb"
|
||||
},
|
||||
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"portionBips": 15,
|
||||
"portionAmount": "185983730585681",
|
||||
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
},
|
||||
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
|
||||
"allQuotes": [
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
|
||||
"nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633",
|
||||
"deadline": 1697481666,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1697481594,
|
||||
"decayEndTime": 1697481654,
|
||||
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "200000000",
|
||||
"endAmount": "200000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": "123803169993201727",
|
||||
"endAmount": "117908377342236273",
|
||||
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
|
||||
},
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": "185983730585681",
|
||||
"endAmount": "177128258400955",
|
||||
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327",
|
||||
"quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890",
|
||||
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
|
||||
"orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9",
|
||||
"startTimeBufferSecs": 45,
|
||||
"auctionPeriodSecs": 60,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
|
||||
},
|
||||
"deadline": 1697481666,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
|
||||
},
|
||||
"deadline": 1697481666,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1697481594,
|
||||
"decayEndTime": 1697481654,
|
||||
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x01b7d653c183183f"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x01a2e50b6386d671"
|
||||
},
|
||||
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
|
||||
},
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0xa926b6321051"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0xa118e2ebf2bb"
|
||||
},
|
||||
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"portionBips": 15,
|
||||
"portionAmount": "185983730585681",
|
||||
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
},
|
||||
{
|
||||
"routing": "CLASSIC",
|
||||
"quote": {
|
||||
"methodParameters": {
|
||||
"calldata": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000652d85d0000000000000000000000000000000000000000000000000000000000000000308060c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000001bdf1285753b47400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000037a8f295612602f2774d331e562be9e61b83a327000000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000001bd45ea74e458eb",
|
||||
"value": "0x00",
|
||||
"to": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"
|
||||
},
|
||||
"blockNumber": "18364784",
|
||||
"amount": "200000000",
|
||||
"amountDecimals": "200",
|
||||
"quote": "126149127803342909",
|
||||
"quoteDecimals": "0.126149127803342909",
|
||||
"quoteGasAdjusted": "122888348391508943",
|
||||
"quoteGasAdjustedDecimals": "0.122888348391508943",
|
||||
"quoteGasAndPortionAdjusted": "122699124699803928",
|
||||
"quoteGasAndPortionAdjustedDecimals": "0.122699124699803928",
|
||||
"gasUseEstimateQuote": "3260779411833966",
|
||||
"gasUseEstimateQuoteDecimals": "0.003260779411833966",
|
||||
"gasUseEstimate": "240911",
|
||||
"gasUseEstimateUSD": "5.153332510477604328",
|
||||
"simulationStatus": "SUCCESS",
|
||||
"simulationError": false,
|
||||
"gasPriceWei": "13535203506",
|
||||
"route": [
|
||||
[
|
||||
{
|
||||
"type": "v2-pool",
|
||||
"address": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
|
||||
"tokenIn": {
|
||||
"chainId": 1,
|
||||
"decimals": "6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"symbol": "USDC"
|
||||
},
|
||||
"tokenOut": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"symbol": "WETH"
|
||||
},
|
||||
"reserve0": {
|
||||
"token": {
|
||||
"chainId": 1,
|
||||
"decimals": "6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"symbol": "USDC"
|
||||
},
|
||||
"quotient": "27487668611269"
|
||||
},
|
||||
"reserve1": {
|
||||
"token": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"symbol": "WETH"
|
||||
},
|
||||
"quotient": "17390022942803382004255"
|
||||
},
|
||||
"amountIn": "200000000",
|
||||
"amountOut": "125959904111637894"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routeString": "[V2] 100.00% = USDC -- [0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc] --> WETH",
|
||||
"quoteId": "f46cf31c-251e-470c-bd57-13209015694e",
|
||||
"portionBips": 15,
|
||||
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327",
|
||||
"portionAmount": "189223691705014",
|
||||
"portionAmountDecimals": "0.000189223691705014",
|
||||
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
|
||||
"tradeType": "EXACT_INPUT",
|
||||
"slippage": 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
114
cypress/fixtures/uniswapx/fillTransactionReceipt.json
Normal file
114
cypress/fixtures/uniswapx/fillTransactionReceipt.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"to": "0xbD7F9D0239f81C94b728d827a87b9864972661eC",
|
||||
"from": "0xa17Fbb0b5a251A7ACA3BD7377e7eCC4F700A2C09",
|
||||
"contractAddress": null,
|
||||
"transactionIndex": 61,
|
||||
"gasUsed": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x03e0c8"
|
||||
},
|
||||
"logsBloom":
|
||||
"0x00000000000000000000008000200100000020000000000000000000000000000000000000000000000000010000000000000000000020000000000001000000000280000000000808000008000000000000000000000000000000000000200010000000100000000008000000000004402000080000000000000010000800000000000000000800000800000000000000000000010000000000000000000000000000000000200000000000005000000000000000000000000000000000000000000002000000000000000000000000040002000000000000000100000000090000000400000000000400000020080000000000000000000000000000000000",
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c",
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"logs": [
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000005f5e100",
|
||||
"logIndex": 103,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0xbD7F9D0239f81C94b728d827a87b9864972661eC",
|
||||
"topics": [
|
||||
"0x78ad7ec0e9f89e74012afa58738b6b661c024cb0fd185ee2f616c0a28924bd66",
|
||||
"0xd10e1d90145460003d98ba4b788564e9549cc93c65a12c9b297720a9d6a586de",
|
||||
"0x000000000000000000000000a17fbb0b5a251a7aca3bd7377e7ecc4f700a2c09",
|
||||
"0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1"
|
||||
],
|
||||
"data": "0x8e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de00",
|
||||
"logIndex": 104,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x0000000000000000000000005777d92f208679db4b9778590fa3cab3ac9e2168",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000056b9a675be430b502",
|
||||
"logIndex": 105,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf",
|
||||
"0x0000000000000000000000005777d92f208679db4b9778590fa3cab3ac9e2168"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000005f5e100",
|
||||
"logIndex": 106,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168",
|
||||
"topics": [
|
||||
"0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
|
||||
"0x00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf"
|
||||
],
|
||||
"data": "0xfffffffffffffffffffffffffffffffffffffffffffffffa946598a41bcf4afe0000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000010c7063b90a5e90d13830000000000000000000000000000000000000000000071b57cb2bb0b5b28224ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc89c",
|
||||
"logIndex": 107,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf",
|
||||
"0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1"
|
||||
],
|
||||
"data": "0x000000000000000000000000000000000000000000000004f409bcc7a52b6358",
|
||||
"logIndex": 108,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
}
|
||||
],
|
||||
"blockNumber": 17444757,
|
||||
"confirmations": 392238,
|
||||
"cumulativeGasUsed": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x4065ac"
|
||||
},
|
||||
"effectiveGasPrice": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x04aa792df0"
|
||||
},
|
||||
"status": 1,
|
||||
"type": 2,
|
||||
"byzantium": true
|
||||
}
|
||||
33
cypress/fixtures/uniswapx/filledStatusResponse.json
Normal file
33
cypress/fixtures/uniswapx/filledStatusResponse.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"settledAmounts": [
|
||||
{
|
||||
"tokenOut": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"amountOut": "91371770080538616664"
|
||||
}
|
||||
],
|
||||
"orderStatus": "filled",
|
||||
"txHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"orderStatus": "insufficient-funds",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
cypress/fixtures/uniswapx/openStatusResponse.json
Normal file
26
cypress/fixtures/uniswapx/openStatusResponse.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"orderStatus": "open",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
cypress/fixtures/uniswapx/orderResponse.json
Normal file
1
cypress/fixtures/uniswapx/orderResponse.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19"}
|
||||
493
cypress/fixtures/uniswapx/quote1.json
Normal file
493
cypress/fixtures/uniswapx/quote1.json
Normal file
@@ -0,0 +1,493 @@
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": "57335948072881703373319552024074512292695687510330025934414357004397546394368",
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": "0",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "300000000",
|
||||
"endAmount": "300000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "289951120815684452958",
|
||||
"endAmount": "267060007981523637666",
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064c91e6e0000000000000000000000000000000000000000000000000000000064c91eaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000000000000011e1a30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e37953000000000000000000000000000000000000000000000000000000000064c91eb6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000fb7e15027ad3e025e00000000000000000000000000000000000000000000000e7a33be508bb395a200000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd",
|
||||
"quoteId": "f9f47cd7-a62c-4622-9ac7-51d0e662245a",
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"auctionPeriodSecs": 60,
|
||||
"startTimeBufferSecs": 30,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x00"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0fb7e15027ad3e025e"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0e7a33be508bb395a2"
|
||||
},
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"allQuotes": [
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": "57335948072881703373319552024074512292695687510330025934414357004397546394368",
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": "0",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "300000000",
|
||||
"endAmount": "300000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "289951120815684452958",
|
||||
"endAmount": "267060007981523637666",
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064c91e6e0000000000000000000000000000000000000000000000000000000064c91eaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000000000000011e1a30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e37953000000000000000000000000000000000000000000000000000000000064c91eb6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000fb7e15027ad3e025e00000000000000000000000000000000000000000000000e7a33be508bb395a200000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd",
|
||||
"quoteId": "f9f47cd7-a62c-4622-9ac7-51d0e662245a",
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"auctionPeriodSecs": 60,
|
||||
"startTimeBufferSecs": 30,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x00"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0fb7e15027ad3e025e"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0e7a33be508bb395a2"
|
||||
},
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"routing": "CLASSIC",
|
||||
"quote": {
|
||||
"blockNumber": "17820918",
|
||||
"amount": "300000000",
|
||||
"amountDecimals": "300",
|
||||
"quote": "299952256425393549464",
|
||||
"quoteDecimals": "299.952256425393549464",
|
||||
"quoteGasAdjusted": "289922128602824170541",
|
||||
"quoteGasAdjustedDecimals": "289.922128602824170541",
|
||||
"gasUseEstimateQuote": "10030127822569378922",
|
||||
"gasUseEstimateQuoteDecimals": "10.030127822569378922",
|
||||
"gasUseEstimate": "128000",
|
||||
"gasUseEstimateUSD": "10.031724",
|
||||
"simulationStatus": "UNATTEMPTED",
|
||||
"simulationError": false,
|
||||
"gasPriceWei": "42803167855",
|
||||
"route": [
|
||||
[
|
||||
{
|
||||
"type": "v3-pool",
|
||||
"address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168",
|
||||
"tokenIn": {
|
||||
"chainId": 1,
|
||||
"decimals": "6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"symbol": "USDC"
|
||||
},
|
||||
"tokenOut": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"symbol": "DAI"
|
||||
},
|
||||
"fee": "100",
|
||||
"liquidity": "534676532046235168447130",
|
||||
"sqrtRatioX96": "79230505815006815109584",
|
||||
"tickCurrent": "-276324",
|
||||
"amountIn": "300000000",
|
||||
"amountOut": "299952256425393549464"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routeString": "[V3] 100.00% = USDC -- 0.01% [0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168] --> DAI",
|
||||
"quoteId": "1dd3bd14-780e-41c6-88e1-30a763f97482",
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"tradeType": "EXACT_INPUT",
|
||||
"slippage": 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
493
cypress/fixtures/uniswapx/quote2.json
Normal file
493
cypress/fixtures/uniswapx/quote2.json
Normal file
@@ -0,0 +1,493 @@
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": "1993350209834725680308575292465150260730647098062962750049345504775310970881",
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"startAmount": "1000000000000000000",
|
||||
"endAmount": "1000000000000000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "929502510517534478575",
|
||||
"endAmount": "919795986077127665276",
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064cd4f240000000000000000000000000000000000000000000000000000000064cd4f60000000000000000000000000165d98de005d2818176b99b1a93b9325dbe581810000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000000000000000000000000000000000000000000000468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d8090010000000000000000000000000000000000000000000000000000000064cd4f6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000092d6c1e31e14520e676a687f0a93788b716beff500000000000000000000000000000000000000000000003263704899af6e50ef000000000000000000000000000000000000000000000031dcbbc80c9555e67c0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"quoteId": "09ce28b7-1ddf-4317-a28d-d21092be9f84",
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"auctionPeriodSecs": 60,
|
||||
"startTimeBufferSecs": 30,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x3263704899af6e50ef"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x31dcbbc80c9555e67c"
|
||||
},
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"allQuotes": [
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": "1993350209834725680308575292465150260730647098062962750049345504775310970881",
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"startAmount": "1000000000000000000",
|
||||
"endAmount": "1000000000000000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "929502510517534478575",
|
||||
"endAmount": "919795986077127665276",
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064cd4f240000000000000000000000000000000000000000000000000000000064cd4f60000000000000000000000000165d98de005d2818176b99b1a93b9325dbe581810000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000000000000000000000000000000000000000000000468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d8090010000000000000000000000000000000000000000000000000000000064cd4f6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000092d6c1e31e14520e676a687f0a93788b716beff500000000000000000000000000000000000000000000003263704899af6e50ef000000000000000000000000000000000000000000000031dcbbc80c9555e67c0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"quoteId": "09ce28b7-1ddf-4317-a28d-d21092be9f84",
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"auctionPeriodSecs": 60,
|
||||
"startTimeBufferSecs": 30,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x3263704899af6e50ef"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x31dcbbc80c9555e67c"
|
||||
},
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"routing": "CLASSIC",
|
||||
"quote": {
|
||||
"blockNumber": "17843654",
|
||||
"amount": "1000000000000000000",
|
||||
"amountDecimals": "1",
|
||||
"quote": "931181529570145926787",
|
||||
"quoteDecimals": "931.181529570145926787",
|
||||
"quoteGasAdjusted": "929033336026294051828",
|
||||
"quoteGasAdjustedDecimals": "929.033336026294051828",
|
||||
"gasUseEstimateQuote": "2148193543851874958",
|
||||
"gasUseEstimateQuoteDecimals": "2.148193543851874958",
|
||||
"gasUseEstimate": "128000",
|
||||
"gasUseEstimateUSD": "4.174934",
|
||||
"simulationStatus": "UNATTEMPTED",
|
||||
"simulationError": false,
|
||||
"gasPriceWei": "17811260539",
|
||||
"route": [
|
||||
[
|
||||
{
|
||||
"type": "v3-pool",
|
||||
"address": "0xD8de6af55F618a7Bc69835D55DDC6582220c36c0",
|
||||
"tokenIn": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"symbol": "WETH"
|
||||
},
|
||||
"tokenOut": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"symbol": "DAI"
|
||||
},
|
||||
"fee": "3000",
|
||||
"liquidity": "62287359628325896425115",
|
||||
"sqrtRatioX96": "2591813593283507889384697884",
|
||||
"tickCurrent": "-68403",
|
||||
"amountIn": "1000000000000000000",
|
||||
"amountOut": "931181529570145926787"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routeString": "[V3] 100.00% = WETH -- 0.3% [0xD8de6af55F618a7Bc69835D55DDC6582220c36c0] --> DAI",
|
||||
"quoteId": "414e5f1c-120a-4e35-9760-c54d4b09e91d",
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"tradeType": "EXACT_INPUT",
|
||||
"slippage": 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
19
cypress/staging/t9n.test.ts
Normal file
19
cypress/staging/t9n.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('translations', () => {
|
||||
it('loads locale from the query param', () => {
|
||||
cy.visit('/?lng=fr-FR')
|
||||
cy.contains('Échanger')
|
||||
cy.contains('Uniswap disponible en : English')
|
||||
})
|
||||
|
||||
it('loads locale from menu', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('français').click({ force: true })
|
||||
cy.location('search').should('match', /\?lng=fr-FR$/)
|
||||
cy.contains('Échanger')
|
||||
cy.contains('Uniswap disponible en : English')
|
||||
})
|
||||
})
|
||||
@@ -2,10 +2,9 @@ import 'cypress-hardhat/lib/browser'
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
|
||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
import { injected } from './ethereum'
|
||||
import { FeatureFlag } from '../../src/featureFlags'
|
||||
import { initialState, UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE, setInitialUserState } from '../utils/user-state'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@@ -13,15 +12,19 @@ declare global {
|
||||
interface ApplicationWindow {
|
||||
ethereum: Eip1193Bridge
|
||||
}
|
||||
interface Chainable<Subject> {
|
||||
/**
|
||||
* Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event.
|
||||
*
|
||||
* @param {string} eventName - The type of the event to search for e.g. SwapEventName.SWAP_TRANSACTION_COMPLETED
|
||||
* @param {number} timeout - The maximum amount of time (in ms) to wait for the event.
|
||||
* @returns {Chainable<Subject>}
|
||||
*/
|
||||
waitForAmplitudeEvent(eventName: string, timeout?: number): Chainable<Subject>
|
||||
}
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
/**
|
||||
* The mock ethereum provider to inject into the page.
|
||||
* @default 'goerli'
|
||||
*/
|
||||
// TODO(INFRA-175): Migrate all usage of 'goerli' to 'hardhat'.
|
||||
ethereum?: 'goerli' | 'hardhat'
|
||||
featureFlags?: Array<{ name: FeatureFlag; value: boolean }>
|
||||
/**
|
||||
* Initial user state.
|
||||
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
|
||||
@@ -38,43 +41,51 @@ Cypress.Commands.overwrite(
|
||||
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
|
||||
if (typeof url !== 'string') throw new Error('Invalid arguments. The first argument to cy.visit must be the path.')
|
||||
|
||||
// Add a hash in the URL if it is not present (to use hash-based routing correctly with queryParams).
|
||||
let hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
|
||||
if (options?.ethereum === 'goerli') hashUrl += `${url.includes('?') ? '&' : '?'}chain=goerli`
|
||||
|
||||
return cy
|
||||
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
|
||||
.provider()
|
||||
.then((provider) =>
|
||||
original({
|
||||
...options,
|
||||
url: hashUrl,
|
||||
url,
|
||||
onBeforeLoad(win) {
|
||||
options?.onBeforeLoad?.(win)
|
||||
|
||||
// We want to test from a clean state, so we clear the local storage (which clears redux).
|
||||
win.localStorage.clear()
|
||||
|
||||
// Set initial user state.
|
||||
win.localStorage.setItem(
|
||||
'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
|
||||
JSON.stringify(options?.userState ?? CONNECTED_WALLET_USER_STATE)
|
||||
)
|
||||
setInitialUserState(win, {
|
||||
...initialState,
|
||||
...CONNECTED_WALLET_USER_STATE,
|
||||
...(options?.userState ?? {}),
|
||||
})
|
||||
|
||||
// Set feature flags, if configured.
|
||||
if (options?.featureFlags) {
|
||||
const featureFlags = options.featureFlags.reduce((flags, flag) => ({ ...flags, [flag]: 'enabled' }), {})
|
||||
const featureFlags = options.featureFlags.reduce(
|
||||
(flags, flag) => ({ ...flags, [flag.name]: flag.value ? 'enabled' : 'control' }),
|
||||
{}
|
||||
)
|
||||
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
|
||||
}
|
||||
|
||||
// Inject the mock ethereum provider.
|
||||
if (options?.ethereum === 'hardhat') {
|
||||
win.ethereum = provider
|
||||
} else {
|
||||
win.ethereum = injected
|
||||
}
|
||||
win.ethereum = provider
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Cypress.Commands.add('waitForAmplitudeEvent', (eventName) => {
|
||||
function checkRequest() {
|
||||
return cy.wait('@amplitude').then((interception) => {
|
||||
const events = interception.request.body.events
|
||||
const event = events.find((event: any) => event.event_type === eventName)
|
||||
|
||||
if (event) {
|
||||
return cy.wrap(event)
|
||||
} else {
|
||||
return checkRequest()
|
||||
}
|
||||
})
|
||||
}
|
||||
return checkRequest()
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
import '@cypress/code-coverage/support'
|
||||
import './commands'
|
||||
import './setupTests'
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Updates cy.visit() to include an injected window.ethereum provider.
|
||||
*/
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
import { Wallet } from '@ethersproject/wallet'
|
||||
|
||||
import { SupportedChainId } from '../../src/constants/chains'
|
||||
|
||||
// todo: figure out how env vars actually work in CI
|
||||
// const TEST_PRIVATE_KEY = Cypress.env('INTEGRATION_TEST_PRIVATE_KEY')
|
||||
const TEST_PRIVATE_KEY = '0xe580410d7c37d26c6ad1a837bbae46bc27f9066a466fb3a66e770523b4666d19'
|
||||
|
||||
// address of the above key
|
||||
const TEST_ADDRESS_NEVER_USE = new Wallet(TEST_PRIVATE_KEY).address
|
||||
const CHAIN_ID = SupportedChainId.GOERLI
|
||||
const HEXLIFIED_CHAIN_ID = `0x${CHAIN_ID.toString(16)}`
|
||||
|
||||
const provider = new JsonRpcProvider('https://goerli.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 5)
|
||||
const signer = new Wallet(TEST_PRIVATE_KEY, provider)
|
||||
export const injected = new (class extends Eip1193Bridge {
|
||||
chainId = CHAIN_ID
|
||||
|
||||
async sendAsync(...args: any[]) {
|
||||
console.debug('sendAsync called', ...args)
|
||||
return this.send(...args)
|
||||
}
|
||||
async send(...args: any[]) {
|
||||
console.debug('send called', ...args)
|
||||
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
|
||||
let callback
|
||||
let method
|
||||
let params
|
||||
if (isCallbackForm) {
|
||||
callback = args[1]
|
||||
method = args[0].method
|
||||
params = args[0].params
|
||||
} else {
|
||||
method = args[0]
|
||||
params = args[1]
|
||||
}
|
||||
if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
|
||||
if (isCallbackForm) {
|
||||
callback({ result: [TEST_ADDRESS_NEVER_USE] })
|
||||
} else {
|
||||
return Promise.resolve([TEST_ADDRESS_NEVER_USE])
|
||||
}
|
||||
}
|
||||
if (method === 'eth_chainId') {
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result: HEXLIFIED_CHAIN_ID })
|
||||
} else {
|
||||
return Promise.resolve(HEXLIFIED_CHAIN_ID)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await super.send(method, params)
|
||||
console.debug('result received', method, params, result)
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result })
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
if (isCallbackForm) {
|
||||
callback(error, null)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
})(signer, provider)
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-ignore
|
||||
import TokenListJSON from '@uniswap/default-token-list'
|
||||
import { CyHttpMessages } from 'cypress/types/net-stubbing'
|
||||
|
||||
beforeEach(() => {
|
||||
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
|
||||
@@ -8,28 +9,34 @@ beforeEach(() => {
|
||||
req.headers['origin'] = 'https://app.uniswap.org'
|
||||
})
|
||||
|
||||
// Infura uses a test endpoint, which allow-lists http://localhost:3000 instead.
|
||||
cy.intercept(/infura.io/, (req) => {
|
||||
req.headers['referer'] = 'http://localhost:3000'
|
||||
req.headers['origin'] = 'http://localhost:3000'
|
||||
req.alias = req.body.method
|
||||
req.continue()
|
||||
})
|
||||
// Network RPCs are disabled for cypress tests - calls should be routed through the connected wallet instead.
|
||||
cy.intercept(/infura.io/, { statusCode: 404 })
|
||||
cy.intercept(/quiknode.pro/, { statusCode: 404 })
|
||||
|
||||
// Log requests to hardhat.
|
||||
cy.intercept(/:8545/, logJsonRpc)
|
||||
|
||||
// Mock analytics responses to avoid analytics in tests.
|
||||
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const requestBody = JSON.stringify(req.body)
|
||||
const byteSize = new Blob([requestBody]).size
|
||||
req.alias = 'amplitude'
|
||||
req.reply(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
server_upload_time: Date.now(),
|
||||
payload_size_bytes: byteSize,
|
||||
events_ingested: req.body.events.length,
|
||||
})
|
||||
}),
|
||||
{
|
||||
'origin-country': 'US',
|
||||
}
|
||||
)
|
||||
}).intercept('https://*.sentry.io', { statusCode: 200 })
|
||||
|
||||
// Mock statsig to allow us to mock flags.
|
||||
cy.intercept(/statsig/, { statusCode: 409 })
|
||||
|
||||
// Mock our own token list responses to avoid the latency of IPFS.
|
||||
cy.intercept('https://gateway.ipfs.io/ipns/tokens.uniswap.org', TokenListJSON)
|
||||
.intercept('https://gateway.ipfs.io/ipns/extendedtokens.uniswap.org', { statusCode: 404 })
|
||||
@@ -39,3 +46,21 @@ beforeEach(() => {
|
||||
// This resets the fork, as well as options like automine.
|
||||
cy.hardhat().then((hardhat) => hardhat.reset())
|
||||
})
|
||||
|
||||
function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) {
|
||||
req.alias = req.body.method
|
||||
const log = Cypress.log({
|
||||
autoEnd: false,
|
||||
name: req.body.method,
|
||||
message: req.body.params?.map((param: any) =>
|
||||
typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10)
|
||||
),
|
||||
})
|
||||
req.on('after:response', (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
log.end()
|
||||
} else {
|
||||
log.error(new Error(`${res.statusCode}: ${res.statusMessage}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"composite": false,
|
||||
"incremental": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"target": "ES5",
|
||||
"isolatedModules": false,
|
||||
"noImplicitAny": false,
|
||||
"target": "ES6",
|
||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
|
||||
"types": ["cypress", "node"]
|
||||
"types": ["cypress", "node"],
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["**/*.ts"],
|
||||
"watchOptions": {
|
||||
"excludeDirectories": ["node_modules"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,42 @@
|
||||
import { connectionMetaKey } from '../../src/connection/meta'
|
||||
import { ConnectionType } from '../../src/connection/types'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
|
||||
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: 'INJECTED' }
|
||||
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: ConnectionType.INJECTED }
|
||||
|
||||
export const DISCONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: undefined }
|
||||
|
||||
/**
|
||||
* This sets the initial value of the "user" slice in IndexedDB.
|
||||
* Other persisted slices are not set, so they will be filled with their respective initial values
|
||||
* when the app runs.
|
||||
*/
|
||||
export function setInitialUserState(win: Cypress.AUTWindow, state: UserState) {
|
||||
// Selected wallet should also be reflected in localStorage, so that eager connections work.
|
||||
if (state.selectedWallet) {
|
||||
win.localStorage.setItem(
|
||||
connectionMetaKey,
|
||||
JSON.stringify({
|
||||
type: state.selectedWallet,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
win.indexedDB.deleteDatabase('redux')
|
||||
const dbRequest = win.indexedDB.open('redux')
|
||||
dbRequest.onsuccess = function () {
|
||||
const db = dbRequest.result
|
||||
const transaction = db.transaction('keyvaluepairs', 'readwrite')
|
||||
const store = transaction.objectStore('keyvaluepairs')
|
||||
store.put(
|
||||
{
|
||||
user: state,
|
||||
},
|
||||
'persist:interface'
|
||||
)
|
||||
}
|
||||
dbRequest.onupgradeneeded = function () {
|
||||
const db = dbRequest.result
|
||||
db.createObjectStore('keyvaluepairs')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'enforce use of retry() for dynamic imports',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportExpression(node) {
|
||||
const grandParent = node.parent.parent
|
||||
if (
|
||||
!(
|
||||
grandParent &&
|
||||
grandParent.type === 'CallExpression' &&
|
||||
// Technically, we are only checking that a function named `retry` wraps the dynamic import.
|
||||
// We do not go as far as enforcing that it is import('utils/retry').retry
|
||||
grandParent.callee.name === 'retry' &&
|
||||
grandParent.arguments.length === 1 &&
|
||||
grandParent.arguments[0].type === 'ArrowFunctionExpression'
|
||||
)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Dynamic import should be wrapped in retry (see `utils/retry.ts`): `retry(() => import(...))`',
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
50
functions/README.md
Normal file
50
functions/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Cloudflare Cloud Functions
|
||||
|
||||
## Purpose
|
||||
|
||||
These functions utilize Cloudflare Functions to dynamically inject meta tags server side for richer link sharing capabilities.
|
||||
|
||||
## Functions
|
||||
|
||||
Currently, there are 2 types of cloudflare functions developed
|
||||
|
||||
- Meta Data Injectors - Workers that inject [Open Graph](https://ogp.me/) standardized meta tags into the `header` of specific webpages.
|
||||
- Currently we support this functionaltiy for three separate webpages: NFT Assets, NFT Collections, and Token Detail Pages
|
||||
- These functions query data from GraphQL and then formats them into HTML `meta` tags to be injected
|
||||
- Dynamically Generated Images - Utilizes Vercel's [Open Graph Image Generation Library](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) to create custom thumbnails for specific webpages
|
||||
- Currently supports NFT Assets, NFT Collections, and Token Detail Pages
|
||||
- These functions query data from GraphQL, and utilize `Satori` to convert HTML into a png image response which is then returned when the api is called.
|
||||
- Can be found in the `api/image` folder.
|
||||
|
||||
## Testing
|
||||
|
||||
Testing is done utilizing a custom jest environment as well as Cloudflare's local tester: `wrangler`. Wrangler enables testing locally by running a proxy to wrap `localhost`. Tests run against a proxy server, so you'll need to start it before running tests:
|
||||
- Manually run `yarn start:cloud` to setup wrangler on `localhost:3000`
|
||||
- Run unit tests with `yarn test:cloud`
|
||||
|
||||
## Deployment
|
||||
|
||||
Functions will be deployed to Cloudlfare where they will be ran automatically when the appropriate route is hit.
|
||||
|
||||
## Miscellaneous
|
||||
- Caching: In order to speed up webpage requests, repeated GraphQL queries will be saved and pulled using Cloudflare's Cache API.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `yarn start:cloud` (NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --node-compat --proxy=3001 --port=3000 -- yarn start), script to start local wrangler environment
|
||||
- `npx wrangler pages dev`: this basis of this command which starts a local instance of wrangler to test cloud functions
|
||||
- `--node-compat`: wrangler option that enables compatibility with Node.js modules
|
||||
- `--proxy:3001`: telling the proxy to listen on port 3001
|
||||
- `--port=3000`: telling wrangler to run our proxy on port 3000
|
||||
- `NODE_OPTIONS=--dns-result-order=ipv4first`: wrangler still serves to IPv4 which isn't compatible with Node 18 which default resolves to IPv6 so we need to specify to serve to IPv4
|
||||
- `PORT-3001 --yarn start`: runs default yarn start on port 3001
|
||||
- `yarn test:cloud` (NODE_OPTIONS=--experimental-vm-modules yarn jest functions --watch --config=functions/jest.config.json), script to test cloud functions with jest
|
||||
- `NODE_OPTIONS=--experimental-vm-modules`: support for ES Modules and Web Assembly
|
||||
- `--config=functions/jest.config.json`: specifying which config file to use
|
||||
|
||||
## Additional Documents
|
||||
- [Open Graph Protocol](https://ogp.me/)
|
||||
- [Open Graph Image Generation](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation)
|
||||
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
||||
- [HTML Rewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/)
|
||||
- [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/)
|
||||
18
functions/[[index]].ts
Normal file
18
functions/[[index]].ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { MetaTagInjector } from './components/metaTagInjector'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ request, next }) => {
|
||||
const imageUri = new URL(request.url).origin + '/images/1200x630_Rich_Link_Preview_Image.png'
|
||||
const data = {
|
||||
title: 'Uniswap Interface',
|
||||
image: imageUri,
|
||||
url: request.url,
|
||||
description: 'Swap or provide liquidity on the Uniswap Protocol',
|
||||
}
|
||||
const res = next()
|
||||
try {
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(data, request)).transform(await res)
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
76
functions/api/image/nfts/asset/[[index]].tsx
Normal file
76
functions/api/image/nfts/asset/[[index]].tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist'
|
||||
import { WATERMARK_URL } from '../../../../constants'
|
||||
import getAsset from '../../../../utils/getAsset'
|
||||
import getFont from '../../../../utils/getFont'
|
||||
import { getRequest } from '../../../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
try {
|
||||
const origin = new URL(request.url).origin
|
||||
const { index } = params
|
||||
const collectionAddress = index[0]?.toString()
|
||||
const tokenId = index[1]?.toString()
|
||||
const cacheUrl = origin + '/nfts/asset/' + collectionAddress + '/' + tokenId
|
||||
|
||||
if (blocklistedCollections.includes(collectionAddress)) {
|
||||
return new Response('Collection unsupported.', { status: 404 })
|
||||
}
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getAsset(collectionAddress, tokenId, cacheUrl),
|
||||
(data): data is NonNullable<Awaited<ReturnType<typeof getAsset>>> => Boolean(data.ogImage)
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return new Response('Asset not found.', { status: 404 })
|
||||
}
|
||||
|
||||
const fontData = await getFont(origin)
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
width: '1200px',
|
||||
height: '630px',
|
||||
}}
|
||||
>
|
||||
<img src={data.ogImage} alt={data.title} width="1200px" />
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '72px',
|
||||
right: '72px',
|
||||
display: 'flex',
|
||||
gap: '24px',
|
||||
}}
|
||||
>
|
||||
<img src={WATERMARK_URL} alt="Uniswap" height="72px" width="324px" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
) as Response
|
||||
} catch (error: any) {
|
||||
return new Response(error.message || error.toString(), { status: 500 })
|
||||
}
|
||||
}
|
||||
29
functions/api/image/nfts/asset/nftImage.test.ts
Normal file
29
functions/api/image/nfts/asset/nftImage.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
const assetImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/804',
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947',
|
||||
]
|
||||
|
||||
test.each(assetImageUrl)('assetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('image/png')
|
||||
})
|
||||
|
||||
const invalidAssetImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/10001',
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/44700',
|
||||
]
|
||||
|
||||
test.each(invalidAssetImageUrl)('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
|
||||
const blockedAssetImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xd4d871419714b778ebec2e22c7c53572b573706e/276',
|
||||
]
|
||||
|
||||
test.each(blockedAssetImageUrl)('blockedAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
122
functions/api/image/nfts/collection/[index].tsx
Normal file
122
functions/api/image/nfts/collection/[index].tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist'
|
||||
import { getColor } from '../../../../../src/utils/getColor'
|
||||
import { CHECK_URL, WATERMARK_URL } from '../../../../constants'
|
||||
import getCollection from '../../../../utils/getCollection'
|
||||
import getFont from '../../../../utils/getFont'
|
||||
import { getRequest } from '../../../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
try {
|
||||
const origin = new URL(request.url).origin
|
||||
const { index } = params
|
||||
const collectionAddress = index?.toString()
|
||||
const cacheUrl = origin + '/nfts/collection/' + collectionAddress
|
||||
|
||||
if (blocklistedCollections.includes(collectionAddress)) {
|
||||
return new Response('Collection unsupported.', { status: 404 })
|
||||
}
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getCollection(collectionAddress, cacheUrl),
|
||||
(data): data is NonNullable<Awaited<ReturnType<typeof getCollection>>> =>
|
||||
Boolean(data.ogImage && data.name && data.isVerified)
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return new Response('Collection not found.', { status: 404 })
|
||||
}
|
||||
|
||||
const [fontData, palette] = await Promise.all([getFont(origin), getColor(data.ogImage)])
|
||||
|
||||
// Split name into words to wrap them since satori does not support inline text wrapping
|
||||
const words = data.name.split(' ')
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
display: 'flex',
|
||||
width: '1200px',
|
||||
height: '630px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: `rgba(${palette[0]}, ${palette[1]}, ${palette[2]}, 0.75)`,
|
||||
padding: '72px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
gap: '48px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={data.ogImage}
|
||||
alt={data.name}
|
||||
width="500px"
|
||||
height="500px"
|
||||
style={{
|
||||
borderRadius: '60px',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '32px',
|
||||
width: '45%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
gap: '12px',
|
||||
fontSize: '72px',
|
||||
fontFamily: 'Inter',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{words.map((word: string) => (
|
||||
<text key={word + index}>{word}</text>
|
||||
))}
|
||||
{data.isVerified && <img src={CHECK_URL} height="54px" />}
|
||||
</div>
|
||||
<img src={WATERMARK_URL} alt="Uniswap" height="72px" width="324px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
) as Response
|
||||
} catch (error: any) {
|
||||
return new Response(error.message || error.toString(), { status: 500 })
|
||||
}
|
||||
}
|
||||
34
functions/api/image/nfts/collection/collectionImage.test.ts
Normal file
34
functions/api/image/nfts/collection/collectionImage.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as matchers from 'jest-extended'
|
||||
expect.extend(matchers)
|
||||
|
||||
const collectionImageUrls = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
|
||||
]
|
||||
|
||||
test.each([...collectionImageUrls])('collectionImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('image/png')
|
||||
})
|
||||
|
||||
const nonexistentImageUrls = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
]
|
||||
|
||||
const invalidCollectionImageUrls = ['http://127.0.0.1:3000/api/image/nfts/collection/0xd3adb33f']
|
||||
|
||||
test.each([...invalidCollectionImageUrls, ...nonexistentImageUrls])('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBeOneOf([404, 500])
|
||||
})
|
||||
|
||||
const blockedCollectionImageUrls = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xd4d871419714b778ebec2e22c7c53572b573706e',
|
||||
]
|
||||
|
||||
test.each(blockedCollectionImageUrls)('blockedCollectionImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBeOneOf([404, 500])
|
||||
})
|
||||
177
functions/api/image/tokens/[[index]].tsx
Normal file
177
functions/api/image/tokens/[[index]].tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { getColor } from '../../../../src/utils/getColor'
|
||||
import { WATERMARK_URL } from '../../../constants'
|
||||
import getFont from '../../../utils/getFont'
|
||||
import getNetworkLogoUrl from '../../../utils/getNetworkLogoURL'
|
||||
import { getRequest } from '../../../utils/getRequest'
|
||||
import getToken from '../../../utils/getToken'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
try {
|
||||
const origin = new URL(request.url).origin
|
||||
const { index } = params
|
||||
const networkName = String(index[0])
|
||||
const tokenAddress = String(index[1])
|
||||
|
||||
const cacheUrl = origin + '/tokens/' + networkName + '/' + tokenAddress
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getToken(networkName, tokenAddress, cacheUrl),
|
||||
(data): data is NonNullable<Awaited<ReturnType<typeof getToken>>> => Boolean(data.symbol && data.name)
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return new Response('Token not found.', { status: 404 })
|
||||
}
|
||||
|
||||
const [fontData, palette] = await Promise.all([getFont(origin), getColor(data.ogImage, true)])
|
||||
|
||||
const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin)
|
||||
|
||||
// Capitalize name such that each word starts with a capital letter
|
||||
let words = data.name.split(' ')
|
||||
words = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
let name = words.join(' ')
|
||||
name = name.trim()
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
display: 'flex',
|
||||
width: '1200px',
|
||||
height: '630px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
backgroundColor: `rgba(${palette[0]}, ${palette[1]}, ${palette[2]})`,
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
padding: '72px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{data.ogImage ? (
|
||||
<img src={data.ogImage} width="144px" style={{ borderRadius: '100%' }}>
|
||||
{networkLogo != '' && (
|
||||
<img
|
||||
src={networkLogo}
|
||||
width="48px"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '2px',
|
||||
bottom: '0px',
|
||||
borderRadius: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</img>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: '144px',
|
||||
height: '144px',
|
||||
borderRadius: '100%',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.12)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '48px',
|
||||
lineHeight: '58px',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{data.name.slice(0, 3).toUpperCase()}
|
||||
</div>
|
||||
{networkLogo != '' && (
|
||||
<img
|
||||
src={networkLogo}
|
||||
width="48px"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '2px',
|
||||
bottom: '0px',
|
||||
borderRadius: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '72px',
|
||||
lineHeight: '72px',
|
||||
marginLeft: '-5px',
|
||||
marginTop: '24px',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '168px',
|
||||
lineHeight: '133px',
|
||||
marginLeft: '-13px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{data.symbol}
|
||||
</div>
|
||||
<img src={WATERMARK_URL} alt="Uniswap" height="72px" width="324px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
) as Response
|
||||
} catch (error: any) {
|
||||
return new Response(error.message || error.toString(), { status: 500 })
|
||||
}
|
||||
}
|
||||
22
functions/api/image/tokens/tokenImage.test.ts
Normal file
22
functions/api/image/tokens/tokenImage.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const tokenImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE',
|
||||
]
|
||||
|
||||
test.each(tokenImageUrl)('tokenImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('image/png')
|
||||
})
|
||||
|
||||
const invalidTokenImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb49',
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum',
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereun',
|
||||
'http://127.0.0.1:3000/api/image/tokens/potato/?potato=1',
|
||||
]
|
||||
|
||||
test.each(invalidTokenImageUrl)('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
1
functions/babel.config.js
Normal file
1
functions/babel.config.js
Normal file
@@ -0,0 +1 @@
|
||||
export const presets = ['@babel/preset-env']
|
||||
20
functions/client.ts
Normal file
20
functions/client.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client'
|
||||
const GRAPHQL_ENDPOINT = 'https://api.uniswap.org/v1/graphql'
|
||||
|
||||
//TODO: Figure out how to make ApolloClient global variable
|
||||
export default new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
uri: GRAPHQL_ENDPOINT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Origin: 'https://app.uniswap.org',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36',
|
||||
},
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
})
|
||||
60
functions/components/metaTagInjector.test.ts
Normal file
60
functions/components/metaTagInjector.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { MetaTagInjector } from './metaTagInjector'
|
||||
|
||||
test('should append meta tag to element', () => {
|
||||
const element = {
|
||||
append: jest.fn(),
|
||||
} as unknown as Element
|
||||
const property = 'property'
|
||||
const content = 'content'
|
||||
const injector = new MetaTagInjector(
|
||||
{
|
||||
title: 'test',
|
||||
url: 'testUrl',
|
||||
image: 'testImage',
|
||||
description: 'testDescription',
|
||||
},
|
||||
new Request('http://localhost')
|
||||
)
|
||||
injector.append(element, property, content)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="${property}" content="${content}"/>`, { html: true })
|
||||
|
||||
injector.element(element)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:title" content="test"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:description" content="testDescription"/>`, {
|
||||
html: true,
|
||||
})
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image" content="testImage"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image:width" content="1200"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image:height" content="630"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image:alt" content="test"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:type" content="website"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:url" content="testUrl"/>`, { html: true })
|
||||
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:card" content="summary_large_image"/>`, {
|
||||
html: true,
|
||||
})
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:title" content="test"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:image" content="testImage"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:image:alt" content="test"/>`, { html: true })
|
||||
|
||||
expect(element.append).toHaveBeenCalledTimes(13)
|
||||
})
|
||||
|
||||
test('should pass through header blocked paths', () => {
|
||||
const element = {
|
||||
append: jest.fn(),
|
||||
} as unknown as Element
|
||||
const request = new Request('http://localhost')
|
||||
request.headers.set('x-blocked-paths', '/')
|
||||
const injector = new MetaTagInjector(
|
||||
{
|
||||
title: 'test',
|
||||
url: 'testUrl',
|
||||
image: 'testImage',
|
||||
description: 'testDescription',
|
||||
},
|
||||
request
|
||||
)
|
||||
injector.element(element)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="x:blocked-paths" content="/"/>`, { html: true })
|
||||
})
|
||||
47
functions/components/metaTagInjector.ts
Normal file
47
functions/components/metaTagInjector.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
type MetaTagInjectorInput = {
|
||||
title: string
|
||||
image?: string
|
||||
url: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener class for Cloudflare's HTMLRewriter {@link https://developers.cloudflare.com/workers/runtime-apis/html-rewriter}
|
||||
* to inject meta tags into the <head> of an HTML document.
|
||||
*/
|
||||
export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
|
||||
constructor(private input: MetaTagInjectorInput, private request: Request) {}
|
||||
|
||||
append(element: Element, property: string, content: string) {
|
||||
element.append(`<meta property="${property}" content="${content}"/>`, { html: true })
|
||||
}
|
||||
|
||||
element(element: Element) {
|
||||
//Open Graph Tags
|
||||
this.append(element, 'og:title', this.input.title)
|
||||
if (this.input.description) {
|
||||
this.append(element, 'og:description', this.input.description)
|
||||
}
|
||||
if (this.input.image) {
|
||||
this.append(element, 'og:image', this.input.image)
|
||||
this.append(element, 'og:image:width', '1200')
|
||||
this.append(element, 'og:image:height', '630')
|
||||
this.append(element, 'og:image:alt', this.input.title)
|
||||
}
|
||||
this.append(element, 'og:type', 'website')
|
||||
this.append(element, 'og:url', this.input.url)
|
||||
|
||||
//Twitter Tags
|
||||
this.append(element, 'twitter:card', 'summary_large_image')
|
||||
this.append(element, 'twitter:title', this.input.title)
|
||||
if (this.input.image) {
|
||||
this.append(element, 'twitter:image', this.input.image)
|
||||
this.append(element, 'twitter:image:alt', this.input.title)
|
||||
}
|
||||
|
||||
const blockedPaths = this.request.headers.get('x-blocked-paths')
|
||||
if (blockedPaths) {
|
||||
this.append(element, 'x:blocked-paths', blockedPaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
2
functions/constants.ts
Normal file
2
functions/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const WATERMARK_URL = 'https://app.uniswap.org/images/324x74_App_Watermark.png'
|
||||
export const CHECK_URL = 'https://app.uniswap.org/images/54x54_Verified_Check.svg'
|
||||
22
functions/default.test.ts
Normal file
22
functions/default.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const defaultUrls = ['http://127.0.0.1:3000/', 'http://127.0.0.1:3000/swap', 'http://127.0.0.1:3000/pools']
|
||||
|
||||
test.each(defaultUrls)('should inject metadata for valid collections', async (defaultUrl) => {
|
||||
const body = await fetch(new Request(defaultUrl)).then((res) => res.text())
|
||||
expect(body).toContain(`<meta property="og:title" content="Uniswap Interface"/>`)
|
||||
expect(body).toContain(
|
||||
`<meta property="og:description" content="Swap or provide liquidity on the Uniswap Protocol"/>`
|
||||
)
|
||||
expect(body).toContain(
|
||||
`<meta property="og:image" content="http://127.0.0.1:3000/images/1200x630_Rich_Link_Preview_Image.png"/>`
|
||||
)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="Uniswap Interface"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="Uniswap Interface"/>`)
|
||||
expect(body).toContain(
|
||||
`<meta property="twitter:image" content="http://127.0.0.1:3000/images/1200x630_Rich_Link_Preview_Image.png"/>`
|
||||
)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="Uniswap Interface"/>`)
|
||||
})
|
||||
10
functions/jest.config.json
Normal file
10
functions/jest.config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"setupFilesAfterEnv": ["<rootDir>/setupAfterEnv.ts"],
|
||||
"preset": "ts-jest",
|
||||
"transform": {
|
||||
"'^.+\\.(ts|tsx)?$'": "ts-jest",
|
||||
"^.+\\.(js|jsx)$": "babel-jest"
|
||||
},
|
||||
"testTimeout": 360000,
|
||||
"cacheDirectory": "../node_modules/.cache/cloud-jest"
|
||||
}
|
||||
15
functions/nfts/asset/[[index]].ts
Normal file
15
functions/nfts/asset/[[index]].ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import getAsset from '../../utils/getAsset'
|
||||
import { getMetadataRequest } from '../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const collectionAddress = index[0]?.toString()
|
||||
const tokenId = index[1]?.toString()
|
||||
return getMetadataRequest(res, request, () => getAsset(collectionAddress, tokenId, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
445
functions/nfts/asset/__snapshots__/nft.test.ts.snap
Normal file
445
functions/nfts/asset/__snapshots__/nft.test.ts.snap
Normal file
@@ -0,0 +1,445 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for valid assets 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Azuki #2550"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki #2550"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki #2550"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="twitter:image:alt" content="Azuki #2550"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid assets 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club #3735"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club #3735"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club #3735"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club #3735"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid assets 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="CryptoPunk #3947"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CryptoPunk #3947"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CryptoPunk #3947"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="twitter:image:alt" content="CryptoPunk #3947"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
62
functions/nfts/asset/nft.test.ts
Normal file
62
functions/nfts/asset/nft.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
const assets = [
|
||||
{
|
||||
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
assetId: '2550',
|
||||
collectionName: 'Azuki',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550',
|
||||
},
|
||||
{
|
||||
address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
assetId: '3735',
|
||||
collectionName: 'Bored Ape Yacht Club',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735',
|
||||
},
|
||||
{
|
||||
address: '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb',
|
||||
assetId: '3947',
|
||||
collectionName: 'CryptoPunk',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947',
|
||||
},
|
||||
]
|
||||
|
||||
test.each(assets)('should inject metadata for valid assets', async (nft) => {
|
||||
const url = 'http://127.0.0.1:3000/nfts/asset/' + nft.address + '/' + nft.assetId
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).not.toContain(`<meta property="og:description"`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${nft.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${nft.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
})
|
||||
|
||||
const invalidAssets = [
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/100000',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/-1',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//2550',
|
||||
]
|
||||
|
||||
test.each(invalidAssets)('should not inject metadata for invalid asset calls', async (url) => {
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
})
|
||||
14
functions/nfts/collection/[index].ts
Normal file
14
functions/nfts/collection/[index].ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import getCollection from '../../utils/getCollection'
|
||||
import { getMetadataRequest } from '../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const collectionAddress = index?.toString()
|
||||
return getMetadataRequest(res, request, () => getCollection(collectionAddress, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
445
functions/nfts/collection/__snapshots__/collection.test.ts.snap
Normal file
445
functions/nfts/collection/__snapshots__/collection.test.ts.snap
Normal file
@@ -0,0 +1,445 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for collections 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Azuki on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="twitter:image:alt" content="Azuki on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for collections 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for collections 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="twitter:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
66
functions/nfts/collection/collection.test.ts
Normal file
66
functions/nfts/collection/collection.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
const collections = [
|
||||
{
|
||||
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
collectionName: 'Azuki',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
},
|
||||
{
|
||||
address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
collectionName: 'Bored Ape Yacht Club',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
},
|
||||
{
|
||||
address: '0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
|
||||
collectionName: 'CLONE X - X TAKASHI MURAKAMI',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
|
||||
},
|
||||
]
|
||||
|
||||
test.each([...collections])('should inject metadata for collections', async (collection) => {
|
||||
const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="${collection.collectionName} on Uniswap"/>`)
|
||||
expect(body).not.toContain(`<meta property="og:description"`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${collection.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="${collection.collectionName} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="${collection.collectionName} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${collection.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="${collection.collectionName} on Uniswap"/>`)
|
||||
})
|
||||
|
||||
const nonexistentCollections = [
|
||||
{
|
||||
address: '0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
},
|
||||
]
|
||||
|
||||
const invalidCollections = [
|
||||
{
|
||||
address: '0xd3adb33f',
|
||||
},
|
||||
]
|
||||
|
||||
test.each([...invalidCollections, ...nonexistentCollections])(
|
||||
'should not inject metadata for nonexistent collections',
|
||||
async (collection) => {
|
||||
const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
}
|
||||
)
|
||||
1
functions/setupAfterEnv.ts
Normal file
1
functions/setupAfterEnv.ts
Normal file
@@ -0,0 +1 @@
|
||||
jest.retryTimes(3)
|
||||
18
functions/tokens/[[index]].ts
Normal file
18
functions/tokens/[[index]].ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { getMetadataRequest } from '../utils/getRequest'
|
||||
import getToken from '../utils/getToken'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const networkName = index[0]?.toString()
|
||||
const tokenAddress = index[1]?.toString()
|
||||
if (!tokenAddress) {
|
||||
return res
|
||||
}
|
||||
return getMetadataRequest(res, request, () => getToken(networkName, tokenAddress, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
593
functions/tokens/__snapshots__/token.test.ts.snap
Normal file
593
functions/tokens/__snapshots__/token.test.ts.snap
Normal file
@@ -0,0 +1,593 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for valid tokens 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Get USDC on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get USDC on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get USDC on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"/><meta property="twitter:image:alt" content="Get USDC on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid tokens 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Get ETH on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get ETH on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/NATIVE"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get ETH on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE"/><meta property="twitter:image:alt" content="Get ETH on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid tokens 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Get MATIC on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/tokens/polygon/NATIVE"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get MATIC on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/polygon/NATIVE"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get MATIC on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/tokens/polygon/NATIVE"/><meta property="twitter:image:alt" content="Get MATIC on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid tokens 4`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Get PEPE on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get PEPE on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get PEPE on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933"/><meta property="twitter:image:alt" content="Get PEPE on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
68
functions/tokens/token.test.ts
Normal file
68
functions/tokens/token.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
const tokens = [
|
||||
{
|
||||
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
network: 'ethereum',
|
||||
symbol: 'USDC',
|
||||
image: 'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
},
|
||||
{
|
||||
address: 'NATIVE',
|
||||
network: 'ethereum',
|
||||
symbol: 'ETH',
|
||||
image: 'http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE',
|
||||
},
|
||||
{
|
||||
address: 'NATIVE',
|
||||
network: 'polygon',
|
||||
symbol: 'MATIC',
|
||||
image: 'http://127.0.0.1:3000/api/image/tokens/polygon/NATIVE',
|
||||
},
|
||||
{
|
||||
address: '0x6982508145454ce325ddbe47a25d4ec3d2311933',
|
||||
network: 'ethereum',
|
||||
symbol: 'PEPE',
|
||||
image: 'http://127.0.0.1:3000/api/image/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933',
|
||||
},
|
||||
]
|
||||
|
||||
test.each(tokens)('should inject metadata for valid tokens', async (token) => {
|
||||
const url = 'http://127.0.0.1:3000/tokens/' + token.network + '/' + token.address
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
expect(body).not.toContain(`<meta property="og:description"`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${token.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${token.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
})
|
||||
|
||||
const invalidTokens = [
|
||||
'http://127.0.0.1:3000/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb49',
|
||||
'http://127.0.0.1:3000/tokens/ethereum',
|
||||
'http://127.0.0.1:3000/tokens/ethereun',
|
||||
'http://127.0.0.1:3000/tokens/ethereum/0x0',
|
||||
'http://127.0.0.1:3000/tokens/ethereum//',
|
||||
'http://127.0.0.1:3000/tokens/potato/?potato=1',
|
||||
]
|
||||
|
||||
test.each(invalidTokens)('should not inject metadata for invalid tokens', async (url) => {
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
})
|
||||
13
functions/tsconfig.json
Normal file
13
functions/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "functions",
|
||||
"composite": false,
|
||||
"incremental": true,
|
||||
"isolatedModules": false,
|
||||
"jsx": "react",
|
||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/functions", // avoid clobbering the build tsbuildinfo
|
||||
"types": ["jest", "node", "@cloudflare/workers-types"],
|
||||
},
|
||||
"include": ["**/*.ts", ".ts", "**/*.tsx"],
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user