Compare commits
777 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2d5f85ce0 | |||
| 9ee5aa173f | |||
| f99f20fe18 | |||
| fb0108196c | |||
| 986cf07391 | |||
| 48eb3f0005 | |||
| a95c8fab5a | |||
| 1c131ee496 | |||
| 911af900ed | |||
|
|
fc7ecc7e3b | ||
|
|
4a5a41c59e | ||
|
|
4bec816e6c | ||
|
|
1d1b15f4ac | ||
|
|
1ffaf723de | ||
|
|
dd4b2dc764 | ||
|
|
5ded55e061 | ||
|
|
0f4ca592f2 | ||
|
|
90497dc08a | ||
|
|
2e618fb2aa | ||
|
|
0aa5727cdd | ||
|
|
79e74e1d13 | ||
|
|
52dc441e31 | ||
|
|
ff6d1cc510 | ||
|
|
76157c057e | ||
|
|
a1bd6f5eb4 | ||
|
|
f903eedc15 | ||
|
|
1feeaea181 | ||
|
|
7b10c94e4d | ||
|
|
f2f59d52cb | ||
|
|
a5034cb1c0 | ||
|
|
2227a38276 | ||
|
|
9f06747958 | ||
|
|
c6b44bb5c9 | ||
|
|
1d64d24d31 | ||
|
|
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 | ||
|
|
e2dd78fd0e | ||
|
|
96d6e00ed6 | ||
|
|
dd29c59238 | ||
|
|
8c2a0f1905 | ||
|
|
682fba219d | ||
|
|
0f5e871054 | ||
|
|
07527bab26 | ||
|
|
7934777fa2 | ||
|
|
2415a1e3cd | ||
|
|
1ba796a895 | ||
|
|
4446eb9b84 | ||
|
|
d23b6e5da6 | ||
|
|
44c355c7f0 | ||
|
|
e4a9764a12 | ||
|
|
303fa15240 | ||
|
|
d180aef306 | ||
|
|
c07c401189 | ||
|
|
65d91eb363 | ||
|
|
bd4042aa16 | ||
|
|
1dcafd2f2d | ||
|
|
66fcdb4465 | ||
|
|
e398e8b950 | ||
|
|
6424fdfbcd | ||
|
|
95814e3271 | ||
|
|
caa2524e27 | ||
|
|
d28a4b34cd | ||
|
|
f3a80c6272 | ||
|
|
b89ee36448 | ||
|
|
fbc55db937 | ||
|
|
835c62acfa | ||
|
|
8fe7c7a0a7 | ||
|
|
41113e6e41 | ||
|
|
58b25d29a9 | ||
|
|
a2db3e2719 | ||
|
|
b62f9066a7 | ||
|
|
258f22e037 | ||
|
|
38b306a80f | ||
|
|
9050f09bfe | ||
|
|
77d46c361a | ||
|
|
4fb48bdd1f | ||
|
|
cf2b6bf568 | ||
|
|
03095f4e48 | ||
|
|
b2966f8d29 | ||
|
|
ef6d1f20ed | ||
|
|
10b156ff2b | ||
|
|
146c5f29cf | ||
|
|
66a3475bf6 | ||
|
|
f6c393b016 | ||
|
|
15f8d34320 | ||
|
|
504e09d3dc | ||
|
|
1f755e8b0d | ||
|
|
f45a7f921b | ||
|
|
29db61ff90 | ||
|
|
8431ad9161 | ||
|
|
fd1aded517 | ||
|
|
27ad7cbd41 | ||
|
|
01e5de436a | ||
|
|
fd5aa1b51e | ||
|
|
a6e1a7e6d9 | ||
|
|
629fe2c144 | ||
|
|
d73763ce75 | ||
|
|
fe6df38997 | ||
|
|
719ee0f5b5 | ||
|
|
75bdf9a8d4 | ||
|
|
efbe3994bb | ||
|
|
93fe8e4349 | ||
|
|
6062f615a0 | ||
|
|
42e3af7b5c | ||
|
|
57274a800d | ||
|
|
5e591455b3 | ||
|
|
ec547ab100 | ||
|
|
9de76c69ae | ||
|
|
85d1b90197 | ||
|
|
38af86e1bb | ||
|
|
11a8df2a3e | ||
|
|
3726b6bb47 | ||
|
|
bfde34c774 | ||
|
|
bd8113d018 | ||
|
|
14e3ef044e | ||
|
|
4fc4bdcd55 | ||
|
|
3733570a89 | ||
|
|
7a042a5199 | ||
|
|
6d5e17a6e7 | ||
|
|
8301c5892c | ||
|
|
59b757dda0 | ||
|
|
92a6ec67b3 | ||
|
|
1d6a1e90d7 | ||
|
|
01aa3291b3 | ||
|
|
5539ebedf7 | ||
|
|
e6adddbf55 | ||
|
|
0050b1e165 | ||
|
|
5bf33ab004 | ||
|
|
a4cfeecd8c | ||
|
|
76cbfdd0b9 | ||
|
|
0db9e51e41 | ||
|
|
82e7925a17 | ||
|
|
2150347ba2 | ||
|
|
2f80646ddd | ||
|
|
55eea6a724 | ||
|
|
709a70652f | ||
|
|
5a7a041f12 | ||
|
|
b60d98fc17 | ||
|
|
38d9ab67eb | ||
|
|
5e6ef1575b | ||
|
|
4a015e9d0d | ||
|
|
c383a0a0a2 | ||
|
|
d6e92804ad | ||
|
|
f0d8f8b23b | ||
|
|
ff080aa957 | ||
|
|
04d9ff7d71 | ||
|
|
406893d99a | ||
|
|
4630720956 | ||
|
|
e73e1540ad | ||
|
|
3a82642fe6 | ||
|
|
2a50d6a17e | ||
|
|
c8a8149127 | ||
|
|
38cde648cf | ||
|
|
33c099119d | ||
|
|
40247ff7e0 | ||
|
|
30d1de8e84 | ||
|
|
0e5328bee9 | ||
|
|
d1995bc5a6 | ||
|
|
4959836c2a | ||
|
|
ef4a80852d | ||
|
|
c2a972eb75 | ||
|
|
47a2768d89 | ||
|
|
ca60caf6b0 | ||
|
|
252acef199 | ||
|
|
00ecb933ac | ||
|
|
607d0d443e | ||
|
|
ff0209a78f | ||
|
|
924e83139b | ||
|
|
4d5cc8267e | ||
|
|
6bc7cfc996 | ||
|
|
97312bb174 | ||
|
|
d0a10fcf8d | ||
|
|
7a1a476e45 | ||
|
|
b3bfc1003a | ||
|
|
3b1ef8033b | ||
|
|
803485b96a | ||
|
|
6df5d3a701 | ||
|
|
2d4eafc6b3 | ||
|
|
9b52fea58a | ||
|
|
b92b286626 | ||
|
|
a8268728d3 | ||
|
|
ab6debbf46 | ||
|
|
4c664645c6 | ||
|
|
4416a84fd7 | ||
|
|
eaaff81a67 | ||
|
|
f7b8f945f2 | ||
|
|
115c65500b | ||
|
|
3d8d29fd18 | ||
|
|
c178da626f | ||
|
|
e3c589ae41 | ||
|
|
6d60aca437 | ||
|
|
c77885c4c0 | ||
|
|
01684977b3 | ||
|
|
025a84de93 | ||
|
|
0025997175 | ||
|
|
acf97e042c | ||
|
|
771618b1c9 | ||
|
|
87144de9c8 | ||
|
|
63d0290a50 |
11
.env
11
.env
@ -1,13 +1,16 @@
|
|||||||
# These API keys are intentionally public. Please do not report them - thank you for your concern.
|
# These API keys are intentionally public. Please do not report them - thank you for your concern.
|
||||||
ESLINT_NO_DEV_ERRORS=true
|
ESLINT_NO_DEV_ERRORS=true
|
||||||
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
REACT_APP_AMPLITUDE_PROXY_URL="https://null.null"
|
||||||
REACT_APP_AWS_API_REGION="us-east-2"
|
REACT_APP_AWS_API_REGION="us-east-2"
|
||||||
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
|
REACT_APP_AWS_API_ENDPOINT="https://null.null"
|
||||||
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
|
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
|
||||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
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_API="https://api.moonpay.com"
|
||||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
|
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_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
|
||||||
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
|
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_STATSIG_PROXY_URL="https://null.null"
|
||||||
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
|
REACT_APP_TEMP_API_URL="https://null.null"
|
||||||
|
REACT_APP_UNISWAP_API_URL="https://null.null"
|
||||||
|
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
# These API keys are intentionally public. Please do not report them - thank you for your concern.
|
# These API keys are intentionally public. Please do not report them - thank you for your concern.
|
||||||
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
REACT_APP_AMPLITUDE_PROXY_URL="https://null.null"
|
||||||
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
|
REACT_APP_AWS_API_ENDPOINT="https://null.null"
|
||||||
REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd"
|
REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd"
|
||||||
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
||||||
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
||||||
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
|
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
|
||||||
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
||||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=production"
|
REACT_APP_MOONPAY_LINK="https://null.null"
|
||||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
||||||
REACT_APP_SENTRY_ENABLED=true
|
REACT_APP_SENTRY_ENABLED=true
|
||||||
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
|
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
|
||||||
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
|
REACT_APP_STATSIG_PROXY_URL="https://null.null"
|
||||||
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"
|
||||||
|
|||||||
52
.eslintrc.js
52
.eslintrc.js
@ -1,10 +1,22 @@
|
|||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
|
|
||||||
|
const { node: restrictedImports } = require('@uniswap/eslint-config/restrictedImports')
|
||||||
require('@uniswap/eslint-config/load')
|
require('@uniswap/eslint-config/load')
|
||||||
|
|
||||||
|
const rulesDirPlugin = require('eslint-plugin-rulesdir')
|
||||||
|
rulesDirPlugin.RULES_DIR = 'eslint_rules'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: '@uniswap/eslint-config/react',
|
extends: ['@uniswap/eslint-config/react'],
|
||||||
|
plugins: ['rulesdir'],
|
||||||
overrides: [
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*'],
|
||||||
|
rules: {
|
||||||
|
'multiline-comment-style': ['error', 'separate-lines'],
|
||||||
|
'rulesdir/no-undefined-or': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Configuration/typings typically export objects/definitions that are used outside of the transpiled package
|
// Configuration/typings typically export objects/definitions that are used outside of the transpiled package
|
||||||
// (eg not captured by the tsconfig). Because it's typical and not exceptional, this is turned off entirely.
|
// (eg not captured by the tsconfig). Because it's typical and not exceptional, this is turned off entirely.
|
||||||
@ -16,6 +28,20 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
rules: {
|
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': [
|
'import/no-restricted-paths': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
@ -44,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.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
|||||||
@uniswap/web-reviewers
|
|
||||||
22
.github/ISSUE_TEMPLATE/bug-report.md
vendored
22
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug Report
|
|
||||||
about: Describe an issue in the Uniswap Interface
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**Bug Description**
|
|
||||||
A clear and concise description of the bug.
|
|
||||||
|
|
||||||
**Steps to Reproduce**
|
|
||||||
|
|
||||||
1. Go to ...
|
|
||||||
2. Click on ...
|
|
||||||
...
|
|
||||||
|
|
||||||
**Expected Behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Additional Context**
|
|
||||||
Add any other context about the problem here (screenshots, whether the bug only occurs only in certain mobile/desktop/browser environments, etc.)
|
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +0,0 @@
|
|||||||
blank_issues_enabled: true
|
|
||||||
contact_links:
|
|
||||||
- name: Support
|
|
||||||
url: https://discord.gg/FCfyBSbCU5
|
|
||||||
about: Please ask and answer questions here
|
|
||||||
- name: List a token
|
|
||||||
url: https://github.com/Uniswap/default-token-list#adding-a-token
|
|
||||||
about: Any requests to add a token to Uniswap should go here
|
|
||||||
19
.github/ISSUE_TEMPLATE/feature-request.md
vendored
19
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature Request
|
|
||||||
about: Suggest an idea for improving the UX of the Uniswap Interface
|
|
||||||
title: ''
|
|
||||||
labels: 'improvement'
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
24
.github/actions/setup/action.yml
vendored
24
.github/actions/setup/action.yml
vendored
@ -1,24 +0,0 @@
|
|||||||
name: Setup
|
|
||||||
description: checkout repo, setup node, and install node_modules
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 14
|
|
||||||
registry-url: https://registry.npmjs.org
|
|
||||||
|
|
||||||
# node_modules/.cache is intentionally omitted, as this is used for build tool caches.
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
id: install-cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
node_modules
|
|
||||||
!node_modules/.cache
|
|
||||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn install --frozen-lockfile --ignore-scripts
|
|
||||||
shell: bash
|
|
||||||
13
.github/dependabot.yml
vendored
13
.github/dependabot.yml
vendored
@ -1,13 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: npm
|
|
||||||
# Files stored in repository root
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: 'daily'
|
|
||||||
allow:
|
|
||||||
- dependency-name: '@uniswap/default-token-list'
|
|
||||||
- dependency-name: '@uniswap/token-lists'
|
|
||||||
- dependency-name: '@uniswap/widgets'
|
|
||||||
reviewers:
|
|
||||||
- 'Uniswap/dependabot-reviewers'
|
|
||||||
45
.github/pull_request_template.md
vendored
45
.github/pull_request_template.md
vendored
@ -1,45 +0,0 @@
|
|||||||
<!-- Your PR title must follow conventional commits: https://github.com/Uniswap/interface#pr-title -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
<!-- Summary of change, including motivation and context. -->
|
|
||||||
<!-- Use verb-driven language: "Fixes XYZ" instead of "This change fixes XYZ" -->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Delete inapplicable lines: -->
|
|
||||||
_JIRA ticket:_
|
|
||||||
_Slack thread:_
|
|
||||||
_Relevant docs:_
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Delete this section if your change does not affect UI. -->
|
|
||||||
## Screen capture
|
|
||||||
|
|
||||||
| Before | After (Desktop) | After (Mobile) |
|
|
||||||
| ------------ |---------------- | -------------- |
|
|
||||||
| paste_before | past_after | paste_after |
|
|
||||||
|
|
||||||
|
|
||||||
## Test plan
|
|
||||||
|
|
||||||
<!-- Delete this section if your change is not a bug fix. -->
|
|
||||||
### Reproducing the error
|
|
||||||
|
|
||||||
<!-- Include steps to reproduce the bug. -->
|
|
||||||
1.
|
|
||||||
|
|
||||||
### QA (ie manual testing)
|
|
||||||
|
|
||||||
<!-- Include steps to test the change, ensuring no regression. -->
|
|
||||||
- [ ] N/A
|
|
||||||
|
|
||||||
|
|
||||||
#### Devices
|
|
||||||
<!-- If applicable, include different devices and screen sizes that may be affected, and how you've tested them. -->
|
|
||||||
|
|
||||||
|
|
||||||
### Automated testing
|
|
||||||
|
|
||||||
<!-- If N/A, check and note so it is obvious to your reviewers and does not show up as an incomplete task. -->
|
|
||||||
<!-- eg - [x] Unit test N/A -->
|
|
||||||
- [ ] Unit test
|
|
||||||
- [ ] Integration/E2E test
|
|
||||||
17
.github/workflows/check-pr-title.yaml
vendored
17
.github/workflows/check-pr-title.yaml
vendored
@ -1,17 +0,0 @@
|
|||||||
name: Check PR Title
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- edited
|
|
||||||
- synchronize
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Ensures that the PR title adheres to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
||||||
conventional-commit:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: amannn/action-semantic-pull-request@v3.4.0
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
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 }}
|
|
||||||
26
.github/workflows/crowdin.yaml
vendored
26
.github/workflows/crowdin.yaml
vendored
@ -1,26 +0,0 @@
|
|||||||
name: Crowdin Upload
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
upload-sources:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: ./.github/actions/setup
|
|
||||||
- run: yarn i18n:extract
|
|
||||||
|
|
||||||
- name: Upload Crowdin sources
|
|
||||||
uses: crowdin/github-action@1.1.0
|
|
||||||
with:
|
|
||||||
upload_sources: true
|
|
||||||
download_translations: false
|
|
||||||
project_id: 458284
|
|
||||||
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN_SECRET }}
|
|
||||||
source: 'src/locales/en-US.po'
|
|
||||||
translation: 'src/locales/%locale%.po'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
126
.github/workflows/release.yaml
vendored
126
.github/workflows/release.yaml
vendored
@ -1,126 +0,0 @@
|
|||||||
name: Release
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 12 * * 1-4' # every day 12: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 prepare
|
|
||||||
- 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@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
|
|
||||||
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'
|
|
||||||
174
.github/workflows/test.yml
vendored
174
.github/workflows/test.yml
vendored
@ -1,174 +0,0 @@
|
|||||||
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.
|
|
||||||
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
# manual trigger
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- 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') }}-
|
|
||||||
- run: yarn lint
|
|
||||||
|
|
||||||
typecheck:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- 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') }}-
|
|
||||||
- run: yarn prepare
|
|
||||||
- run: yarn typecheck
|
|
||||||
|
|
||||||
deps-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: ./.github/actions/setup
|
|
||||||
- run: yarn yarn-deduplicate --strategy=highest --list --fail
|
|
||||||
|
|
||||||
unit-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- 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') }}-
|
|
||||||
- run: yarn prepare
|
|
||||||
- run: yarn test --silent --maxWorkers=100%
|
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
fail_ci_if_error: false
|
|
||||||
verbose: true
|
|
||||||
flags: unit-tests
|
|
||||||
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: ./.github/actions/setup
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
id: build-cache
|
|
||||||
with:
|
|
||||||
path: node_modules/.cache
|
|
||||||
key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
|
||||||
restore-keys: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-
|
|
||||||
- run: yarn prepare
|
|
||||||
- run: yarn build
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: build
|
|
||||||
path: build
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build-e2e:
|
|
||||||
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 prepare
|
|
||||||
- run: yarn build:e2e
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: build-e2e
|
|
||||||
path: build
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
size-tests:
|
|
||||||
needs: [build]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: ./.github/actions/setup
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: build
|
|
||||||
path: build
|
|
||||||
- run: yarn test:size
|
|
||||||
|
|
||||||
|
|
||||||
cypress-test-matrix:
|
|
||||||
needs: [build-e2e]
|
|
||||||
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:
|
|
||||||
containers: [1, 2, 3, 4]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: ./.github/actions/setup
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
id: cypress-cache
|
|
||||||
with:
|
|
||||||
path: /root/.cache/Cypress
|
|
||||||
key: ${{ runner.os }}-cypress
|
|
||||||
- run: |
|
|
||||||
yarn cypress install
|
|
||||||
yarn cypress info
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: build-e2e
|
|
||||||
path: build
|
|
||||||
|
|
||||||
- uses: cypress-io/github-action@v4
|
|
||||||
with:
|
|
||||||
install: false
|
|
||||||
start: yarn serve
|
|
||||||
wait-on: 'http://localhost:3000'
|
|
||||||
browser: chrome
|
|
||||||
record: true
|
|
||||||
parallel: true
|
|
||||||
env:
|
|
||||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
fail_ci_if_error: false
|
|
||||||
verbose: true
|
|
||||||
flags: e2e-tests
|
|
||||||
|
|
||||||
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
|
|
||||||
cypress-tests:
|
|
||||||
if: ${{ always() }}
|
|
||||||
needs: [cypress-test-matrix]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: needs.cypress-test-matrix.result != 'success'
|
|
||||||
run: exit 1
|
|
||||||
14
.gitignore
vendored
14
.gitignore
vendored
@ -5,11 +5,12 @@
|
|||||||
/src/types/v3
|
/src/types/v3
|
||||||
/src/abis/types
|
/src/abis/types
|
||||||
/src/locales/**/*.js
|
/src/locales/**/*.js
|
||||||
/src/locales/**/en-US.po
|
/src/locales/**/*.po
|
||||||
/src/locales/**/pseudo.po
|
|
||||||
|
|
||||||
# generated graphql types
|
# generated files
|
||||||
/src/graphql/**/__generated__
|
/src/**/__generated__
|
||||||
|
|
||||||
|
# schema
|
||||||
schema.graphql
|
schema.graphql
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
@ -18,6 +19,8 @@ schema.graphql
|
|||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
/cache
|
/cache
|
||||||
|
/functions/coverage
|
||||||
|
/.swc
|
||||||
|
|
||||||
# builds
|
# builds
|
||||||
/build
|
/build
|
||||||
@ -45,7 +48,10 @@ notes.txt
|
|||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
cypress/downloads
|
||||||
cypress/videos
|
cypress/videos
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
|
||||||
.vercel
|
.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@ -12,5 +12,14 @@
|
|||||||
},
|
},
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"eslint.enable": true,
|
"eslint.enable": true,
|
||||||
"eslint.debug": true
|
"eslint.debug": true,
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,10 +69,10 @@ Other things to note:
|
|||||||
|
|
||||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
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>
|
- Swap on Uniswap V2: <https://app.uniswap.org/swap?use=v2>
|
||||||
- View V2 liquidity: <https://app.uniswap.org/#/pools/v2>
|
- View V2 liquidity: <https://app.uniswap.org/pools/v2>
|
||||||
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
|
- Add V2 liquidity: <https://app.uniswap.org/add/v2>
|
||||||
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>
|
- Migrate V2 liquidity to V3: <https://app.uniswap.org/migrate/v2>
|
||||||
|
|
||||||
## Accessing Uniswap V1
|
## 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/**/*"
|
- "**/instrumented/**/*"
|
||||||
- "**/styles/**/*"
|
- "**/styles/**/*"
|
||||||
- "styles/**/*"
|
- "styles/**/*"
|
||||||
|
- "**/styled.tsx"
|
||||||
- "**/constants/**/*"
|
- "**/constants/**/*"
|
||||||
- "constants/**/*"
|
- "constants/**/*"
|
||||||
|
- "src/dev/*"
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project: off
|
||||||
|
patch: off
|
||||||
|
|
||||||
flag_management:
|
flag_management:
|
||||||
default_rules:
|
default_rules:
|
||||||
@ -15,11 +22,22 @@ flag_management:
|
|||||||
- type: project
|
- type: project
|
||||||
target: auto
|
target: auto
|
||||||
threshold: 1%
|
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
|
if_ci_failed: error
|
||||||
- type: patch
|
- type: patch
|
||||||
target: 80%
|
target: 50%
|
||||||
individual_flags:
|
individual_flags:
|
||||||
- name: e2e-tests
|
- name: unit-tests
|
||||||
|
- name: cloud-tests
|
||||||
statuses:
|
statuses:
|
||||||
- type: patch
|
- type: project
|
||||||
target: 0%
|
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
|
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:
|
generates:
|
||||||
./src/graphql/thegraph/schema/schema.graphql:
|
./src/graphql/thegraph/schema/schema.graphql:
|
||||||
plugins:
|
plugins:
|
||||||
|
|||||||
170
craco.config.cjs
170
craco.config.cjs
@ -1,51 +1,40 @@
|
|||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
|
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
|
||||||
|
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
|
||||||
const { execSync } = require('child_process')
|
const { execSync } = require('child_process')
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
|
const path = require('path')
|
||||||
const { DefinePlugin, IgnorePlugin } = require('webpack')
|
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
||||||
|
const { IgnorePlugin, ProvidePlugin } = require('webpack')
|
||||||
|
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
|
||||||
|
|
||||||
const commitHash = execSync('git rev-parse HEAD').toString().trim()
|
const commitHash = execSync('git rev-parse HEAD').toString().trim()
|
||||||
const isProduction = process.env.NODE_ENV === 'production'
|
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.
|
// 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.
|
// Omit them from production builds, as they slow down the feedback loop.
|
||||||
const shouldLintOrTypeCheck = !isProduction
|
const shouldLintOrTypeCheck = !isProduction
|
||||||
|
|
||||||
|
function getCacheDirectory(cacheName) {
|
||||||
|
// Include the trailing slash to denote that this is a directory.
|
||||||
|
return `${path.join(__dirname, 'node_modules/.cache/', cacheName)}/`
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
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: {
|
eslint: {
|
||||||
enable: shouldLintOrTypeCheck,
|
enable: shouldLintOrTypeCheck,
|
||||||
pluginOptions(eslintConfig) {
|
pluginOptions(eslintConfig) {
|
||||||
return Object.assign(eslintConfig, {
|
return Object.assign(eslintConfig, {
|
||||||
cache: true,
|
cache: true,
|
||||||
cacheLocation: 'node_modules/.cache/eslint/',
|
cacheLocation: getCacheDirectory('eslint'),
|
||||||
ignorePath: '.gitignore',
|
ignorePath: '.gitignore',
|
||||||
|
// Use our own eslint/plugins/config, as overrides interfere with caching.
|
||||||
|
// This ensures that `yarn start` and `yarn lint` share one cache.
|
||||||
|
eslintPath: require.resolve('eslint'),
|
||||||
|
resolvePluginsRelativeTo: null,
|
||||||
|
baseConfig: null,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -55,38 +44,64 @@ module.exports = {
|
|||||||
jest: {
|
jest: {
|
||||||
configure(jestConfig) {
|
configure(jestConfig) {
|
||||||
return Object.assign(jestConfig, {
|
return Object.assign(jestConfig, {
|
||||||
cacheDirectory: 'node_modules/.cache/jest',
|
cacheDirectory: getCacheDirectory('jest'),
|
||||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
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',
|
||||||
|
'\\.(t|j)sx?$': '@swc/jest',
|
||||||
|
},
|
||||||
|
// Use d3-arrays's build directly, as jest does not support its exports.
|
||||||
|
transformIgnorePatterns: ['d3-array'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
'd3-array': 'd3-array/dist/d3-array.min.js',
|
||||||
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
webpack: {
|
webpack: {
|
||||||
plugins: [new VanillaExtractPlugin({ identifiers: 'short' })],
|
plugins: [
|
||||||
|
// Webpack 5 does not polyfill node globals, so we do so for those necessary:
|
||||||
|
new ProvidePlugin({
|
||||||
|
// - react-markdown requires process.cwd
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
],
|
||||||
configure: (webpackConfig) => {
|
configure: (webpackConfig) => {
|
||||||
|
// Configure webpack plugins:
|
||||||
webpackConfig.plugins = webpackConfig.plugins
|
webpackConfig.plugins = webpackConfig.plugins
|
||||||
.map((plugin) => {
|
.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.
|
// 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.
|
// See https://webpack.js.org/plugins/mini-css-extract-plugin/#remove-order-warnings.
|
||||||
if (plugin instanceof MiniCssExtractPlugin) {
|
if (plugin instanceof MiniCssExtractPlugin) {
|
||||||
plugin.options.ignoreOrder = true
|
plugin.options.ignoreOrder = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable TypeScript's config overwrite, as it interferes with incremental build caching.
|
||||||
|
// This ensures that `yarn start` and `yarn typecheck` share one cache.
|
||||||
|
if (plugin.constructor.name == 'ForkTsCheckerWebpackPlugin') {
|
||||||
|
delete plugin.options.typescript.configOverwrite
|
||||||
|
}
|
||||||
|
|
||||||
return plugin
|
return plugin
|
||||||
})
|
})
|
||||||
.filter((plugin) => {
|
.filter((plugin) => {
|
||||||
// Case sensitive paths are enforced by TypeScript.
|
// Case sensitive paths are already enforced by TypeScript.
|
||||||
// See https://www.typescriptlang.org/tsconfig#forceConsistentCasingInFileNames.
|
// See https://www.typescriptlang.org/tsconfig#forceConsistentCasingInFileNames.
|
||||||
if (plugin instanceof CaseSensitivePathsPlugin) return false
|
if (plugin instanceof CaseSensitivePathsPlugin) return false
|
||||||
|
|
||||||
@ -96,10 +111,71 @@ module.exports = {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// We're currently on Webpack 4.x which doesn't support the `exports` field in package.json.
|
// Configure webpack resolution:
|
||||||
// Instead, we need to manually map the import path to the correct exports path (eg dist or build folder).
|
webpackConfig.resolve = Object.assign(webpackConfig.resolve, {
|
||||||
// See https://github.com/webpack/webpack/issues/9509.
|
plugins: webpackConfig.resolve.plugins.map((plugin) => {
|
||||||
webpackConfig.resolve.alias['@uniswap/conedison'] = '@uniswap/conedison/dist'
|
// Allow vanilla-extract in production builds.
|
||||||
|
// This is necessary because create-react-app guards against external imports.
|
||||||
|
// See https://sandroroth.com/blog/vanilla-extract-cra#production-build.
|
||||||
|
if (plugin instanceof ModuleScopePlugin) {
|
||||||
|
plugin.allowedPaths.push(path.join(__dirname, 'node_modules/@vanilla-extract/webpack-plugin'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}),
|
||||||
|
// Webpack 5 does not resolve node modules, so we do so for those necessary:
|
||||||
|
fallback: {
|
||||||
|
// - react-markdown requires path
|
||||||
|
path: require.resolve('path-browserify'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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) => {
|
||||||
|
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,
|
||||||
|
isProduction
|
||||||
|
? {
|
||||||
|
splitChunks: {
|
||||||
|
// Cap the chunk size to 5MB.
|
||||||
|
// react-scripts suggests a chunk size under 1MB after gzip, but we can only measure maxSize before gzip.
|
||||||
|
// react-scripts also caps cacheable chunks at 5MB, which gzips to below 1MB, so we cap chunk size there.
|
||||||
|
// See https://github.com/facebook/create-react-app/blob/d960b9e/packages/react-scripts/config/webpack.config.js#L713-L716.
|
||||||
|
maxSize: 5 * 1024 * 1024,
|
||||||
|
// Optimize over all chunks, instead of async chunks (the default), so that initial chunks are also optimized.
|
||||||
|
chunks: 'all',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
return webpackConfig
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,25 +1,19 @@
|
|||||||
import codeCoverageTask from '@cypress/code-coverage/task'
|
|
||||||
import { defineConfig } from 'cypress'
|
import { defineConfig } from 'cypress'
|
||||||
import { setupHardhatEvents } from 'cypress-hardhat'
|
import { setupHardhatEvents } from 'cypress-hardhat'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
projectId: 'yp82ef',
|
projectId: 'yp82ef',
|
||||||
videoUploadOnPasses: false,
|
|
||||||
defaultCommandTimeout: 24000, // 2x average block time
|
defaultCommandTimeout: 24000, // 2x average block time
|
||||||
chromeWebSecurity: false,
|
chromeWebSecurity: false,
|
||||||
retries: { runMode: 2 },
|
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
|
||||||
|
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: {
|
e2e: {
|
||||||
async setupNodeEvents(on, config) {
|
async setupNodeEvents(on, config) {
|
||||||
await setupHardhatEvents(on, config)
|
await setupHardhatEvents(on, config)
|
||||||
codeCoverageTask(on, config)
|
return config
|
||||||
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'),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
baseUrl: 'http://localhost:3000',
|
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,41 +4,47 @@ import { aliasQuery, hasQuery } from '../utils/graphql-test-utils'
|
|||||||
|
|
||||||
describe('Add Liquidity', () => {
|
describe('Add Liquidity', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req) => {
|
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3?source=uniswap', (req) => {
|
||||||
aliasQuery(req, 'feeTierDistribution')
|
aliasQuery(req, 'feeTierDistribution')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('loads the two correct tokens', () => {
|
it('loads the token pair', () => {
|
||||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/500')
|
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500')
|
||||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
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.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', () => {
|
it('clears the token selection when chain changes', () => {
|
||||||
cy.visit('/add/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500')
|
||||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
|
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', 'ETH')
|
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||||
|
cy.get('[data-testid="chain-selector"]').last().click()
|
||||||
|
cy.contains('Polygon').click()
|
||||||
|
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||||
|
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('not.contain.text', 'UNI')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skip('token not in storage is loaded', () => {
|
it('does not crash if token is duplicated', () => {
|
||||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
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', 'UNI')
|
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'UNI')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skip('single token can be selected', () => {
|
it('single token can be selected', () => {
|
||||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f')
|
|
||||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
|
||||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
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.fixture('feeTierDistribution.json').then((feeTierDistribution) => {
|
||||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req: CyHttpMessages.IncomingHttpRequest) => {
|
cy.intercept(
|
||||||
if (hasQuery(req, 'FeeTierDistributionQuery')) {
|
'POST',
|
||||||
req.alias = 'FeeTierDistributionQuery'
|
'/subgraphs/name/uniswap/uniswap-v3?source=uniswap',
|
||||||
|
(req: CyHttpMessages.IncomingHttpRequest) => {
|
||||||
|
if (hasQuery(req, 'FeeTierDistribution')) {
|
||||||
|
req.alias = 'FeeTierDistribution'
|
||||||
|
|
||||||
req.reply({
|
req.reply({
|
||||||
body: {
|
body: {
|
||||||
@ -51,14 +57,60 @@ describe('Add Liquidity', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
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.wait('@FeeTierDistributionQuery')
|
// 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')
|
||||||
|
|
||||||
cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.3% fee tier')
|
// Repeat for Max Price step counter
|
||||||
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40%')
|
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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
28
cypress/e2e/buy-crypto-modal.test.ts
Normal file
28
cypress/e2e/buy-crypto-modal.test.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
|
describe('Buy Crypto Modal', () => {
|
||||||
|
it('should open and close', () => {
|
||||||
|
cy.visit('/')
|
||||||
|
|
||||||
|
// Open the fiat onramp modal
|
||||||
|
cy.get(getTestSelector('buy-fiat-button')).click()
|
||||||
|
cy.get(getTestSelector('fiat-onramp-modal')).should('be.visible')
|
||||||
|
|
||||||
|
// Click on a location that should be outside the modal, which should close it
|
||||||
|
cy.get('body').click(0, 100)
|
||||||
|
cy.get(getTestSelector('fiat-onramp-modal')).should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open and close, mobile viewport', () => {
|
||||||
|
cy.viewport('iphone-6')
|
||||||
|
cy.visit('/')
|
||||||
|
|
||||||
|
// Open the fiat onramp modal
|
||||||
|
cy.get(getTestSelector('buy-fiat-button')).click()
|
||||||
|
cy.get(getTestSelector('fiat-onramp-modal')).should('be.visible')
|
||||||
|
|
||||||
|
// Click on a location that should be outside the modal, which should close it
|
||||||
|
cy.get('body').click(10, 10)
|
||||||
|
cy.get(getTestSelector('fiat-onramp-modal')).should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,25 +1,27 @@
|
|||||||
import { getTestSelector } from '../utils'
|
import { getTestSelector } from '../utils'
|
||||||
|
import { CONNECTED_WALLET_USER_STATE, DISCONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||||
|
|
||||||
describe('Landing Page', () => {
|
describe('Landing Page', () => {
|
||||||
it('shows landing page when no selectedWallet', () => {
|
it('shows landing page when no user state exists', () => {
|
||||||
cy.visit('/', { noWallet: true })
|
cy.visit('/', { userState: DISCONNECTED_WALLET_USER_STATE })
|
||||||
cy.get(getTestSelector('landing-page'))
|
cy.get(getTestSelector('landing-page'))
|
||||||
cy.screenshot()
|
cy.screenshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('redirects to swap page when selectedWallet is INJECTED', () => {
|
it('redirects to swap page when a user has already connected a wallet', () => {
|
||||||
cy.visit('/', { selectedWallet: 'INJECTED' })
|
cy.visit('/', { userState: CONNECTED_WALLET_USER_STATE })
|
||||||
cy.get('#swap-page')
|
cy.get('#swap-page')
|
||||||
cy.url().should('include', '/swap')
|
cy.url().should('include', '/swap')
|
||||||
cy.screenshot()
|
cy.screenshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows landing page when selectedWallet is INJECTED and ?intro=true is in query', () => {
|
it('shows landing page when a user has already connected a wallet but ?intro=true is in query', () => {
|
||||||
cy.visit('/?intro=true', { selectedWallet: 'INJECTED' })
|
cy.visit('/?intro=true', { userState: CONNECTED_WALLET_USER_STATE })
|
||||||
cy.get(getTestSelector('landing-page'))
|
cy.get(getTestSelector('landing-page'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows landing page when the unicorn icon in nav is selected', () => {
|
it('shows landing page when the unicorn icon in nav is selected', () => {
|
||||||
|
cy.visit('/swap')
|
||||||
cy.get(getTestSelector('uniswap-logo')).click()
|
cy.get(getTestSelector('uniswap-logo')).click()
|
||||||
cy.get(getTestSelector('landing-page'))
|
cy.get(getTestSelector('landing-page'))
|
||||||
})
|
})
|
||||||
@ -37,4 +39,50 @@ describe('Landing Page', () => {
|
|||||||
cy.get(getTestSelector('pool-nav-link')).last().click()
|
cy.get(getTestSelector('pool-nav-link')).last().click()
|
||||||
cy.url().should('include', '/pools')
|
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', () => {
|
describe('Link', () => {
|
||||||
it('should update route', () => {
|
it('should update route', () => {
|
||||||
cy.viewport(2000, 1600)
|
cy.viewport(2000, 1600)
|
||||||
cy.visit('/')
|
cy.visit('/swap')
|
||||||
cy.contains('Pool').click()
|
cy.contains('Pool').click()
|
||||||
cy.get('[data-cy="join-pool-button"]').should('exist')
|
cy.get('[data-cy="join-pool-button"]').should('exist')
|
||||||
})
|
})
|
||||||
|
|||||||
131
cypress/e2e/mini-portfolio/accounts.test.ts
Normal file
131
cypress/e2e/mini-portfolio/accounts.test.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { getTestSelector } from '../../utils'
|
||||||
|
|
||||||
|
describe('Mini Portfolio account drawer', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
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('@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')
|
||||||
|
})
|
||||||
|
|
||||||
|
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('@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('@portfolioSpy').should('have.been.calledOnce')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches account information', () => {
|
||||||
|
// Open the mini portfolio
|
||||||
|
cy.intercept(/graphql/, { fixture: 'mini-portfolio/tokens.json' })
|
||||||
|
cy.get(getTestSelector('web3-status-connected')).click()
|
||||||
|
|
||||||
|
// 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', () => {
|
||||||
|
cy.hardhat().then((hardhat) => {
|
||||||
|
const accountA = hardhat.wallets[0].address
|
||||||
|
const accountB = hardhat.wallets[1].address
|
||||||
|
|
||||||
|
// Opens the account drawer
|
||||||
|
cy.get(getTestSelector('web3-status-connected')).click()
|
||||||
|
|
||||||
|
// A shortened version of the first account's address should be shown
|
||||||
|
cy.contains(accountA.slice(0, 6)).should('exist')
|
||||||
|
|
||||||
|
// Stores the current portfolio balance to later compare to next account's balance
|
||||||
|
cy.get(getTestSelector('portfolio-total-balance'))
|
||||||
|
.invoke('text')
|
||||||
|
.then((originalBalance) => {
|
||||||
|
// TODO(INFRA-3) Replace window.ethereum access below with cypress-hardhat utility
|
||||||
|
// Simulates the wallet changing accounts via eip-1193 event
|
||||||
|
cy.window().then((win) => win.ethereum.emit('accountsChanged', [accountB]))
|
||||||
|
|
||||||
|
// The second account's address should now be shown
|
||||||
|
cy.contains(accountB.slice(0, 6)).should('exist')
|
||||||
|
|
||||||
|
// The second account's portfolio balance should differ from the original balance
|
||||||
|
cy.get(getTestSelector('portfolio-total-balance')).should('not.have.text', originalBalance)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
114
cypress/e2e/mini-portfolio/activity-history.test.ts
Normal file
114
cypress/e2e/mini-portfolio/activity-history.test.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { USDC_MAINNET } from '../../../src/constants/tokens'
|
||||||
|
import { getTestSelector } from '../../utils'
|
||||||
|
|
||||||
|
describe('mini-portfolio activity history', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.hardhat()
|
||||||
|
.then((hardhat) => hardhat.wallet.getTransactionCount())
|
||||||
|
.then((nonce) => {
|
||||||
|
// Mock graphql response to include specific nonces.
|
||||||
|
cy.intercept(
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://beta.api.uniswap.org/v1/graphql',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
data: {
|
||||||
|
portfolios: [
|
||||||
|
{
|
||||||
|
id: 'UG9ydGZvbGlvOjB4NUNlYUI3NGU0NDZkQmQzYkY2OUUyNzcyMDBGMTI5ZDJiQzdBMzdhMQ==',
|
||||||
|
assetActivities: [
|
||||||
|
{
|
||||||
|
id: 'QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnME5tUm1PVGs0T0RrNVl6UmtNR1kzWTJNNE9HRTVNVFEzTURBME9EWmtOVGhrTURnNFpqbG1NelkxTnpRM1l6WXdZek15WVRFNE4yWXlaRFEwWVdVNFh6QjRZV1EyWXpCa05XTmlOVEZsWWpjMU5qUTFaRGszT1RneE4yRTJZVEkxTmpreU1UbG1ZbVE1Wmw4d2VEQXpOR0UwTURjMk5EUTROV1kzWlRBNFkyRXhOak0yTm1VMU1ETTBPVEZoTm1GbU56ZzFNR1E9',
|
||||||
|
timestamp: 1681150079,
|
||||||
|
type: 'UNKNOWN',
|
||||||
|
chain: 'ETHEREUM',
|
||||||
|
transaction: {
|
||||||
|
id: 'VHJhbnNhY3Rpb246MHg0NmRmOTk4ODk5YzRkMGY3Y2M4OGE5MTQ3MDA0ODZkNThkMDg4ZjlmMzY1NzQ3YzYwYzMyYTE4N2YyZDQ0YWU4XzB4YWQ2YzBkNWNiNTFlYjc1NjQ1ZDk3OTgxN2E2YTI1NjkyMTlmYmQ5Zl8weDAzNGE0MDc2NDQ4NWY3ZTA4Y2ExNjM2NmU1MDM0OTFhNmFmNzg1MGQ=',
|
||||||
|
blockNumber: 17019453,
|
||||||
|
hash: '0x46df998899c4d0f7cc88a914700486d58d088f9f365747c60c32a187f2d44ae8',
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
to: '0x034a40764485f7e08ca16366e503491a6af7850d',
|
||||||
|
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||||
|
nonce,
|
||||||
|
__typename: 'Transaction',
|
||||||
|
},
|
||||||
|
assetChanges: [],
|
||||||
|
__typename: 'AssetActivity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhneE16UXpaR1ppTlROaE9XRmpNR00yWW1aaVpqUTNNRFEyWWpObFkyRXhORGN3TUdZd00yWXhOMkV3WWpnM1pqWXpPRFpsWVRnNU16QTRNVFZtWmpoaFh6QjRZMkUzTXpOalkySm1OelZoTXpnME1ERXhPR1ZpT1RjNU9EVTJOemRpTkdRMk56TTBZemMwWmw4d2VERmlOVEUxTkdGaE5HSTRaakF5TjJJNVptUXhPVE0wTVRFek1tWmpPV1JoWlRFd1pqY3pOVGs9',
|
||||||
|
timestamp: 1681149995,
|
||||||
|
type: 'SEND',
|
||||||
|
chain: 'ETHEREUM',
|
||||||
|
transaction: {
|
||||||
|
id: 'VHJhbnNhY3Rpb246MHgxMzQzZGZiNTNhOWFjMGM2YmZiZjQ3MDQ2YjNlY2ExNDcwMGYwM2YxN2EwYjg3ZjYzODZlYTg5MzA4MTVmZjhhXzB4Y2E3MzNjY2JmNzVhMzg0MDExOGViOTc5ODU2NzdiNGQ2NzM0Yzc0Zl8weDFiNTE1NGFhNGI4ZjAyN2I5ZmQxOTM0MTEzMmZjOWRhZTEwZjczNTk=',
|
||||||
|
blockNumber: 17019446,
|
||||||
|
hash: '0x1343dfb53a9ac0c6bfbf47046b3eca14700f03f17a0b87f6386ea8930815ff8a',
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
to: '0x1b5154aa4b8f027b9fd19341132fc9dae10f7359',
|
||||||
|
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||||
|
nonce: nonce + 1,
|
||||||
|
__typename: 'Transaction',
|
||||||
|
},
|
||||||
|
assetChanges: [
|
||||||
|
{
|
||||||
|
__typename: 'TokenTransfer',
|
||||||
|
id: 'VG9rZW5UcmFuc2ZlcjoweDVjZWFiNzRlNDQ2ZGJkM2JmNjllMjc3MjAwZjEyOWQyYmM3YTM3YTFfMHhiMWRjNDlmMDY1N2FkNTA1YjUzNzUyN2RkOWE1MDk0YTM2NTkzMWMxXzB4MTM0M2RmYjUzYTlhYzBjNmJmYmY0NzA0NmIzZWNhMTQ3MDBmMDNmMTdhMGI4N2Y2Mzg2ZWE4OTMwODE1ZmY4YQ==',
|
||||||
|
asset: {
|
||||||
|
id: 'VG9rZW46RVRIRVJFVU1fMHgxY2MyYjA3MGNhZjAxNmE3ZGRjMzA0N2Y5MzI3MmU4Yzc3YzlkZGU5',
|
||||||
|
name: 'USD Coin (USDC)',
|
||||||
|
symbol: 'USDC',
|
||||||
|
address: '0x1cc2b070caf016a7ddc3047f93272e8c77c9dde9',
|
||||||
|
decimals: 6,
|
||||||
|
chain: 'ETHEREUM',
|
||||||
|
standard: null,
|
||||||
|
project: {
|
||||||
|
id: 'VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4MWNjMmIwNzBjYWYwMTZhN2RkYzMwNDdmOTMyNzJlOGM3N2M5ZGRlOQ==',
|
||||||
|
isSpam: true,
|
||||||
|
logo: null,
|
||||||
|
__typename: 'TokenProject',
|
||||||
|
},
|
||||||
|
__typename: 'Token',
|
||||||
|
},
|
||||||
|
tokenStandard: 'ERC20',
|
||||||
|
quantity: '18011.212084',
|
||||||
|
sender: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||||
|
recipient: '0xb1dc49f0657ad505b537527dd9a5094a365931c1',
|
||||||
|
direction: 'OUT',
|
||||||
|
transactedValue: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
__typename: 'AssetActivity',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
__typename: 'Portfolio',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).as('graphql')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should deduplicate activity history by nonce', () => {
|
||||||
|
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')
|
||||||
|
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.get(getTestSelector('confirmation-close-icon')).click()
|
||||||
|
|
||||||
|
// Check activity history tab.
|
||||||
|
cy.get(getTestSelector('web3-status-connected')).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'
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||||
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
|
||||||
|
|
||||||
describe('Testing nfts', () => {
|
describe('Testing nfts', () => {
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('/')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should load nft leaderboard', () => {
|
it('should load nft leaderboard', () => {
|
||||||
|
cy.visit('/')
|
||||||
cy.get(getTestSelector('nft-nav')).first().click()
|
cy.get(getTestSelector('nft-nav')).first().click()
|
||||||
cy.get(getTestSelector('nft-nav')).first().should('exist')
|
cy.get(getTestSelector('nft-nav')).first().should('exist')
|
||||||
cy.get(getTestSelector('nft-nav')).first().click()
|
cy.get(getTestSelector('nft-nav')).first().click()
|
||||||
@ -16,7 +12,7 @@ describe('Testing nfts', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should load pudgy penguin collection page', () => {
|
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-asset')).should('exist')
|
||||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
|
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
|
||||||
cy.get(getTestSelector('nft-filter')).first().click()
|
cy.get(getTestSelector('nft-filter')).first().click()
|
||||||
@ -24,13 +20,13 @@ describe('Testing nfts', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should be able to navigate to activity', () => {
|
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')).first().click()
|
||||||
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should go to the details page', () => {
|
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-filter')).first().click()
|
||||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||||
cy.get(getTestSelector('nft-collection-asset')).first().click()
|
cy.get(getTestSelector('nft-collection-asset')).first().click()
|
||||||
@ -41,7 +37,10 @@ describe('Testing nfts', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should toggle buy now on details page', () => {
|
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-text')).should('exist')
|
||||||
cy.get(getTestSelector('nft-details-description')).click()
|
cy.get(getTestSelector('nft-details-description')).click()
|
||||||
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
||||||
@ -49,15 +48,11 @@ describe('Testing nfts', () => {
|
|||||||
cy.get(getTestSelector('nft-bag')).should('exist')
|
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('web3-status-connected')).click()
|
||||||
cy.get(getTestSelector('nft-view-self-nfts')).click()
|
cy.get(getTestSelector('mini-portfolio-navbar')).contains('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-nft')).first().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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
277
cypress/e2e/permit2.test.ts
Normal file
277
cypress/e2e/permit2.test.ts
Normal file
@ -0,0 +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, USDT } from '../../src/constants/tokens'
|
||||||
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
|
/** Initiates a swap. */
|
||||||
|
function initiateSwap() {
|
||||||
|
// 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.contains('Confirm swap').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Permit2', () => {
|
||||||
|
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(inputToken: Token) {
|
||||||
|
// check token approval
|
||||||
|
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(inputToken: Token) {
|
||||||
|
cy.hardhat()
|
||||||
|
.then(({ approval, wallet }) => approval.getPermit2Allowance({ owner: wallet, token: inputToken }))
|
||||||
|
.then((allowance) => {
|
||||||
|
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 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
// 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) => {
|
||||||
|
// Reject token approval
|
||||||
|
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction').log(false)
|
||||||
|
tokenApprovalStub.rejects(USER_REJECTION) // rejects token approval
|
||||||
|
initiateSwap()
|
||||||
|
|
||||||
|
// Verify token approval rejection
|
||||||
|
cy.wrap(tokenApprovalStub).should('be.calledOnce')
|
||||||
|
cy.contains('Review swap')
|
||||||
|
|
||||||
|
// Allow token approval
|
||||||
|
cy.then(() => tokenApprovalStub.restore())
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// Verify token approval
|
||||||
|
cy.get(getTestSelector('popups')).contains('Approved')
|
||||||
|
expectTokenAllowanceForPermit2ToBeMax(DAI)
|
||||||
|
|
||||||
|
// Verify permit2 approval rejection
|
||||||
|
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
|
||||||
|
cy.contains('Review swap')
|
||||||
|
|
||||||
|
// Allow permit2 approval
|
||||||
|
cy.then(() => permitApprovalStub.restore())
|
||||||
|
cy.contains('Confirm swap').click()
|
||||||
|
|
||||||
|
// 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', () => {
|
||||||
|
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()
|
||||||
|
|
||||||
|
// Verify token approval
|
||||||
|
cy.get(getTestSelector('popups')).contains('Approved')
|
||||||
|
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||||
|
})
|
||||||
|
|
||||||
|
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', () => {
|
describe('Remove Liquidity', () => {
|
||||||
it('eth remove', () => {
|
it('loads the token pair in v2', () => {
|
||||||
cy.visit('/remove/v2/ETH/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
cy.visit(`/remove/v2/ETH/${UNI_MAINNET}`)
|
||||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('eth remove swap order', () => {
|
it('loads the token pair in v3', () => {
|
||||||
cy.visit('/remove/v2/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
|
cy.visit(`/remove/1`)
|
||||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'UNI')
|
cy.get('#remove-liquidity-tokens').should('contain.text', 'UNI/ETH')
|
||||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', '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', () => {
|
it('should redirect to error pages if pool does not exist', () => {
|
||||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
// Duplicate-token v2 pools redirect to position unavailable
|
||||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
cy.visit(`/remove/v2/ETH/ETH`)
|
||||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
cy.contains('Position unavailable')
|
||||||
})
|
|
||||||
|
|
||||||
it('does not crash if ETH is duplicated', () => {
|
// Single-token pools don't exist
|
||||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
cy.visit('/remove/v2/ETH')
|
||||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
cy.url().should('match', /\/not-found/)
|
||||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
|
|
||||||
|
// Nonexistent v3 pool
|
||||||
|
cy.visit(`/remove/${MaxUint256}`)
|
||||||
|
cy.contains('Position unavailable')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,7 +9,7 @@ describe('Service Worker', () => {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
'\n' +
|
'\n' +
|
||||||
'Service Worker tests must be run on a production-like build\n' +
|
'Service Worker tests must be run on a production-like build\n' +
|
||||||
'To test, build with `yarn build:e2e` and serve with `yarn serve`'
|
'To test, build with `yarn build` and serve with `yarn serve`'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -20,65 +20,78 @@ describe('Service Worker', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function unregister() {
|
function unregisterServiceWorker() {
|
||||||
return cy.log('unregister service worker').then(async () => {
|
return cy.log('unregisters service worker').then(async () => {
|
||||||
const cacheKeys = await window.caches.keys()
|
|
||||||
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
|
|
||||||
if (cacheKey) {
|
|
||||||
await window.caches.delete(cacheKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sw = await window.navigator.serviceWorker.getRegistration(Cypress.config().baseUrl ?? undefined)
|
const sw = await window.navigator.serviceWorker.getRegistration(Cypress.config().baseUrl ?? undefined)
|
||||||
await sw?.unregister()
|
await sw?.unregister()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
before(unregister)
|
before(unregisterServiceWorker)
|
||||||
after(unregister)
|
after(unregisterServiceWorker)
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept({ hostname: 'www.google-analytics.com' }, (req) => {
|
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||||
const body = req.body.toString()
|
const body = JSON.stringify(req.body)
|
||||||
if (req.query['ep.event_category'] === 'Service Worker' || body.includes('Service%20Worker')) {
|
const serviceWorkerStatus = body.match(/"service_worker":"(\w+)"/)?.[1]
|
||||||
if (req.query['en'] === 'Not Installed' || body.includes('Not%20Installed')) {
|
if (serviceWorkerStatus) {
|
||||||
req.alias = 'NotInstalled'
|
req.alias = `ServiceWorker:${serviceWorkerStatus}`
|
||||||
} else if (req.query['en'] === 'Cache Hit' || body.includes('Cache%20Hit')) {
|
|
||||||
req.alias = 'CacheHit'
|
|
||||||
} else if (req.query['en'] === 'Cache Miss' || body.includes('Cache%20Miss')) {
|
|
||||||
req.alias = 'CacheMiss'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('installs a ServiceWorker', () => {
|
it('installs a ServiceWorker and reports the uninstalled status to analytics', () => {
|
||||||
cy.visit('/', { serviceWorker: true })
|
cy.visit('/', { serviceWorker: true })
|
||||||
.get('#swap-page')
|
cy.wait('@ServiceWorker:uninstalled')
|
||||||
.wait('@NotInstalled', { timeout: 20000 })
|
cy.window().should(
|
||||||
.window({ timeout: 20000 })
|
'have.nested.property',
|
||||||
.and((win) => {
|
// The parent is checked instead of the AUT because it is on the same origin,
|
||||||
expect(win.navigator.serviceWorker.controller?.state).to.equal('activated')
|
// and the AUT will not be considered "activated" until the parent is idle.
|
||||||
|
'parent.navigator.serviceWorker.controller.state',
|
||||||
|
'activated'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('cache hit', () => {
|
||||||
|
it('reports the hit to analytics', () => {
|
||||||
|
cy.visit('/', { serviceWorker: true })
|
||||||
|
cy.wait('@ServiceWorker:hit')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('records a cache hit', () => {
|
describe('cache miss', () => {
|
||||||
cy.visit('/', { serviceWorker: true }).get('#swap-page').wait('@CacheHit', { timeout: 20000 })
|
let cache: Cache | undefined
|
||||||
})
|
let request: Request | undefined
|
||||||
|
let response: Response | undefined
|
||||||
it('records a cache miss', () => {
|
before(() => {
|
||||||
cy.then(async () => {
|
// Mocks the index.html in the cache to force a cache miss.
|
||||||
|
cy.visit('/', { serviceWorker: true }).then(async () => {
|
||||||
const cacheKeys = await window.caches.keys()
|
const cacheKeys = await window.caches.keys()
|
||||||
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
|
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
|
||||||
assert(cacheKey)
|
assert(cacheKey)
|
||||||
|
|
||||||
const cache = await window.caches.open(cacheKey)
|
cache = await window.caches.open(cacheKey)
|
||||||
const keys = await cache.keys()
|
const keys = await cache.keys()
|
||||||
const key = keys.find((key) => key.url.match(/index/))
|
request = keys.find((key) => key.url.match(/index/))
|
||||||
assert(key)
|
assert(request)
|
||||||
|
|
||||||
await cache.put(key, new Response())
|
response = await cache.match(request)
|
||||||
|
assert(response)
|
||||||
|
|
||||||
|
await cache.put(request, new Response())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
after(() => {
|
||||||
|
// Restores the index.html in the cache so that re-runs behave as expected.
|
||||||
|
// This is necessary because the Service Worker will not re-populate the cache.
|
||||||
|
cy.then(async () => {
|
||||||
|
if (cache && request && response) {
|
||||||
|
await cache.put(request, response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('reports the miss to analytics', () => {
|
||||||
|
cy.visit('/', { serviceWorker: true })
|
||||||
|
cy.wait('@ServiceWorker:miss')
|
||||||
})
|
})
|
||||||
.visit('/', { serviceWorker: true })
|
|
||||||
.get('#swap-page')
|
|
||||||
.wait('@CacheMiss', { timeout: 20000 })
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,108 +0,0 @@
|
|||||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
|
||||||
import { getClassContainsSelector, getTestSelector } from '../utils'
|
|
||||||
|
|
||||||
const UNI_GOERLI = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
|
|
||||||
const WETH_GOERLI = '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6'
|
|
||||||
|
|
||||||
describe('swap widget integration tests', () => {
|
|
||||||
const verifyInputToken = (inputText: string) => {
|
|
||||||
cy.get(getClassContainsSelector('TokenButtonRow')).first().contains(inputText)
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyOutputToken = (outputText: string) => {
|
|
||||||
cy.get(getClassContainsSelector('TokenButtonRow')).last().contains(outputText)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectOutputAndSwitch = (outputText: string) => {
|
|
||||||
// open token selector...
|
|
||||||
cy.contains('Select token').click()
|
|
||||||
// select token...
|
|
||||||
cy.contains(outputText).click({ force: true })
|
|
||||||
|
|
||||||
cy.get('body')
|
|
||||||
.then(($body) => {
|
|
||||||
if ($body.find(getTestSelector('TokenSafetyWrapper')).length) {
|
|
||||||
return 'I understand'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'You pay' // Just click on a random element as a no-op
|
|
||||||
})
|
|
||||||
.then((selector) => {
|
|
||||||
cy.contains(selector).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
// token selector should close...
|
|
||||||
cy.contains('Search name or paste address').should('not.exist')
|
|
||||||
|
|
||||||
cy.get(getClassContainsSelector('ReverseButton')).first().click()
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('widget on swap page', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.viewport(1200, 800)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have the correct default input/output and token selection should work', () => {
|
|
||||||
cy.visit('/swap', { featureFlags: [FeatureFlag.swapWidget] }).then(() => {
|
|
||||||
cy.wait('@eth_blockNumber')
|
|
||||||
verifyInputToken('ETH')
|
|
||||||
verifyOutputToken('Select token')
|
|
||||||
|
|
||||||
selectOutputAndSwitch('WETH')
|
|
||||||
|
|
||||||
verifyInputToken('WETH')
|
|
||||||
verifyOutputToken('ETH')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have the correct default input from URL params ', () => {
|
|
||||||
cy.visit(`/swap?inputCurrency=${WETH_GOERLI}`, {
|
|
||||||
featureFlags: [FeatureFlag.swapWidget],
|
|
||||||
}).then(() => {
|
|
||||||
cy.wait('@eth_blockNumber')
|
|
||||||
})
|
|
||||||
|
|
||||||
verifyInputToken('WETH')
|
|
||||||
verifyOutputToken('Select token')
|
|
||||||
|
|
||||||
selectOutputAndSwitch('Ether')
|
|
||||||
|
|
||||||
verifyInputToken('ETH')
|
|
||||||
verifyOutputToken('WETH')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have the correct default output from URL params ', () => {
|
|
||||||
cy.visit(`/swap?outputCurrency=${WETH_GOERLI}`, {
|
|
||||||
featureFlags: [FeatureFlag.swapWidget],
|
|
||||||
}).then(() => {
|
|
||||||
cy.wait('@eth_blockNumber')
|
|
||||||
})
|
|
||||||
|
|
||||||
verifyInputToken('Select token')
|
|
||||||
verifyOutputToken('WETH')
|
|
||||||
|
|
||||||
cy.get(getClassContainsSelector('ReverseButton')).first().click()
|
|
||||||
verifyInputToken('WETH')
|
|
||||||
verifyOutputToken('Select token')
|
|
||||||
|
|
||||||
selectOutputAndSwitch('Ether')
|
|
||||||
|
|
||||||
verifyInputToken('ETH')
|
|
||||||
verifyOutputToken('WETH')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('widget on Token Detail Page', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.viewport(1200, 800)
|
|
||||||
cy.visit(`/tokens/ethereum/${UNI_GOERLI}`, { featureFlags: [FeatureFlag.swapWidget] }).then(() => {
|
|
||||||
cy.wait('@eth_blockNumber')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have the expected output for a tokens detail page', () => {
|
|
||||||
verifyOutputToken('UNI')
|
|
||||||
cy.contains('Connect to Ethereum').should('exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
import { WETH_GOERLI } from '../fixtures/constants'
|
|
||||||
import { getTestSelector } from '../utils'
|
|
||||||
|
|
||||||
describe('Swap', () => {
|
|
||||||
const verifyAmount = (field: 'input' | 'output', amountText: string | null) => {
|
|
||||||
if (amountText === null) {
|
|
||||||
cy.get(`#swap-currency-${field} .token-amount-input`).should('not.have.value')
|
|
||||||
} else {
|
|
||||||
cy.get(`#swap-currency-${field} .token-amount-input`).should('have.value', amountText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyToken = (field: 'input' | 'output', tokenSymbol: string | null) => {
|
|
||||||
if (tokenSymbol === null) {
|
|
||||||
cy.get(`#swap-currency-${field} .token-symbol-container`).should('contain.text', 'Select token')
|
|
||||||
} else {
|
|
||||||
cy.get(`#swap-currency-${field} .token-symbol-container`).should('contain.text', tokenSymbol)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectOutput = (tokenSymbol: string) => {
|
|
||||||
// open token selector...
|
|
||||||
cy.contains('Select token').click()
|
|
||||||
// select token...
|
|
||||||
cy.contains(tokenSymbol).click()
|
|
||||||
|
|
||||||
cy.get('body')
|
|
||||||
.then(($body) => {
|
|
||||||
if ($body.find(getTestSelector('TokenSafetyWrapper')).length) {
|
|
||||||
return 'I understand'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'no-op' // Don't click on anything, a no-op
|
|
||||||
})
|
|
||||||
.then((content) => {
|
|
||||||
if (content !== 'no-op') {
|
|
||||||
cy.contains(content).click()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// token selector should close...
|
|
||||||
cy.contains('Search name or paste address').should('not.exist')
|
|
||||||
}
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
cy.visit('/swap')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('starts with ETH selected by default', () => {
|
|
||||||
verifyAmount('input', '')
|
|
||||||
verifyToken('input', 'ETH')
|
|
||||||
verifyAmount('output', null)
|
|
||||||
verifyToken('output', null)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can enter an amount into input', () => {
|
|
||||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('zero swap amount', () => {
|
|
||||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('invalid swap amount', () => {
|
|
||||||
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can enter an amount into output', () => {
|
|
||||||
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('zero output amount', () => {
|
|
||||||
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have the correct default input/output and token selection should work', () => {
|
|
||||||
cy.visit('/swap')
|
|
||||||
verifyToken('input', 'ETH')
|
|
||||||
verifyToken('output', null)
|
|
||||||
|
|
||||||
selectOutput('WETH')
|
|
||||||
cy.get(getTestSelector('swap-currency-button')).first().click()
|
|
||||||
|
|
||||||
verifyToken('input', 'WETH')
|
|
||||||
verifyToken('output', 'ETH')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have the correct default input from URL params ', () => {
|
|
||||||
cy.visit(`/swap?inputCurrency=${WETH_GOERLI}`)
|
|
||||||
|
|
||||||
verifyToken('input', 'WETH')
|
|
||||||
verifyToken('output', null)
|
|
||||||
|
|
||||||
selectOutput('Ether')
|
|
||||||
cy.get(getTestSelector('swap-currency-button')).first().click()
|
|
||||||
|
|
||||||
verifyToken('input', 'ETH')
|
|
||||||
verifyToken('output', 'WETH')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have the correct default output from URL params ', () => {
|
|
||||||
cy.visit(`/swap?outputCurrency=${WETH_GOERLI}`)
|
|
||||||
|
|
||||||
verifyToken('input', null)
|
|
||||||
verifyToken('output', 'WETH')
|
|
||||||
|
|
||||||
cy.get(getTestSelector('swap-currency-button')).first().click()
|
|
||||||
verifyToken('input', 'WETH')
|
|
||||||
verifyToken('output', null)
|
|
||||||
|
|
||||||
selectOutput('Ether')
|
|
||||||
cy.get(getTestSelector('swap-currency-button')).first().click()
|
|
||||||
|
|
||||||
verifyToken('input', 'ETH')
|
|
||||||
verifyToken('output', 'WETH')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
|
|
||||||
cy.visit('/swap')
|
|
||||||
selectOutput('WETH')
|
|
||||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
|
|
||||||
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Opens and closes the settings menu', () => {
|
|
||||||
cy.visit('/swap')
|
|
||||||
cy.contains('Settings').should('not.exist')
|
|
||||||
cy.get(getTestSelector('swap-settings-button')).click()
|
|
||||||
cy.contains('Slippage tolerance').should('exist')
|
|
||||||
cy.contains('Transaction deadline').should('exist')
|
|
||||||
cy.contains('Auto Router API').should('exist')
|
|
||||||
cy.contains('Expert Mode').should('exist')
|
|
||||||
cy.get(getTestSelector('swap-settings-button')).click()
|
|
||||||
cy.contains('Settings').should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('inputs reset when navigating between pages', () => {
|
|
||||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
|
|
||||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
|
||||||
cy.visit('/pool')
|
|
||||||
cy.visit('/swap')
|
|
||||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
|
||||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
135
cypress/e2e/swap/errors.test.ts
Normal file
135
cypress/e2e/swap/errors.test.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { BigNumber } from '@ethersproject/bignumber'
|
||||||
|
import { InterfaceSectionName } from '@uniswap/analytics-events'
|
||||||
|
import { CurrencyAmount } from '@uniswap/sdk-core'
|
||||||
|
|
||||||
|
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
|
||||||
|
import { DAI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||||
|
import { getBalance, getTestSelector } from '../../utils'
|
||||||
|
|
||||||
|
describe('Swap errors', () => {
|
||||||
|
it('wallet rejection', () => {
|
||||||
|
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'))
|
||||||
|
|
||||||
|
// 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.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}`)
|
||||||
|
cy.hardhat({ automine: false })
|
||||||
|
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||||
|
// 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.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()
|
||||||
|
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
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=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||||
|
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)
|
||||||
|
send.withArgs('eth_estimateGas').resolves(BigNumber.from(2_000_000))
|
||||||
|
send.callThrough()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set slippage to a very low value.
|
||||||
|
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||||
|
cy.get(getTestSelector('max-slippage-settings')).click()
|
||||||
|
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
|
||||||
|
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')
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
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('-')
|
||||||
|
})
|
||||||
|
})
|
||||||
146
cypress/e2e/swap/fees.test.ts
Normal file
146
cypress/e2e/swap/fees.test.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
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 }],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
34
cypress/e2e/swap/settings.test.ts
Normal file
34
cypress/e2e/swap/settings.test.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { getTestSelector } from '../../utils'
|
||||||
|
|
||||||
|
describe('Swap settings', () => {
|
||||||
|
it('Opens and closes the settings menu', () => {
|
||||||
|
cy.visit('/swap')
|
||||||
|
cy.contains('Settings').should('not.exist')
|
||||||
|
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||||
|
cy.get(getTestSelector('mobile-settings-menu')).should('not.exist')
|
||||||
|
cy.contains('Max. slippage').should('exist')
|
||||||
|
cy.contains('Transaction deadline').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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
98
cypress/e2e/swap/swap.test.ts
Normal file
98
cypress/e2e/swap/swap.test.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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[ChainId.MAINNET]
|
||||||
|
|
||||||
|
describe('Swap', () => {
|
||||||
|
describe('Swap on main page', () => {
|
||||||
|
it('starts with ETH selected by default', () => {
|
||||||
|
cy.visit('/swap')
|
||||||
|
cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '')
|
||||||
|
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should default inputs from URL params ', () => {
|
||||||
|
cy.visit(`/swap?inputCurrency=${UNI_MAINNET.address}`)
|
||||||
|
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'UNI')
|
||||||
|
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
|
||||||
|
|
||||||
|
cy.visit(`/swap?outputCurrency=${UNI_MAINNET.address}`)
|
||||||
|
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token')
|
||||||
|
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||||
|
|
||||||
|
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`)
|
||||||
|
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
|
||||||
|
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('inputs reset when navigating between pages', () => {
|
||||||
|
cy.visit('/swap')
|
||||||
|
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||||
|
cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01')
|
||||||
|
cy.visit('/pool').visit('/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')
|
||||||
|
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')).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.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()
|
||||||
|
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')
|
||||||
|
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||||
|
const finalBalance = initialBalance + 1
|
||||||
|
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
|
||||||
|
getBalance(USDC_MAINNET).should('eq', finalBalance)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
376
cypress/e2e/swap/uniswapx.test.ts
Normal file
376
cypress/e2e/swap/uniswapx.test.ts
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
import { ChainId, CurrencyAmount } from '@uniswap/sdk-core'
|
||||||
|
import { CyHttpMessages } from 'cypress/types/net-stubbing'
|
||||||
|
|
||||||
|
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}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 be visible
|
||||||
|
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can swap exact-in trades using uniswapX', () => {
|
||||||
|
// Setup a swap
|
||||||
|
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||||
|
cy.wait('@quote')
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
|
||||||
|
// 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}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
|
||||||
|
// 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}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can view UniswapX order status progress in activity', () => {
|
||||||
|
// Setup a swap
|
||||||
|
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
78
cypress/e2e/swap/wrap.test.ts
Normal file
78
cypress/e2e/swap/wrap.test.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { ChainId, CurrencyAmount, WETH9 } from '@uniswap/sdk-core'
|
||||||
|
|
||||||
|
import { getBalance, getTestSelector } from '../../utils'
|
||||||
|
|
||||||
|
const WETH = WETH9[ChainId.MAINNET]
|
||||||
|
|
||||||
|
describe('Swap wrap', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
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').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')
|
||||||
|
cy.get('#swap-currency-input .token-amount-input').should('have.value', '0.02')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to wrap ETH', () => {
|
||||||
|
getBalance(WETH).then((initialBalance) => {
|
||||||
|
cy.contains('Enter ETH amount')
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Submit transaction
|
||||||
|
cy.contains('Wrap').click()
|
||||||
|
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')
|
||||||
|
cy.get(getTestSelector('popups')).contains('Wrapped')
|
||||||
|
const finalBalance = initialBalance + 1
|
||||||
|
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
|
||||||
|
getBalance(WETH).should('equal', finalBalance)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to unwrap WETH', () => {
|
||||||
|
cy.hardhat().then(async (hardhat) => {
|
||||||
|
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(WETH, 1e18))
|
||||||
|
await hardhat.mine()
|
||||||
|
})
|
||||||
|
|
||||||
|
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').type('1').should('have.value', 1)
|
||||||
|
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
|
||||||
|
|
||||||
|
// Submit transaction
|
||||||
|
cy.contains('Unwrap').click()
|
||||||
|
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')
|
||||||
|
cy.get(getTestSelector('popups')).contains('Unwrapped')
|
||||||
|
const finalBalance = initialBalance - 1
|
||||||
|
cy.get('#swap-currency-input').contains(`Balance: ${finalBalance}`)
|
||||||
|
getBalance(WETH).should('equal', finalBalance)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,16 +1,23 @@
|
|||||||
import { getClassContainsSelector, getTestSelector } from '../utils'
|
import { ChainId, WETH9 } from '@uniswap/sdk-core'
|
||||||
|
import { FeatureFlag } from 'featureFlags'
|
||||||
|
|
||||||
|
import { ARB, UNI } from '../../src/constants/tokens'
|
||||||
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
|
const UNI_MAINNET = UNI[ChainId.MAINNET]
|
||||||
|
|
||||||
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
|
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
|
||||||
|
|
||||||
describe('Token details', () => {
|
describe('Token details', () => {
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/')
|
cy.viewport(1440, 900)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Uniswap token should have all information populated', () => {
|
it('Uniswap token should have all information populated', () => {
|
||||||
// Uniswap token
|
// 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
|
// Price chart should be filled in
|
||||||
cy.get('[data-cy="chart-header"]').should('include.text', '$')
|
cy.get('[data-cy="chart-header"]').should('include.text', '$')
|
||||||
cy.get('[data-cy="price-chart"]').should('exist')
|
cy.get('[data-cy="price-chart"]').should('exist')
|
||||||
@ -40,49 +47,52 @@ describe('Token details', () => {
|
|||||||
|
|
||||||
// Contract address should be displayed
|
// Contract address should be displayed
|
||||||
cy.contains(UNI_ADDRESS).should('exist')
|
cy.contains(UNI_ADDRESS).should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
// Swap widget should have this token pre-selected as the “destination” token
|
it('Uniswap token should have correct stats boxes if infoTDP flag on', () => {
|
||||||
cy.get(getTestSelector('token-select')).should('include.text', 'UNI')
|
// Uniswap token
|
||||||
|
cy.visit(`/tokens/ethereum/${UNI_ADDRESS}`, {
|
||||||
|
featureFlags: [{ name: FeatureFlag.infoTDP, value: true }],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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('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', () => {
|
it('token with warning and low trading volume should have all information populated', () => {
|
||||||
// Shiba predator token, low trading volume and also has warning modal
|
// Null token created for this test, 0 trading volume and has warning modal
|
||||||
cy.visit('/tokens/ethereum/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
cy.visit('/tokens/ethereum/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||||
|
|
||||||
// Should have missing price chart when price unavailable (expected for this token)
|
// Should have missing price chart when price unavailable (expected for this token)
|
||||||
if (cy.get('[data-cy="chart-header"]').contains('Price Unavailable')) {
|
if (cy.get('[data-cy="chart-header"]').contains('Price unavailable')) {
|
||||||
cy.get('[data-cy="missing-chart"]').should('exist')
|
cy.get('[data-cy="missing-chart"]').should('exist')
|
||||||
}
|
}
|
||||||
// Stats should have: TVL, 24H Volume, 52W low, 52W high
|
|
||||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
// Stats should not exist
|
||||||
cy.get(getTestSelector('token-details-stats')).within(() => {
|
cy.get(getTestSelector('token-details-stats')).should('not.exist')
|
||||||
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')
|
|
||||||
})
|
|
||||||
|
|
||||||
// About section should have description of token
|
// About section should have description of token
|
||||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
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.get('[data-cy="resources-container"]').within(() => {
|
||||||
cy.contains('Etherscan')
|
cy.contains('Etherscan')
|
||||||
.should('have.attr', 'href')
|
.should('have.attr', 'href')
|
||||||
.and('include', 'etherscan.io/address/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
.and('include', 'etherscan.io/address/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||||
cy.contains('More analytics')
|
cy.contains('More analytics')
|
||||||
.should('have.attr', 'href')
|
.should('have.attr', 'href')
|
||||||
.and('include', 'info.uniswap.org/#/tokens/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
.and('include', 'info.uniswap.org/#/tokens/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||||
cy.contains('Website').should('have.attr', 'href').and('include', 'qom')
|
|
||||||
cy.contains('Twitter').should('have.attr', 'href').and('include', 'twitter.com/ShibaPredator1')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Contract address should be displayed
|
// Contract address should be displayed
|
||||||
cy.contains('0xa71d0588EAf47f12B13cF8eC750430d21DF04974').should('exist')
|
cy.contains('0x1eFBB78C8b917f67986BcE54cE575069c0143681').should('exist')
|
||||||
|
|
||||||
// Swap widget should have this token pre-selected as the “destination” token
|
|
||||||
cy.get(getTestSelector('token-select')).should('include.text', 'QOM')
|
|
||||||
|
|
||||||
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
|
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
|
||||||
cy.get('[data-cy="token-safety-message"]')
|
cy.get('[data-cy="token-safety-message"]')
|
||||||
@ -90,27 +100,68 @@ describe('Token details', () => {
|
|||||||
.and('include.text', "This token isn't traded on leading U.S. centralized exchanges")
|
.and('include.text', "This token isn't traded on leading U.S. centralized exchanges")
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Swap on Token Detail Page', () => {
|
describe('swapping', () => {
|
||||||
const verifyOutputToken = (outputText: string) => {
|
|
||||||
cy.get(getClassContainsSelector('TokenButtonRow')).last().contains(outputText)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// On mobile widths, we just link back to /swap instead of rendering the swap component.
|
// On mobile widths, we just link back to /swap instead of rendering the swap component.
|
||||||
cy.viewport(1200, 800)
|
cy.viewport(1200, 800)
|
||||||
cy.visit(`/tokens/goerli/${UNI_ADDRESS}`).then(() => {
|
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`).then(() => {
|
||||||
cy.wait('@eth_blockNumber')
|
cy.wait('@eth_blockNumber')
|
||||||
|
cy.scrollTo('top')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have the expected output for a tokens detail page', () => {
|
it('should have the expected output for a tokens detail page', () => {
|
||||||
verifyOutputToken('UNI')
|
cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '')
|
||||||
|
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token')
|
||||||
|
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||||
|
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should automatically navigate to the new TDP', () => {
|
||||||
|
cy.get(`#swap-currency-output .open-currency-select-button`).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('should not share swap state with the main swap page', () => {
|
it('should not share swap state with the main swap page', () => {
|
||||||
verifyOutputToken('UNI')
|
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()
|
||||||
cy.visit('/swap')
|
cy.visit('/swap')
|
||||||
cy.contains('UNI').should('not.exist')
|
cy.contains('UNI').should('not.exist')
|
||||||
|
cy.contains('WETH').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can enter an amount into input', () => {
|
||||||
|
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('zero swap amount', () => {
|
||||||
|
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalid swap amount', () => {
|
||||||
|
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can enter an amount into output', () => {
|
||||||
|
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('zero output amount', () => {
|
||||||
|
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a L2 token even if the user is connected to a different network', () => {
|
||||||
|
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.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,82 +1,29 @@
|
|||||||
describe.skip('Token explore filter', () => {
|
describe('Token explore filter', () => {
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should filter correctly by uni search term', () => {
|
|
||||||
cy.visit('/tokens')
|
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"]')
|
function aliasFilteredTokens(filter: string) {
|
||||||
.clear()
|
cy.get('[data-cy="token-name"]').then((tokens) => {
|
||||||
.type('uni')
|
cy.wrap(Array.from(tokens).filter((token) => token.innerText.toLowerCase().includes(filter))).as('filteredTokens')
|
||||||
.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 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', () => {
|
it('should filter correctly by dao search term', () => {
|
||||||
cy.visit('/tokens')
|
aliasFilteredTokens('dao')
|
||||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
searchFor('dao')
|
||||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
|
||||||
const filteredByDao = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('dao'))
|
|
||||||
cy.wrap(filteredByDao).as('filteredByDao')
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
cy.get('@filteredTokens').then((filteredTokens) => {
|
||||||
.clear()
|
cy.get('[data-cy="token-name"]').then((tokens) => {
|
||||||
.type('dao')
|
cy.wrap(Array.from(tokens)).should('deep.equal', Array.from(filteredTokens))
|
||||||
.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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
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
|
// 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('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-NATIVE')).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-NATIVE')).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-NATIVE')).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-NATIVE')).find(getTestSelector('tvl-cell')).should('include.text', '$')
|
||||||
cy.get(getTestSelector('token-table-row-ETH'))
|
cy.get(getTestSelector('token-table-row-NATIVE'))
|
||||||
.find(getTestSelector('percent-change-cell'))
|
.find(getTestSelector('percent-change-cell'))
|
||||||
.should('include.text', '%')
|
.should('include.text', '%')
|
||||||
cy.get(getTestSelector('header-row')).find(getTestSelector('price-cell')).click()
|
cy.get(getTestSelector('header-row')).find(getTestSelector('price-cell')).click()
|
||||||
@ -24,24 +24,24 @@ describe('Token explore', () => {
|
|||||||
it('should update when time window toggled', () => {
|
it('should update when time window toggled', () => {
|
||||||
cy.visit('/tokens/ethereum')
|
cy.visit('/tokens/ethereum')
|
||||||
cy.get(getTestSelector('time-selector')).should('contain', '1D')
|
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'))
|
.find(getTestSelector('volume-cell'))
|
||||||
.then(function ($elem) {
|
.then(function ($elem) {
|
||||||
cy.wrap($elem.text()).as('dailyEthVol')
|
cy.wrap($elem.text()).as('dailyEthVol')
|
||||||
})
|
})
|
||||||
cy.get(getTestSelector('time-selector')).click()
|
cy.get(getTestSelector('time-selector')).click()
|
||||||
cy.get(getTestSelector('1Y')).click()
|
cy.get(getTestSelector('1Y')).click()
|
||||||
cy.get(getTestSelector('token-table-row-ETH'))
|
cy.get(getTestSelector('token-table-row-NATIVE'))
|
||||||
.find(getTestSelector('volume-cell'))
|
.find(getTestSelector('volume-cell'))
|
||||||
.then(function ($elem) {
|
.then(function ($elem) {
|
||||||
cy.wrap($elem.text()).as('yearlyEthVol')
|
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', () => {
|
it('should navigate to token detail page when row clicked', () => {
|
||||||
cy.visit('/tokens/ethereum')
|
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-about-section')).should('exist')
|
||||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||||
cy.get(getTestSelector('token-info-container')).should('exist')
|
cy.get(getTestSelector('token-info-container')).should('exist')
|
||||||
@ -53,13 +53,13 @@ describe('Token explore', () => {
|
|||||||
it('should update when global network changed', () => {
|
it('should update when global network changed', () => {
|
||||||
cy.visit('/tokens/ethereum')
|
cy.visit('/tokens/ethereum')
|
||||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', '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
|
// note: cannot switch global chain via UI because we cannot approve the network switch
|
||||||
// in metamask modal using plain cypress. this is a workaround.
|
// in metamask modal using plain cypress. this is a workaround.
|
||||||
cy.visit('/tokens/polygon')
|
cy.visit('/tokens/polygon')
|
||||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', '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', () => {
|
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-selected')).click()
|
||||||
cy.get(getTestSelector('tokens-network-filter-option-optimism')).click()
|
cy.get(getTestSelector('tokens-network-filter-option-optimism')).click()
|
||||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
||||||
cy.reload()
|
cy.get(getTestSelector('chain-selector-logo')).find('title').should('include.text', 'Ethereum logo')
|
||||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
|
||||||
cy.get(getTestSelector('chain-selector-logo')).invoke('attr', 'alt').should('eq', 'Ethereum')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,64 +1,72 @@
|
|||||||
|
import { ChainId } from '@uniswap/sdk-core'
|
||||||
|
import { UNI } from 'constants/tokens'
|
||||||
|
|
||||||
import { getTestSelector } from '../utils'
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
|
const UNI_ADDRESS = UNI[ChainId.MAINNET].address.toLowerCase()
|
||||||
|
|
||||||
describe('Universal search bar', () => {
|
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.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', () => {
|
function getSearchBar() {
|
||||||
// Search for uni token by name.
|
return cy.get('[data-cy="search-bar-input"]').last()
|
||||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
|
}
|
||||||
cy.get('[data-cy="searchbar-token-row-UNI"]')
|
|
||||||
|
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')
|
.should('contain.text', 'Uniswap')
|
||||||
.and('contain.text', 'UNI')
|
.and('contain.text', 'UNI')
|
||||||
.and('contain.text', '$')
|
.and('contain.text', '$')
|
||||||
.and('contain.text', '%')
|
.and('contain.text', '%')
|
||||||
cy.get('[data-cy="searchbar-token-row-UNI"]').click()
|
.click()
|
||||||
|
cy.location('pathname').should('equal', '/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984')
|
||||||
|
|
||||||
cy.get('div').contains('Uniswap').should('exist')
|
openSearch()
|
||||||
// Stats should have: TVL, 24H Volume, 52W low, 52W high.
|
cy.get(getTestSelector('searchbar-dropdown'))
|
||||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
.contains(getTestSelector('searchbar-dropdown'), 'Recent searches')
|
||||||
cy.get(getTestSelector('token-details-stats')).within(() => {
|
.find(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||||
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"]')
|
|
||||||
.should('exist')
|
.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', () => {
|
it(
|
||||||
// Search for mTSLA, which is a blocked token.
|
'should go to the selected result when recent results are shown',
|
||||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
|
// this test is experiencing flake despite being correct, i can see the right value in DOM
|
||||||
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
|
// 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,28 +1,10 @@
|
|||||||
|
import { FeatureFlag } from 'featureFlags'
|
||||||
|
|
||||||
import { getTestSelector } from '../utils'
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
function visit(darkMode: boolean) {
|
|
||||||
cy.visit('/swap', {
|
|
||||||
onBeforeLoad(win) {
|
|
||||||
cy.stub(win, 'matchMedia')
|
|
||||||
.withArgs('(prefers-color-scheme: dark)')
|
|
||||||
.returns({
|
|
||||||
matches: darkMode,
|
|
||||||
addEventListener() {
|
|
||||||
// do nothing
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Wallet Dropdown', () => {
|
describe('Wallet Dropdown', () => {
|
||||||
before(() => {
|
function itChangesTheme() {
|
||||||
cy.visit('/pools')
|
it('should change theme', () => {
|
||||||
})
|
|
||||||
|
|
||||||
it('should change the theme', () => {
|
|
||||||
cy.get(getTestSelector('web3-status-connected')).click()
|
|
||||||
cy.get(getTestSelector('wallet-settings')).click()
|
|
||||||
cy.get(getTestSelector('theme-lightmode')).click()
|
cy.get(getTestSelector('theme-lightmode')).click()
|
||||||
|
|
||||||
cy.get(getTestSelector('theme-lightmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
cy.get(getTestSelector('theme-lightmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||||
@ -39,58 +21,185 @@ describe('Wallet Dropdown', () => {
|
|||||||
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||||
cy.get(getTestSelector('theme-auto')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
cy.get(getTestSelector('theme-auto')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
it('should select a language', () => {
|
|
||||||
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
|
|
||||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
|
|
||||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
cy.location('search').should('match', /\?lng=en-US$/)
|
||||||
cy.get(getTestSelector('wallet-back')).click()
|
cy.contains('Uniswap available in: English').should('not.exist')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('connected', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get(getTestSelector('web3-status-connected')).click()
|
||||||
|
cy.get(getTestSelector('wallet-settings')).click()
|
||||||
|
})
|
||||||
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should change the theme when not connected', () => {
|
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-disconnect')).click()
|
||||||
cy.get(getTestSelector('wallet-settings')).click()
|
cy.get(getTestSelector('wallet-settings')).click()
|
||||||
cy.get(getTestSelector('theme-lightmode')).should('exist')
|
})
|
||||||
|
itChangesTheme()
|
||||||
|
itChangesLocale()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should select a language when not connected', () => {
|
describe('with color theme', () => {
|
||||||
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
|
function visitSwapWithColorTheme({ dark }: { dark: boolean }) {
|
||||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
|
cy.visit('/swap', {
|
||||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
onBeforeLoad(win) {
|
||||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
cy.stub(win, 'matchMedia')
|
||||||
cy.get(getTestSelector('wallet-back')).click()
|
.withArgs('(prefers-color-scheme: dark)')
|
||||||
|
.returns({
|
||||||
|
matches: dark,
|
||||||
|
addEventListener() {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
|
removeEventListener() {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
it('should properly use dark system theme when auto theme setting is selected', () => {
|
it('should properly use dark system theme when auto theme setting is selected', () => {
|
||||||
visit(true)
|
visitSwapWithColorTheme({ dark: true })
|
||||||
cy.get(getTestSelector('web3-status-connected')).click()
|
cy.get(getTestSelector('web3-status-connected')).click()
|
||||||
cy.get(getTestSelector('wallet-settings')).click()
|
cy.get(getTestSelector('wallet-settings')).click()
|
||||||
cy.get(getTestSelector('theme-auto')).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', () => {
|
it('should properly use light system theme when auto theme setting is selected', () => {
|
||||||
visit(false)
|
visitSwapWithColorTheme({ dark: false })
|
||||||
cy.get(getTestSelector('web3-status-connected')).click()
|
cy.get(getTestSelector('web3-status-connected')).click()
|
||||||
cy.get(getTestSelector('wallet-settings')).click()
|
cy.get(getTestSelector('wallet-settings')).click()
|
||||||
cy.get(getTestSelector('theme-auto')).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)')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mobile', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.viewport('iphone-6').visit('/')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
|
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
|
||||||
visit(false)
|
|
||||||
cy.viewport('iphone-6')
|
|
||||||
cy.get(getTestSelector('web3-status-connected')).click()
|
cy.get(getTestSelector('web3-status-connected')).click()
|
||||||
cy.get(getTestSelector('wallet-buy-crypto')).click()
|
cy.get(getTestSelector('wallet-buy-crypto')).click()
|
||||||
cy.contains('Buy crypto').should('not.be.visible')
|
cy.contains('Buy crypto').should('not.be.visible')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
|
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
|
||||||
visit(true)
|
|
||||||
cy.viewport('iphone-6')
|
|
||||||
cy.get(getTestSelector('web3-status-connected')).click()
|
cy.get(getTestSelector('web3-status-connected')).click()
|
||||||
cy.root().click(15, 40)
|
cy.root().click(15, 40)
|
||||||
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export const WETH_GOERLI = '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6'
|
|
||||||
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
91
cypress/support/commands.ts
Normal file
91
cypress/support/commands.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import 'cypress-hardhat/lib/browser'
|
||||||
|
|
||||||
|
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||||
|
|
||||||
|
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
|
||||||
|
namespace Cypress {
|
||||||
|
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<{ name: FeatureFlag; value: boolean }>
|
||||||
|
/**
|
||||||
|
* Initial user state.
|
||||||
|
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
|
||||||
|
*/
|
||||||
|
userState?: Partial<UserState>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
Cypress.Commands.overwrite(
|
||||||
|
'visit',
|
||||||
|
(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.')
|
||||||
|
|
||||||
|
return cy
|
||||||
|
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
|
||||||
|
.provider()
|
||||||
|
.then((provider) =>
|
||||||
|
original({
|
||||||
|
...options,
|
||||||
|
url,
|
||||||
|
onBeforeLoad(win) {
|
||||||
|
options?.onBeforeLoad?.(win)
|
||||||
|
|
||||||
|
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.name]: flag.value ? 'enabled' : 'control' }),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject the mock ethereum provider.
|
||||||
|
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,90 +5,17 @@
|
|||||||
// https://on.cypress.io/configuration
|
// https://on.cypress.io/configuration
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.ts using ES2015 syntax:
|
import './commands'
|
||||||
import '@cypress/code-coverage/support'
|
import './setupTests'
|
||||||
|
|
||||||
import assert from 'assert'
|
// Squelch logs from fetches, as they clutter the logs so much as to make them unusable.
|
||||||
|
// See https://docs.cypress.io/api/commands/intercept#Disabling-logs-for-a-request.
|
||||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
// TODO(https://github.com/cypress-io/cypress/issues/26069): Squelch only wildcard logs once Cypress allows it.
|
||||||
import { injected } from './ethereum'
|
const log = Cypress.log
|
||||||
|
Cypress.log = function (options, ...args) {
|
||||||
declare global {
|
if (options.displayName === 'script' || options.name === 'request') return
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
return log(options, ...args)
|
||||||
namespace Cypress {
|
} as typeof log
|
||||||
interface ApplicationWindow {
|
|
||||||
ethereum: typeof injected
|
|
||||||
}
|
|
||||||
interface VisitOptions {
|
|
||||||
serviceWorker?: true
|
|
||||||
featureFlags?: Array<FeatureFlag>
|
|
||||||
selectedWallet?: string
|
|
||||||
noWallet?: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
Cypress.Commands.overwrite(
|
|
||||||
'visit',
|
|
||||||
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
|
|
||||||
assert(typeof url === 'string')
|
|
||||||
|
|
||||||
cy.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 }).then(() => {
|
|
||||||
original({
|
|
||||||
...options,
|
|
||||||
url:
|
|
||||||
(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url) +
|
|
||||||
`${url.includes('?') ? '&' : '?'}chain=goerli`,
|
|
||||||
onBeforeLoad(win) {
|
|
||||||
options?.onBeforeLoad?.(win)
|
|
||||||
win.localStorage.clear()
|
|
||||||
|
|
||||||
const userState = {
|
|
||||||
selectedWallet: options?.noWallet !== true ? options?.selectedWallet || 'INJECTED' : undefined,
|
|
||||||
fiatOnrampDismissed: true,
|
|
||||||
}
|
|
||||||
win.localStorage.setItem('redux_localstorage_simple_user', JSON.stringify(userState))
|
|
||||||
|
|
||||||
if (options?.featureFlags) {
|
|
||||||
const featureFlags = options.featureFlags.reduce(
|
|
||||||
(flags, flag) => ({
|
|
||||||
...flags,
|
|
||||||
[flag]: 'enabled',
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
|
|
||||||
}
|
|
||||||
|
|
||||||
win.ethereum = injected
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Infura security policies are based on Origin headers.
|
|
||||||
// These are stripped by cypress because chromeWebSecurity === false; this adds them back in.
|
|
||||||
cy.intercept(/infura.io/, (res) => {
|
|
||||||
res.headers['origin'] = 'http://localhost:3000'
|
|
||||||
res.alias = res.body.method
|
|
||||||
res.continue()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Graphql security policies are based on Origin headers.
|
|
||||||
// These are stripped by cypress because chromeWebSecurity === false; this adds them back in.
|
|
||||||
cy.intercept('https://api.uniswap.org/v1/graphql', (res) => {
|
|
||||||
res.headers['origin'] = 'https://app.uniswap.org'
|
|
||||||
res.continue()
|
|
||||||
})
|
|
||||||
cy.intercept('https://beta.api.uniswap.org/v1/graphql', (res) => {
|
|
||||||
res.headers['origin'] = 'https://app.uniswap.org'
|
|
||||||
res.continue()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Cypress.on('uncaught:exception', () => {
|
Cypress.on('uncaught:exception', () => {
|
||||||
// returning false here prevents Cypress from failing the test
|
// returning false here prevents Cypress from failing the test
|
||||||
|
|||||||
@ -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)
|
|
||||||
66
cypress/support/setupTests.ts
Normal file
66
cypress/support/setupTests.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// @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.
|
||||||
|
cy.intercept('*', (req) => {
|
||||||
|
req.headers['referer'] = 'https://app.uniswap.org'
|
||||||
|
req.headers['origin'] = 'https://app.uniswap.org'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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 })
|
||||||
|
.intercept('https://gateway.ipfs.io/ipns/unsupportedtokens.uniswap.org', { statusCode: 404 })
|
||||||
|
|
||||||
|
// Reset hardhat between tests to ensure isolation.
|
||||||
|
// 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": {
|
"compilerOptions": {
|
||||||
"esModuleInterop": true,
|
"composite": false,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"isolatedModules": false,
|
||||||
"noEmit": true,
|
"noImplicitAny": false,
|
||||||
"strict": true,
|
"target": "ES6",
|
||||||
"target": "ES5",
|
|
||||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
|
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
|
||||||
"types": ["cypress", "node"]
|
"types": ["cypress", "node"],
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"],
|
|
||||||
"include": ["**/*.ts"],
|
"include": ["**/*.ts"],
|
||||||
"watchOptions": {
|
|
||||||
"excludeDirectories": ["node_modules"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
|
import { Currency } from '@uniswap/sdk-core'
|
||||||
|
|
||||||
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
|
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
|
||||||
|
|
||||||
export const getTestSelectorStartsWith = (selectorId: string) => `[data-testid^=${selectorId}]`
|
export const getTestSelectorStartsWith = (selectorId: string) => `[data-testid^=${selectorId}]`
|
||||||
|
|
||||||
export const getClassContainsSelector = (selectorId: string) => `[class*=${selectorId}]`
|
/** Gets the balance of a token as a Chainable. */
|
||||||
|
export function getBalance(token: Currency) {
|
||||||
|
return cy
|
||||||
|
.hardhat()
|
||||||
|
.then((hardhat) => hardhat.getBalance(hardhat.wallet, token))
|
||||||
|
.then((balance) => Number(balance.toFixed(1)))
|
||||||
|
}
|
||||||
|
|||||||
42
cypress/utils/user-state.ts
Normal file
42
cypress/utils/user-state.ts
Normal file
@ -0,0 +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: 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
44
eslint_rules/no-undefined-or.js
Normal file
44
eslint_rules/no-undefined-or.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description: 'Enforce the use of optional object fields',
|
||||||
|
category: 'Best Practices',
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
fixable: 'code',
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
'TSPropertySignature > TSTypeAnnotation > TSUnionType': (node) => {
|
||||||
|
const types = node.types
|
||||||
|
const hasUndefined = types.some((typeNode) => typeNode.type === 'TSUndefinedKeyword')
|
||||||
|
|
||||||
|
if (hasUndefined) {
|
||||||
|
const typesWithoutUndefined = types.filter((typeNode) => typeNode.type !== 'TSUndefinedKeyword')
|
||||||
|
|
||||||
|
// If there is more than one type left after removing 'undefined',
|
||||||
|
// join them together with ' | ' to create a new union type.
|
||||||
|
const newTypeSource =
|
||||||
|
typesWithoutUndefined.length > 1
|
||||||
|
? typesWithoutUndefined.map((typeNode) => context.getSourceCode().getText(typeNode)).join(' | ')
|
||||||
|
: context.getSourceCode().getText(typesWithoutUndefined[0])
|
||||||
|
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: `Prefer optional properties to "Type | undefined".`,
|
||||||
|
fix(fixer) {
|
||||||
|
const propertySignature = node.parent.parent
|
||||||
|
const isAlreadyOptional = propertySignature.optional
|
||||||
|
const newTypeAnnotation = isAlreadyOptional ? `: ${newTypeSource}` : `?: ${newTypeSource}`
|
||||||
|
return fixer.replaceText(node.parent, newTypeAnnotation)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
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>
|
||||||
|
"
|
||||||
|
`;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user