Compare commits
614 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 |
11
.env
11
.env
@@ -1,13 +1,16 @@
|
||||
# These API keys are intentionally public. Please do not report them - thank you for your concern.
|
||||
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_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_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://magical-alien-tab.quiknode.pro/669e87e569a8277d3fbd9e202f9df93189f19f4c"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
|
||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
|
||||
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
|
||||
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
|
||||
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
|
||||
REACT_APP_STATSIG_PROXY_URL="https://null.null"
|
||||
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.
|
||||
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
|
||||
REACT_APP_AMPLITUDE_PROXY_URL="https://null.null"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://null.null"
|
||||
REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd"
|
||||
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
||||
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
||||
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
|
||||
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
||||
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_SENTRY_ENABLED=true
|
||||
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
|
||||
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
|
||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
||||
REACT_APP_STATSIG_PROXY_URL="https://null.null"
|
||||
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://ultra-blue-flower.quiknode.pro/770b22d5f362c537bc8fe19b034c45b22958f880"
|
||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap"
|
||||
|
||||
40
.eslintrc.js
40
.eslintrc.js
@@ -1,5 +1,6 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const { node: restrictedImports } = require('@uniswap/eslint-config/restrictedImports')
|
||||
require('@uniswap/eslint-config/load')
|
||||
|
||||
const rulesDirPlugin = require('eslint-plugin-rulesdir')
|
||||
@@ -13,7 +14,6 @@ module.exports = {
|
||||
files: ['**/*'],
|
||||
rules: {
|
||||
'multiline-comment-style': ['error', 'separate-lines'],
|
||||
'rulesdir/enforce-retry-on-import': 'error',
|
||||
'rulesdir/no-undefined-or': 'error',
|
||||
},
|
||||
},
|
||||
@@ -28,6 +28,20 @@ module.exports = {
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
...restrictedImports,
|
||||
paths: [
|
||||
...restrictedImports.paths,
|
||||
{
|
||||
name: '@uniswap/smart-order-router',
|
||||
message: 'Only import types, unless you are in the client-side SOR, to preserve lazy-loading.',
|
||||
allowTypeImports: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'import/no-restricted-paths': [
|
||||
'error',
|
||||
{
|
||||
@@ -56,6 +70,30 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: ':matches(ExportAllDeclaration)',
|
||||
message: 'Barrel exports bloat the bundle size by preventing tree-shaking.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
excludedFiles: ['src/analytics/*'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@uniswap/analytics',
|
||||
message: `Do not import from '@uniswap/analytics' directly. Use 'analytics' instead.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
@uniswap/web-admins
|
||||
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.
|
||||
48
.github/actions/report/action.yml
vendored
48
.github/actions/report/action.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Report
|
||||
description: Report test failures via Slack
|
||||
inputs:
|
||||
name:
|
||||
description: The name of the failing test
|
||||
required: true
|
||||
SLACK_WEBHOOK_URL:
|
||||
description: The webhook URL to send the report to
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "${{ inputs.name }} failing on `${{ github.ref_name }}`",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*${{ inputs.name }} failing on `${{ github.ref_name }}`:* <https://github.com/${{ github.repository}}/actions/runs/${{ github.run_id }}|view failing action>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "_This is blocking pull requests and branch promotions._\n_Please prioritize fixing the build._"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
# The !oncall bot requires its own message:
|
||||
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "!oncall web"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
75
.github/actions/setup/action.yml
vendored
75
.github/actions/setup/action.yml
vendored
@@ -1,75 +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
|
||||
cache: 'yarn'
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
# node_modules/.cache is intentionally omitted, as this is used for build tool caches.
|
||||
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
|
||||
|
||||
# Validators compile quickly, so caching can be omitted.
|
||||
- run: yarn ajv
|
||||
shell: bash
|
||||
|
||||
# Contracts are compiled from source. If source hasn't changed, the contracts do not need to be re-compiled.
|
||||
- uses: actions/cache@v3
|
||||
id: contracts-cache
|
||||
with:
|
||||
path: |
|
||||
src/abis/types
|
||||
src/types/v3
|
||||
key: ${{ runner.os }}-contracts-${{ hashFiles('src/abis/**/*.json', 'node_modules/@uniswap/**/artifacts/contracts/**/*.json') }}
|
||||
- if: steps.contracts-cache.outputs.cache-hit != 'true'
|
||||
run: yarn contracts
|
||||
shell: bash
|
||||
|
||||
# GraphQL is generated from schema. The schema is always fetched, but if unchanged, graphql does not need to be re-generated.
|
||||
- run: yarn graphql:fetch
|
||||
shell: bash
|
||||
- uses: actions/cache@v3
|
||||
id: graphql-cache
|
||||
with:
|
||||
path: src/graphql/**/__generated__
|
||||
key: ${{ runner.os }}-graphql-${{ hashFiles('src/graphql/**/schema.graphql') }}
|
||||
- if: steps.graphql-cache.outputs.cache-hit != 'true'
|
||||
run: yarn graphql:generate
|
||||
shell: bash
|
||||
|
||||
# Messages are extracted from source.
|
||||
# A record of source file content hashes and catalogs is maintained in node_modules/.cache/lingui.
|
||||
# Messages are always extracted, but extraction may short-circuit from the custom extractor's cache.
|
||||
- uses: actions/cache@v3
|
||||
id: i18n-extract-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-i18n-extract-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-i18n-extract-
|
||||
- run: yarn i18n:extract
|
||||
shell: bash
|
||||
|
||||
# Translations are compiled from messages. If messages haven't changed, the translations do not need to be re-compiled.
|
||||
- uses: actions/cache@v3
|
||||
id: i18n-compile-cache
|
||||
with:
|
||||
path: src/locales/*.js
|
||||
key: ${{ runner.os }}-i18n-compile-${{ hashFiles('src/locales/*.po') }}
|
||||
- if: steps.i18n-compile-cache.outputs.cache-hit !='true'
|
||||
run: yarn i18n:compile
|
||||
shell: bash
|
||||
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -1,12 +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'
|
||||
reviewers:
|
||||
- 'Uniswap/dependabot-reviewers'
|
||||
52
.github/pull_request_template.md
vendored
52
.github/pull_request_template.md
vendored
@@ -1,52 +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: -->
|
||||
_Linear ticket:_
|
||||
_Slack thread:_
|
||||
_Relevant docs:_
|
||||
|
||||
|
||||
<!-- Delete this section if your change does not affect UI. -->
|
||||
## Screen capture
|
||||
|
||||
### Before
|
||||
| Mobile | Desktop |
|
||||
| ------------ | ------------ |
|
||||
| paste_before | paste_before |
|
||||
|
||||
|
||||
### After
|
||||
| Mobile | Desktop |
|
||||
| ------------ | ----------- |
|
||||
| paste_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
|
||||
32
.github/workflows/1-main-to-staging.yml
vendored
32
.github/workflows/1-main-to-staging.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: 1 | Push main -> staging
|
||||
|
||||
# This CI job is responsible for pushing the current contents of the `main` branch to the
|
||||
# `releases/staging` branch, which will in turn kick off a deploy to the staging environment.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
# https://stackoverflow.com/questions/57921401/push-to-origin-from-github-action
|
||||
jobs:
|
||||
push-staging:
|
||||
name: 'Push to staging branch'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: push/staging
|
||||
steps:
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
|
||||
ref: main
|
||||
- name: Git config
|
||||
run: |
|
||||
git config user.name "UL Service Account"
|
||||
git config user.email "hello-happy-puppy@users.noreply.github.com"
|
||||
- name: Add CODEOWNERS file
|
||||
run: |
|
||||
echo "@uniswap/web-admins" > CODEOWNERS
|
||||
git add CODEOWNERS
|
||||
git commit -m "ci: add global CODEOWNERS"
|
||||
- name: Git push
|
||||
run: |
|
||||
git push origin main:releases/staging --force
|
||||
67
.github/workflows/2-deploy-to-staging.yml
vendored
67
.github/workflows/2-deploy-to-staging.yml
vendored
@@ -1,67 +0,0 @@
|
||||
name: 2 | Deploy staging
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'releases/staging'
|
||||
|
||||
jobs:
|
||||
deploy-to-staging:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: deploy/staging
|
||||
steps:
|
||||
- name: Send Slack message that deploy is starting
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Staging deploy started for branch: ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
- run: yarn build
|
||||
env:
|
||||
REACT_APP_STAGING: 1
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Update Cloudflare Pages deployment
|
||||
id: pages-deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: interface-staging
|
||||
directory: build
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
|
||||
branch: main
|
||||
- name: Send Slack message about deployment outcome
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
if: always()
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Staging deploy **${{ steps.pages-deployment.outcome }}** for: ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
with:
|
||||
environment: staging
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
27
.github/workflows/3-staging-to-prod.yml
vendored
27
.github/workflows/3-staging-to-prod.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: 3 | Push staging -> prod
|
||||
|
||||
# This CI job is responsible for force pushing the content of releases/staging to releases/prod. It
|
||||
# is restricted to web-reviewers through virtue of the GitHub environment protection rules for the
|
||||
# prod environment.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push-prod:
|
||||
name: 'Push to prod branch'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: push/prod
|
||||
steps:
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
|
||||
ref: releases/staging
|
||||
- name: Git config
|
||||
run: |
|
||||
git config user.name "UL Service Account"
|
||||
git config user.email "hello-happy-puppy@users.noreply.github.com"
|
||||
- name: Git push
|
||||
run: |
|
||||
git push origin releases/staging:releases/prod --force
|
||||
116
.github/workflows/4-deploy-to-prod.yml
vendored
116
.github/workflows/4-deploy-to-prod.yml
vendored
@@ -1,116 +0,0 @@
|
||||
name: 4 | Deploy prod
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'releases/prod'
|
||||
|
||||
jobs:
|
||||
deploy-to-prod:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: deploy/prod
|
||||
steps:
|
||||
- name: Send Slack message that build is starting
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Production deploy started for branch: ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
- run: yarn build
|
||||
- name: Bump and tag
|
||||
id: github-tag-action
|
||||
uses: mathieudutour/github-tag-action@d745f2e74aaf1ee82e747b181f7a0967978abee0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_branches: releases/prod
|
||||
default_bump: patch
|
||||
|
||||
- name: Pin to IPFS
|
||||
id: pinata
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
|
||||
with:
|
||||
pin-name: Uniswap ${{ steps.github-tag-action.outputs.new_tag }}
|
||||
path: './build'
|
||||
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
||||
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
||||
|
||||
- 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: ${{ steps.github-tag-action.outputs.new_tag }}
|
||||
release_name: Release ${{ steps.github-tag-action.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 mirrored at [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
You can also access the Uniswap Interface 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 hosted deployment of 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 }}/)
|
||||
|
||||
${{ steps.github-tag-action.outputs.changelog }}
|
||||
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Update Cloudflare Pages deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
id: pages-deployment
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: build
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
|
||||
branch: main
|
||||
|
||||
- name: Send Slack message about deployment outcome
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
if: always()
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Production deploy **${{ steps.pages-deployment.outcome }}** for: ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
|
||||
continue-on-error: true
|
||||
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'
|
||||
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 }}
|
||||
@@ -1,94 +0,0 @@
|
||||
name: Slack notifications for releases/* merges
|
||||
|
||||
# This CI job will push notifications to Slack whenever code is merged into any releases/* branch
|
||||
#
|
||||
# The steps of the command line kung-fu shown below are as follows:
|
||||
# First we take the JSON-formatted Github context
|
||||
# echo $GITHUB_CONTEXT \
|
||||
# Then we parse out the specific fields we want for our messages using jq and format it into tab-separated values
|
||||
# | jq '.event.commits[] | [.url, .id[0:7], .author.username, .timestamp, .message] | @tsv' \
|
||||
# We need to do some cleaning on this output - specifically removing quotes and replacing newlines with something easier to split
|
||||
# | sed 's/"//g' | sed 's/\\t/;/g' | sed 's/\\n/;/g' | sed 's/\\//g' \
|
||||
# We then use awk to format the TSV into a Slack message
|
||||
# | awk -F';' '{print "• <"$1"|"$2"> (<https://github.com/"$3"|"$3">, "$4") - "$5}' \
|
||||
# We need to deal with some escaping issues with newlines so that we don't break the Slack message format
|
||||
# | sed 's/$/\\n/g' | tr -d '\n' \
|
||||
# Finally we have to truncate the message to 3,000 characters max, otherwise Slack will reject it
|
||||
# | awk '{print substr($0,0,3000);}' \
|
||||
# Then shove the bytes into a file to store them in their exact format
|
||||
# > /tmp/parsed_github_context
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'releases/*'
|
||||
|
||||
jobs:
|
||||
notify-slack:
|
||||
name: 'Emit Slack notification(s)'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: notify/releases
|
||||
steps:
|
||||
- name: Parse event to slug
|
||||
id: parse-slug
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
# Formats the contents of the GitHub event into slugs: one line per commit, formatted for Slack.
|
||||
# Explanation for each line is in the comments above.
|
||||
run: |
|
||||
echo $GITHUB_CONTEXT \
|
||||
| jq '.event.commits[] | [.url, .id[0:7], .author.username, .timestamp, .message] | @tsv' \
|
||||
| sed 's/"//g' | sed 's/\\t/;/g' | sed 's/\\n/;/g' | sed 's/\\//g' \
|
||||
| awk -F';' '{print "• <"$1"|"$2"> (<https://github.com/"$3"|"$3">, "$4") - "$5}' \
|
||||
| sed 's/$/\\n/g' | tr -d '\n' \
|
||||
| awk '{print substr($0,0,3000);}' \
|
||||
> /tmp/parsed_github_context
|
||||
echo "SLACK_COMMITS=$(cat /tmp/parsed_github_context)" >> "$GITHUB_OUTPUT"
|
||||
- name: Send custom JSON data to Slack workflow
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "divider"
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Code merged to <https://github.com/Uniswap/interface/tree/${{ github.ref }}|${{ github.ref_name }}> branch:*\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "divider"
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Actor*: <https://github.com/${{ github.triggering_actor }}/|${{ github.triggering_actor }}>\n*Force pushed*: ${{ github.event.forced || false }}\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "${{ steps.parse-slug.outputs.SLACK_COMMITS || 'New branch created' }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "<${{ github.event.compare}}|View Diff>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
125
.github/workflows/release.yaml
vendored
125
.github/workflows/release.yaml
vendored
@@ -1,125 +0,0 @@
|
||||
name: Release
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 16 * * 1-4' # every day 16:00 UTC Monday-Thursday
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
wait-on-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: unit-tests
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: unit-tests
|
||||
- id: cypress-tests
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: cypress-tests
|
||||
- if: steps.unit-tests.outputs.conclusion != 'success' || steps.cypress-tests.outputs.conclusion != 'success'
|
||||
run: exit 1
|
||||
|
||||
tag:
|
||||
needs: wait-on-tests
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
new_tag: ${{ steps.github-tag-action.outputs.new_tag }}
|
||||
changelog: ${{ steps.github-tag-action.outputs.changelog }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Bump and tag
|
||||
id: github-tag-action
|
||||
uses: mathieudutour/github-tag-action@v6.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_branches: .*
|
||||
default_bump: patch
|
||||
|
||||
release:
|
||||
needs: tag
|
||||
if: ${{ needs.tag.outputs.new_tag != null }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn build
|
||||
|
||||
- name: Pin to IPFS
|
||||
id: pinata
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
|
||||
with:
|
||||
pin-name: Uniswap ${{ needs.tag.outputs.new_tag }}
|
||||
path: './build'
|
||||
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
||||
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
||||
|
||||
- name: Pin to Crust
|
||||
uses: crustio/ipfs-crust-action@v2.0.3
|
||||
continue-on-error: true
|
||||
timeout-minutes: 2
|
||||
with:
|
||||
cid: ${{ steps.pinata.outputs.hash }}
|
||||
seeds: ${{ secrets.CRUST_SEEDS }}
|
||||
|
||||
- name: Convert CIDv0 to CIDv1
|
||||
id: convert-cidv0
|
||||
uses: uniswap/convert-cidv0-cidv1@v1.0.0
|
||||
with:
|
||||
cidv0: ${{ steps.pinata.outputs.hash }}
|
||||
|
||||
- name: Release
|
||||
uses: actions/create-release@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ needs.tag.outputs.new_tag }}
|
||||
release_name: Release ${{ needs.tag.outputs.new_tag }}
|
||||
body: |
|
||||
IPFS hash of the deployment:
|
||||
- CIDv0: `${{ steps.pinata.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert-cidv0.outputs.cidv1 }}`
|
||||
|
||||
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
You can also access the Uniswap Interface directly from an IPFS gateway.
|
||||
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
|
||||
**You should always use an IPFS gateway that enforces origin separation**, or our alias to the latest release at [app.uniswap.org](https://app.uniswap.org).
|
||||
Your Uniswap settings are never remembered across different URLs.
|
||||
|
||||
IPFS gateways:
|
||||
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
|
||||
- [ipfs://${{ steps.pinata.outputs.hash }}/](ipfs://${{ steps.pinata.outputs.hash }}/)
|
||||
|
||||
${{ needs.tag.outputs.changelog }}
|
||||
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Update Cloudflare Pages deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: build
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
with:
|
||||
environment: production
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
190
.github/workflows/test.yml
vendored
190
.github/workflows/test.yml
vendored
@@ -1,190 +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.
|
||||
# Caches are saved every run (by keying on github.run_id), and the most recent available cache is loaded.
|
||||
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
|
||||
|
||||
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
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Lint
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
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 typecheck
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Typecheck
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
deps-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn yarn-deduplicate --strategy=highest --list --fail
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Dependency checks
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
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 test --coverage --maxWorkers=100%
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
flags: unit-tests
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Unit tests
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
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 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
|
||||
|
||||
# Allows for parallel re-runs of cypress tests without re-building.
|
||||
cypress-rerun:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: exit 0
|
||||
|
||||
cypress-test-matrix:
|
||||
needs: [build-e2e, cypress-rerun]
|
||||
runs-on: ubuntu-latest
|
||||
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
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-${{ hashFiles('**/node_modules/cypress/package.json') }}
|
||||
- run: |
|
||||
yarn cypress install
|
||||
yarn cypress info
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-e2e
|
||||
path: build
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: hardhat-cache
|
||||
with:
|
||||
path: cache
|
||||
key: ${{ runner.os }}-hardhat-${{ hashFiles('hardhat.config.js') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-hardhat-${{ hashFiles('hardhat.config.js') }}-
|
||||
|
||||
- uses: cypress-io/github-action@v4
|
||||
with:
|
||||
install: false
|
||||
record: true
|
||||
parallel: true
|
||||
start: yarn serve
|
||||
wait-on: 'http://localhost:3000'
|
||||
browser: chrome
|
||||
group: e2e
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMIT_INFO_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message }}
|
||||
COMMIT_INFO_AUTHOR: ${{ github.event.sender.login || github.event.head_commit.author.login }}
|
||||
# Cypress requires an email for filtering by author, but GitHub does not expose one.
|
||||
# GitHub's public profile email can be deterministically produced from user id/login.
|
||||
COMMIT_INFO_EMAIL: ${{ github.event.sender.id || github.event.head_commit.author.id }}+${{ github.event.sender.login || github.event.head_commit.author.login }}@users.noreply.github.com
|
||||
COMMIT_INFO_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.sha }}
|
||||
COMMIT_INFO_TIMESTAMP: ${{ github.event.pull_request.updated_at || github.event.head_commit.timestamp }}
|
||||
CYPRESS_PULL_REQUEST_ID: ${{ github.event.pull_request.number }}
|
||||
CYPRESS_PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }}
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
flags: e2e-tests
|
||||
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cypress tests
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
|
||||
cypress-tests:
|
||||
if: always()
|
||||
needs: [cypress-test-matrix]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: needs.cypress-test-matrix.result != 'success'
|
||||
run: exit 1
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -5,8 +5,7 @@
|
||||
/src/types/v3
|
||||
/src/abis/types
|
||||
/src/locales/**/*.js
|
||||
/src/locales/**/en-US.po
|
||||
/src/locales/**/pseudo.po
|
||||
/src/locales/**/*.po
|
||||
|
||||
# generated files
|
||||
/src/**/__generated__
|
||||
@@ -20,6 +19,8 @@ schema.graphql
|
||||
# testing
|
||||
/coverage
|
||||
/cache
|
||||
/functions/coverage
|
||||
/.swc
|
||||
|
||||
# builds
|
||||
/build
|
||||
@@ -47,7 +48,10 @@ notes.txt
|
||||
|
||||
package-lock.json
|
||||
|
||||
cypress/downloads
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
||||
.vercel
|
||||
|
||||
.wrangler
|
||||
|
||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
25
.snyk
25
.snyk
@@ -1,25 +0,0 @@
|
||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.25.0
|
||||
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
|
||||
ignore:
|
||||
SNYK-JS-OPENZEPPELINCONTRACTS-2964946:
|
||||
- '*':
|
||||
reason: None Given
|
||||
expires: 2099-01-01T00:00:00.000Z
|
||||
created: 2022-12-08T16:25:57.347Z
|
||||
SNYK-JS-OPENZEPPELINCONTRACTS-2958047:
|
||||
- '*':
|
||||
reason: None Given
|
||||
expires: 2099-01-01T00:00:00.000Z
|
||||
created: 2022-12-08T16:26:09.720Z
|
||||
SNYK-JS-OPENZEPPELINCONTRACTS-2958050:
|
||||
- '*':
|
||||
reason: None Given
|
||||
expires: 2099-01-01T00:00:00.000Z
|
||||
created: 2022-12-08T16:26:17.702Z
|
||||
SNYK-JS-OPENZEPPELINCONTRACTS-2965580:
|
||||
- '*':
|
||||
reason: None Given
|
||||
expires: 2099-01-01T00:00:00.000Z
|
||||
created: 2022-12-08T16:26:34.283Z
|
||||
patch: {}
|
||||
36
.swcrc
Normal file
36
.swcrc
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
// has to duplicate from package.json, see swc issue: https://swc.rs/docs/configuration/compilation#env
|
||||
// this breaks jest because jest is setting target for some reason
|
||||
// "env": {
|
||||
// "targets": "> 0.5%, not dead"
|
||||
// },
|
||||
"jsc": {
|
||||
// without this swc breaks WalletConnect class super() call
|
||||
"target": "es2020",
|
||||
"keepClassNames": true,
|
||||
"experimental": {
|
||||
"plugins": [
|
||||
[
|
||||
"@lingui/swc-plugin",
|
||||
{}
|
||||
],
|
||||
[
|
||||
"@swc/plugin-styled-components",
|
||||
{
|
||||
"displayName": true
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@uniswap/web-admins
|
||||
@@ -69,10 +69,10 @@ Other things to note:
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
- Swap on Uniswap V2: <https://app.uniswap.org/#/swap?use=v2>
|
||||
- View V2 liquidity: <https://app.uniswap.org/#/pools/v2>
|
||||
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
|
||||
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>
|
||||
- Swap on Uniswap V2: <https://app.uniswap.org/swap?use=v2>
|
||||
- View V2 liquidity: <https://app.uniswap.org/pools/v2>
|
||||
- Add V2 liquidity: <https://app.uniswap.org/add/v2>
|
||||
- Migrate V2 liquidity to V3: <https://app.uniswap.org/migrate/v2>
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
module.exports = {
|
||||
styledComponents: {
|
||||
fileName: isDev,
|
||||
displayName: isDev,
|
||||
},
|
||||
}
|
||||
26
codecov.yml
26
codecov.yml
@@ -6,8 +6,15 @@ ignore:
|
||||
- "**/instrumented/**/*"
|
||||
- "**/styles/**/*"
|
||||
- "styles/**/*"
|
||||
- "**/styled.tsx"
|
||||
- "**/constants/**/*"
|
||||
- "constants/**/*"
|
||||
- "src/dev/*"
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
|
||||
flag_management:
|
||||
default_rules:
|
||||
@@ -15,11 +22,22 @@ flag_management:
|
||||
- type: project
|
||||
target: auto
|
||||
threshold: 1%
|
||||
# Adjust the base when removing code to avoid penalizing tech debt payback / dead code removal.
|
||||
removed_code_behavior: adjust_base
|
||||
if_ci_failed: error
|
||||
- type: patch
|
||||
target: 80%
|
||||
target: 50%
|
||||
individual_flags:
|
||||
- name: e2e-tests
|
||||
- name: unit-tests
|
||||
- name: cloud-tests
|
||||
statuses:
|
||||
- type: patch
|
||||
target: 0%
|
||||
- type: project
|
||||
target: 80%
|
||||
|
||||
comment:
|
||||
layout: flags
|
||||
hide_comment_details: false
|
||||
|
||||
github_checks:
|
||||
# Turn off GitHub Check annotations, as they make it more difficult to review code.
|
||||
annotations: false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
overrideExisting: true
|
||||
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
|
||||
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap'
|
||||
generates:
|
||||
./src/graphql/thegraph/schema/schema.graphql:
|
||||
plugins:
|
||||
|
||||
111
craco.config.cjs
111
craco.config.cjs
@@ -5,11 +5,14 @@ const { execSync } = require('child_process')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const path = require('path')
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
||||
const { DefinePlugin, IgnorePlugin, ProvidePlugin } = require('webpack')
|
||||
const { IgnorePlugin, ProvidePlugin } = require('webpack')
|
||||
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
|
||||
|
||||
const commitHash = execSync('git rev-parse HEAD').toString().trim()
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
process.env.REACT_APP_GIT_COMMIT_HASH = commitHash
|
||||
|
||||
// Linting and type checking are only necessary as part of development and testing.
|
||||
// Omit them from production builds, as they slow down the feedback loop.
|
||||
const shouldLintOrTypeCheck = !isProduction
|
||||
@@ -20,32 +23,6 @@ function getCacheDirectory(cacheName) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
babel: {
|
||||
plugins: [
|
||||
'@vanilla-extract/babel-plugin',
|
||||
...(process.env.REACT_APP_ADD_COVERAGE_INSTRUMENTATION
|
||||
? [
|
||||
[
|
||||
'istanbul',
|
||||
{
|
||||
all: true,
|
||||
include: ['src/**/*.tsx', 'src/**/*.ts'],
|
||||
exclude: [
|
||||
'src/**/*.css',
|
||||
'src/**/*.css.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.test.tsx',
|
||||
'src/**/*.spec.ts',
|
||||
'src/**/*.spec.tsx',
|
||||
'src/**/graphql/**/*',
|
||||
'src/**/*.d.ts',
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
eslint: {
|
||||
enable: shouldLintOrTypeCheck,
|
||||
pluginOptions(eslintConfig) {
|
||||
@@ -68,16 +45,20 @@ module.exports = {
|
||||
configure(jestConfig) {
|
||||
return Object.assign(jestConfig, {
|
||||
cacheDirectory: getCacheDirectory('jest'),
|
||||
transform: Object.assign(jestConfig.transform, {
|
||||
transform: {
|
||||
...Object.entries(jestConfig.transform).reduce((transform, [key, value]) => {
|
||||
if (value.match(/babel/)) return transform
|
||||
return { ...transform, [key]: value }
|
||||
}, {}),
|
||||
// Transform vanilla-extract using its own transformer.
|
||||
// See https://sandroroth.com/blog/vanilla-extract-cra#jest-transform.
|
||||
'\\.css\\.ts$': '@vanilla-extract/jest-transform',
|
||||
}),
|
||||
// Use @uniswap/conedison's build directly, as jest does not support its exports.
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||
'\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
// Use d3-arrays's build directly, as jest does not support its exports.
|
||||
transformIgnorePatterns: ['d3-array'],
|
||||
moduleNameMapper: {
|
||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
|
||||
'd3-array': 'd3-array/dist/d3-array.min.js',
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -87,25 +68,24 @@ module.exports = {
|
||||
// Webpack 5 does not polyfill node globals, so we do so for those necessary:
|
||||
new ProvidePlugin({
|
||||
// - react-markdown requires process.cwd
|
||||
process: 'process/browser',
|
||||
process: 'process/browser.js',
|
||||
}),
|
||||
new VanillaExtractPlugin(),
|
||||
new RetryChunkLoadPlugin({
|
||||
cacheBust: `function() {
|
||||
return 'cache-bust=' + Date.now();
|
||||
}`,
|
||||
// Retries with exponential backoff (500ms, 1000ms, 2000ms).
|
||||
retryDelay: `function(retryAttempt) {
|
||||
return 2 ** (retryAttempt - 1) * 500;
|
||||
}`,
|
||||
maxRetries: 3,
|
||||
}),
|
||||
// vanilla-extract has poor performance on M1 machines with 'debug' identifiers, so we use 'short' instead.
|
||||
// See https://vanilla-extract.style/documentation/integrations/webpack/#identifiers for docs.
|
||||
// See https://github.com/vanilla-extract-css/vanilla-extract/issues/771#issuecomment-1249524366.
|
||||
new VanillaExtractPlugin({ identifiers: 'short' }),
|
||||
],
|
||||
configure: (webpackConfig) => {
|
||||
// Configure webpack plugins:
|
||||
webpackConfig.plugins = webpackConfig.plugins
|
||||
.map((plugin) => {
|
||||
// Extend process.env with dynamic values (eg commit hash).
|
||||
// This will make dynamic values available to JavaScript only, not to interpolated HTML (ie index.html).
|
||||
if (plugin instanceof DefinePlugin) {
|
||||
Object.assign(plugin.definitions['process.env'], {
|
||||
REACT_APP_GIT_COMMIT_HASH: JSON.stringify(commitHash),
|
||||
})
|
||||
}
|
||||
|
||||
// CSS ordering is mitigated through scoping / naming conventions, so we can ignore order warnings.
|
||||
// See https://webpack.js.org/plugins/mini-css-extract-plugin/#remove-order-warnings.
|
||||
if (plugin instanceof MiniCssExtractPlugin) {
|
||||
@@ -150,19 +130,32 @@ module.exports = {
|
||||
},
|
||||
})
|
||||
|
||||
// Retain source maps for node_modules packages:
|
||||
webpackConfig.module.rules[0] = {
|
||||
...webpackConfig.module.rules[0],
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
|
||||
// Configure webpack transpilation (create-react-app specifies transpilation rules in a oneOf):
|
||||
webpackConfig.module.rules[1].oneOf = webpackConfig.module.rules[1].oneOf.map((rule) => {
|
||||
// The fallback rule (eg for dependencies).
|
||||
if (rule.loader && rule.loader.match(/babel-loader/) && !rule.include) {
|
||||
// Allow not-fully-specified modules so that legacy packages are still able to build.
|
||||
rule.resolve = { fullySpecified: false }
|
||||
|
||||
// The class properties transform is required for @uniswap/analytics to build.
|
||||
rule.options.plugins.push('@babel/plugin-proposal-class-properties')
|
||||
if (rule.loader && rule.loader.match(/babel-loader/)) {
|
||||
rule.loader = 'swc-loader'
|
||||
delete rule.options
|
||||
}
|
||||
return rule
|
||||
})
|
||||
|
||||
// Run terser compression on node_modules before tree-shaking, so that tree-shaking is more effective.
|
||||
// This works by eliminating dead code, so that webpack can identify unused imports and tree-shake them;
|
||||
// it is only necessary for node_modules - it is done through linting for our own source code -
|
||||
// see https://medium.com/engineering-housing/dead-code-elimination-and-tree-shaking-at-housing-part-1-307a94b30f23#7e03:
|
||||
webpackConfig.module.rules.push({
|
||||
enforce: 'post',
|
||||
test: /node_modules.*\.(js)$/,
|
||||
loader: path.join(__dirname, 'scripts/terser-loader.js'),
|
||||
options: { compress: true, mangle: false },
|
||||
})
|
||||
|
||||
// Configure webpack optimization:
|
||||
webpackConfig.optimization = Object.assign(
|
||||
webpackConfig.optimization,
|
||||
@@ -181,16 +174,8 @@ module.exports = {
|
||||
: {}
|
||||
)
|
||||
|
||||
// Configure webpack caching:
|
||||
webpackConfig.cache = Object.assign(webpackConfig.cache, {
|
||||
cacheDirectory: getCacheDirectory('webpack'),
|
||||
})
|
||||
|
||||
// Ignore failed source mappings to avoid spamming the console.
|
||||
// Source mappings for a package will fail if the package does not provide them, but the build will still succeed,
|
||||
// so it is unnecessary (and bothersome) to log it. This should be turned off when debugging missing sourcemaps.
|
||||
// See https://webpack.js.org/loaders/source-map-loader#ignoring-warnings.
|
||||
webpackConfig.ignoreWarnings = [/Failed to parse source map/]
|
||||
// Configure webpack resolution. webpackConfig.cache is unused with swc-loader, but the resolver can still cache:
|
||||
webpackConfig.resolve = Object.assign(webpackConfig.resolve, { unsafeCache: true })
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
|
||||
@@ -1,37 +1,19 @@
|
||||
import codeCoverageTask from '@cypress/code-coverage/task'
|
||||
import { defineConfig } from 'cypress'
|
||||
import { setupHardhatEvents } from 'cypress-hardhat'
|
||||
import { unlinkSync } from 'fs'
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'yp82ef',
|
||||
defaultCommandTimeout: 24000, // 2x average block time
|
||||
chromeWebSecurity: false,
|
||||
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
|
||||
retries: { runMode: 2 },
|
||||
retries: { runMode: process.env.CYPRESS_RETRIES ? +process.env.CYPRESS_RETRIES : 1 },
|
||||
video: false, // GH provides 2 CPUs, and cypress video eats one up, see https://github.com/cypress-io/cypress/issues/20468#issuecomment-1307608025
|
||||
e2e: {
|
||||
async setupNodeEvents(on, config) {
|
||||
await setupHardhatEvents(on, config)
|
||||
codeCoverageTask(on, config)
|
||||
|
||||
// Delete recorded videos for specs that passed without flakes.
|
||||
on('after:spec', async (spec, results) => {
|
||||
if (results && results.video) {
|
||||
// If there were no failures (including flakes), delete the recorded video.
|
||||
if (!results.tests?.some((test) => test.attempts.some((attempt) => attempt?.state === 'failed'))) {
|
||||
unlinkSync(results.video)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...config,
|
||||
// Only enable Chrome.
|
||||
// Electron (the default) has issues injecting window.ethereum before pageload, so it is not viable.
|
||||
browsers: config.browsers.filter(({ name }) => name === 'chrome'),
|
||||
}
|
||||
return config
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
specPattern: 'cypress/{e2e,staging}/**/*.test.ts',
|
||||
},
|
||||
})
|
||||
|
||||
201
cypress/README.md
Normal file
201
cypress/README.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# e2e testing with Cypress
|
||||
|
||||
End-to-end tests are run through [Cypress](https://docs.cypress.io/api/table-of-contents/), which runs tests in a real browser. Cypress is a little different than other testing frameworks, and e2e tests are a little different than unit tests, so this directory has its own set of patterns, idioms, and best practices. Not only that, but we're testing against a forked blockchain, not just against typical Web APIs, so we have unique flows that you may not have seen elsewhere.
|
||||
|
||||
## Running your first e2e tests
|
||||
|
||||
Cypress tests run against a local server, so you'll need to run the application locally at the same time. The fastest way to run e2e tests is to use your dev server: `yarn start`.
|
||||
|
||||
Open cypress at the same time with `yarn cypress:open`. You should do this from another window or tab, so that you can continue to see any typechecking/linting warnings from `yarn start`.
|
||||
|
||||
Cypress opens its own instance of Chrome, with a list of "E2E specs" for you to select. When you're developing locally, you usually only want to run one spec file at a time. Select your spec by clicking on the filename and it will run.
|
||||
|
||||
## Glossary
|
||||
|
||||
#### spec
|
||||
Cypress considers each file a separate spec, or collection of tests.
|
||||
Specs are always run as a whole through `yarn cypress:open` or on the same machine through CI.
|
||||
|
||||
#### Thenable
|
||||
Cypress queues commands to run in the browser using [Thenables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables), not Promises.
|
||||
For this reason, you should not use `async/await` syntax in Cypress unless it is wholly-contained in a `cy.then` function argument.
|
||||
|
||||
## Writing your first e2e test
|
||||
|
||||
_For an excellent treatment on tests, check out the [Cypress Fundamentals](https://learn.cypress.io/cypress-fundamentals/how-to-write-a-test) course._
|
||||
_While some of that will be paraphrased here, this should be sufficient to get you started:_
|
||||
|
||||
### What is a test?
|
||||
|
||||
Cypress tests are just like any other test: you should set up an initial state, execute an action, and verify the action's consequence. This is codified in the AAA (Arrange-Act-Assert) pattern, and you'll see this in most of our tests. In _our_ case, it plays out as:
|
||||
|
||||
1. Arrange: Visit a page, eg `cy.visit('/swap')`, and set up the state, on the blockchain and the page.
|
||||
2. Act: Initiate your action under test, eg `initiateSwap()`
|
||||
3. Assert: Verify that the action has occured, eg `// Verify swap has occured`
|
||||
|
||||
You'll usually see the setup, followed by a newline, followed by assertions with comments stating what they are asserting.
|
||||
Because Cypress tests are translated into user actions, it may be hard to follow the action being described. You should use comments liberally to describe what you are doing and what you intend to test, to make tests easier to read and maintain in the future.
|
||||
|
||||
### Thinking about tests: queuing up a sequence of commands
|
||||
|
||||
Cypress uses `Thenable`s to achieve "command chaining". A test is described as a series of commands, which are only executed once the previous command in the chain has executed.
|
||||
|
||||
```
|
||||
cy.visit('/swap')
|
||||
cy.contains('Select token').click()
|
||||
cy.contains('DAI').click()
|
||||
```
|
||||
|
||||
In this example, `cy.contains('Select token').click()` is queued up right away (all the code is synchronous), but it will not execute until `/swap` has loaded (all the commands are chained); and `click()` will not execute until `Select token` has been found.
|
||||
|
||||
This becomes more relevant as you work with data on the blockchain, as you'll need to load it at the correct time, _after_ it's been modified by the application:
|
||||
|
||||
```
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// wait for the transaction to be executed
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// BAD: This will get the balance _before_ the other queued actions have executed.
|
||||
const balance = await hardhat.getBalance(hardhat.wallet, USDC_MAINNET)
|
||||
cy.wrap(balance).should('deep.equal', expectedBalance)
|
||||
})
|
||||
```
|
||||
|
||||
```
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// wait for the transaction to be executed
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// GOOD: cy.then chains the command so that it runs _after_ executing the swap
|
||||
cy.hardhat()
|
||||
.then((hardhat) => hardhat.getBalance(hardhat.wallet, USDC_MAINNET))
|
||||
.should('deep.equal', expectedBalance)
|
||||
})
|
||||
```
|
||||
|
||||
### Working with the blockchain (ie hardhat)
|
||||
|
||||
Our tests use a local hardhat node to simulate blockchain transactions. This can be accessed with `cy.hardhat().then((hardhat) => ...)`.
|
||||
|
||||
By default, automining is turned on, so that any transaction that you send to the blockchain is mined immediately. If you want to assert on intermediate states (between sending a transaction and mining it), you can turn off automining: `cy.hardhat({ automine: false })`.
|
||||
|
||||
The hardhat integration has built-in utilities to let you modify and assert on balances, approvals, and permits, and should be fully typed. Check it out at [Uniswap/cypress-hardhat](https://github.com/Uniswap/cypress-hardhat).
|
||||
|
||||
### Asserting on wallet methods
|
||||
|
||||
Wallet methods to hardhat are all aliased. If you'd like to assert that a method was sent to the wallet, you can do so using the method name, prefixed with `@`:
|
||||
|
||||
```
|
||||
// Asserts that `eth_sendRawTransaction` was sent to the wallet.
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
```
|
||||
|
||||
Sometimes, you may want a method to _fail_. In this case, you can stub it, but you should disable logging to avoid spamming the test:
|
||||
|
||||
```
|
||||
// Stub calls to eth_signTypedData_v4 and fail them
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Note the closure to keep signTypedDataStub in scope. Using closures instead of variables (eg let) helps prevent misuse of chaining.
|
||||
const signTypedDataStub = cy.stub(hardhat.provider, 'send').log(false)
|
||||
signTypedDataStub.withArgs('eth_signTypedData_v4).rejects(USER_REJECTION)
|
||||
signTypedDataStub.callThrough() // allws other methods to call through to hardhat
|
||||
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Verify the call occured
|
||||
// Note the call to cy.wrap to correctly queue the chained command. Without this, the test would occur before the stub is called.
|
||||
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
|
||||
|
||||
// Restore the stub
|
||||
// note the call to cy.then to correctly queue the chained command. Without this, the stub would be restored immediately.
|
||||
cy.then(() => permitApprovalStub.restore())
|
||||
})
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
<!-- Best practices should all be labeled using H3, with the rationale italicized at the end of the section. -->
|
||||
<!-- Best practice 🤣 is to also include an example before your rationale. -->
|
||||
|
||||
### Spec / test grouping
|
||||
|
||||
Each spec should be specific to one route, _not_ one functional behavior.
|
||||
For example, `token-details.test.ts` is separated from `swap.test.ts`.
|
||||
|
||||
If a route has different functional behaviors, that route should become a directory name, and its spec should be split.
|
||||
For example, `swap.test.ts` may be split into `swap/swap.test.ts`, `swap/wrap.test.ts`, `swap/permit2.test.ts`.
|
||||
|
||||
_This prevents specs from growing too large, which is important because they are always run as a whole locally and on the same machine through CI. If a spec grows too large, it will have a longer local feedback loop, and it will become the bottleneck for CI test runtime._
|
||||
|
||||
_Similarly, avoid actions outside the scope of your spec, as it will cause total testing time to increase._
|
||||
|
||||
### Use closures instead of variables
|
||||
|
||||
Avoid usage of `let`, instead assigning a constant. In practice, this means using closures for your variables:
|
||||
|
||||
```javascript
|
||||
let badVariable
|
||||
|
||||
cy.hardhat({ automine: false })
|
||||
.then((hardhat) => cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address)))
|
||||
.then((initialBalance) => {
|
||||
// Do not assign to a variable outside of your closure!
|
||||
badVariable = initialBalance // <-- bad!
|
||||
|
||||
// Use initial balance here, within the closure.
|
||||
})
|
||||
|
||||
cy.get('.class-name').then((el) => {
|
||||
// Do not use badVariable here! It may have changed value due to the queued async nature of Cypress.
|
||||
expect(el).should('contain', badVariable) // <-- bad!
|
||||
})
|
||||
```
|
||||
|
||||
_This prevents misuse of a not-yet-initialized variable, or a variable that has changed as the test progresses._
|
||||
|
||||
### Prefer selecting elements using on-screen text over data-testid attributes
|
||||
|
||||
When selecting components (eg with `cy.get`), prefer defining your selector with visible UI. Sometimes this is not possible (eg if the text is duplicated on-screen), and you'll need to add a `data-testid` property.
|
||||
|
||||
_Defining tests using visual fields helps ensure that we don't break them. `data-testid` may select an element that is only selectable programmatically, and should be used only when necessary, as its use may cover up UI breakages._
|
||||
|
||||
_You'll still want to use `data-testid` in cases where the text is rendered in multiple containers and you need to select the correct one, or where the component doesn't render predictable text output._
|
||||
|
||||
### Avoid branching logic
|
||||
|
||||
Do not write tests that rely on if-statements or conditionals. Do not create helper methods which do more than one thing, and rely on branching logic to apply to different but similar situations.
|
||||
|
||||
_Tests should be readable and simple. Branching logic makes it harder to reason about tests, and may hide otherwise flaky or ill-defined behaviors._
|
||||
|
||||
_Similarly, you should avoid complicated for-loops. Sometimes, for simple repetition, for-loops are ok._
|
||||
|
||||
### Avoid spamming the console
|
||||
|
||||
It is ok to include logging while you are developing a test, but that logging should be removed if it is not needed to debug (potential) errors.
|
||||
|
||||
For example, stubbing a wallet method will result in dumping a hex string (the calldata) to the log. Instead, suppress logging from methods which you know will flood the log.
|
||||
|
||||
```javascript
|
||||
cy.stub(hardhat.wallet, 'sendTransaction')
|
||||
.log(false) // <-- suppresses logs from this stub
|
||||
.rejects(new Error('user cancelled'))
|
||||
```
|
||||
|
||||
_Unnecessary logs it makes it harder to reason about a test overall._
|
||||
|
||||
### Name helper methods using transitive verbs
|
||||
|
||||
Name helper methods using "action verbs": `expectsThisToHappen`, not `expectThisToHappen`; `selectsToken(token: string)`, not `selectAToken(token: string)`.
|
||||
|
||||
_This makes your tests read more naturally, and makes it easier to follow given existing `should` syntax._
|
||||
@@ -4,61 +4,113 @@ import { aliasQuery, hasQuery } from '../utils/graphql-test-utils'
|
||||
|
||||
describe('Add Liquidity', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req) => {
|
||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3?source=uniswap', (req) => {
|
||||
aliasQuery(req, 'feeTierDistribution')
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/500')
|
||||
it('loads the token pair', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.contains('0.05% fee tier')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/add/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
it('clears the token selection when chain changes', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.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', () => {
|
||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'UNI')
|
||||
it('does not crash if token is duplicated', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it.skip('single token can be selected', () => {
|
||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
||||
it('single token can be selected', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it.skip('loads fee tier distribution', () => {
|
||||
it('loads fee tier distribution', () => {
|
||||
cy.fixture('feeTierDistribution.json').then((feeTierDistribution) => {
|
||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req: CyHttpMessages.IncomingHttpRequest) => {
|
||||
if (hasQuery(req, 'FeeTierDistributionQuery')) {
|
||||
req.alias = 'FeeTierDistributionQuery'
|
||||
cy.intercept(
|
||||
'POST',
|
||||
'/subgraphs/name/uniswap/uniswap-v3?source=uniswap',
|
||||
(req: CyHttpMessages.IncomingHttpRequest) => {
|
||||
if (hasQuery(req, 'FeeTierDistribution')) {
|
||||
req.alias = 'FeeTierDistribution'
|
||||
|
||||
req.reply({
|
||||
body: {
|
||||
data: {
|
||||
...feeTierDistribution,
|
||||
req.reply({
|
||||
body: {
|
||||
data: {
|
||||
...feeTierDistribution,
|
||||
},
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
'access-control-allow-origin': '*',
|
||||
},
|
||||
})
|
||||
headers: {
|
||||
'access-control-allow-origin': '*',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
|
||||
cy.wait('@FeeTierDistribution')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.30% fee tier')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40% select')
|
||||
})
|
||||
})
|
||||
|
||||
cy.wait('@FeeTierDistributionQuery')
|
||||
it('disables increment and decrement until initial prices are inputted', () => {
|
||||
// ETH / BITCOIN pool (0.05% tier not created)
|
||||
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
|
||||
// Set starting price in order to enable price range step counters
|
||||
cy.get('.start-price-input').type('1000')
|
||||
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.3% fee tier')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40%')
|
||||
// Min Price increment / decrement buttons should be disabled
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
|
||||
// Enter min price, which should enable the buttons
|
||||
cy.get('.rate-input-0').eq(0).type('900').blur()
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('not.be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('not.be.disabled')
|
||||
|
||||
// Repeat for Max Price step counter
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
|
||||
// Enter max price, which should enable the buttons
|
||||
cy.get('.rate-input-0').eq(1).type('1100').blur()
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('not.be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('allows full range selection on new pool creation', () => {
|
||||
// ETH / BITCOIN pool (0.05% tier not created)
|
||||
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
|
||||
// Set starting price in order to enable price range step counters
|
||||
cy.get('.start-price-input').type('1000')
|
||||
cy.get('[data-testid="set-full-range"]').click()
|
||||
// Check that the min price is 0 and the max price is infinity
|
||||
cy.get('.rate-input-0').eq(0).should('have.value', '0')
|
||||
cy.get('.rate-input-0').eq(1).should('have.value', '∞')
|
||||
// Increment and decrement buttons are disabled when full range is selected
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
|
||||
// Check that url params were added
|
||||
cy.url().then((url) => {
|
||||
const params = new URLSearchParams(url)
|
||||
const minPrice = params.get('minPrice')
|
||||
const maxPrice = params.get('maxPrice')
|
||||
// Note: although 0 and ∞ displayed, actual values in query are ticks at limit
|
||||
return minPrice && maxPrice && parseFloat(minPrice) < parseFloat(maxPrice)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
import { CONNECTED_WALLET_USER_STATE, DISCONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
|
||||
describe('Landing Page', () => {
|
||||
it('shows landing page when no user state exists', () => {
|
||||
cy.visit('/', { userState: {} })
|
||||
cy.visit('/', { userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
cy.get(getTestSelector('landing-page'))
|
||||
cy.screenshot()
|
||||
})
|
||||
@@ -39,4 +39,50 @@ describe('Landing Page', () => {
|
||||
cy.get(getTestSelector('pool-nav-link')).last().click()
|
||||
cy.url().should('include', '/pools')
|
||||
})
|
||||
|
||||
it('does not render landing page when / path is blocked', () => {
|
||||
cy.intercept('/', (req) => {
|
||||
req.reply((res) => {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(res.body, 'text/html')
|
||||
const meta = document.createElement('meta')
|
||||
meta.setAttribute('property', 'x:blocked-paths')
|
||||
meta.setAttribute('content', '/,/buy')
|
||||
doc.head.appendChild(meta)
|
||||
|
||||
res.body = doc.documentElement.outerHTML
|
||||
})
|
||||
})
|
||||
cy.visit('/', { userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
|
||||
cy.get(getTestSelector('landing-page')).should('not.exist')
|
||||
cy.get(getTestSelector('buy-fiat-button')).should('not.exist')
|
||||
cy.url().should('include', '/swap')
|
||||
})
|
||||
|
||||
it('does not render uk compliance banner in US', () => {
|
||||
cy.visit('/swap')
|
||||
cy.contains('UK disclaimer').should('not.exist')
|
||||
})
|
||||
|
||||
it('renders uk compliance banner in uk', () => {
|
||||
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const requestBody = JSON.stringify(req.body)
|
||||
const byteSize = new Blob([requestBody]).size
|
||||
req.alias = 'amplitude'
|
||||
req.reply(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
server_upload_time: Date.now(),
|
||||
payload_size_bytes: byteSize,
|
||||
events_ingested: req.body.events.length,
|
||||
}),
|
||||
{
|
||||
'origin-country': 'GB',
|
||||
}
|
||||
)
|
||||
})
|
||||
cy.visit('/swap')
|
||||
cy.contains('UK disclaimer')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
describe('Link', () => {
|
||||
it('should update route', () => {
|
||||
cy.viewport(2000, 1600)
|
||||
cy.visit('/')
|
||||
cy.visit('/swap')
|
||||
cy.contains('Pool').click()
|
||||
cy.get('[data-cy="join-pool-button"]').should('exist')
|
||||
})
|
||||
|
||||
@@ -2,30 +2,71 @@ import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Mini Portfolio account drawer', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(/api.uniswap.org\/v1\/graphql/, cy.spy().as('gqlSpy'))
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
const portfolioSpy = cy.spy().as('portfolioSpy')
|
||||
cy.intercept(/api.uniswap.org\/v1\/graphql/, (req) => {
|
||||
if (req.body.operationName === 'PortfolioBalances') {
|
||||
portfolioSpy(req)
|
||||
}
|
||||
})
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('fetches balances when account button is first hovered', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@gqlSpy').should('not.have.been.called')
|
||||
cy.get('@portfolioSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
it('should not re-fetch balances on second hover', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@portfolioSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
|
||||
// Balances should not be refetched upon second hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
it('should not re-fetch balances when the account drawer is opened', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@portfolioSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
|
||||
// Balances should not be refetched upon opening drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
// Balances should not be refetched upon closing & reopening drawer
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
it('fetches account information', () => {
|
||||
// Open the mini portfolio
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/tokens.json' })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
|
||||
// Verify that wallet state loads correctly
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Tokens')
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('Hidden (197)')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/nfts.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('I Got Plenty')
|
||||
|
||||
// Skip this for now, someone sent test account an NFT on block 17445713 that causes this test to fail
|
||||
// cy.intercept(/graphql/, { fixture: 'mini-portfolio/pools.json' })
|
||||
// cy.get(getTestSelector('mini-portfolio-navbar')).contains('Pools').click()
|
||||
// cy.get(getTestSelector('mini-portfolio-page')).contains('No pools yet')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/full_activity.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('Contract Interaction')
|
||||
})
|
||||
|
||||
it('refetches balances when account changes', () => {
|
||||
@@ -55,4 +96,36 @@ describe('Mini Portfolio account drawer', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fetches ENS name', () => {
|
||||
cy.hardhat().then(() => {
|
||||
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
|
||||
const haydenENS = 'hayden.eth'
|
||||
|
||||
// Opens the account drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// Simulate wallet changing to Hayden's account
|
||||
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
|
||||
|
||||
// Hayden's ENS name should be shown
|
||||
cy.contains(haydenENS).should('exist')
|
||||
|
||||
// Close account drawer
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
|
||||
// Switch chain to Polygon
|
||||
cy.get(getTestSelector('chain-selector')).eq(1).click()
|
||||
cy.contains('Polygon').click()
|
||||
|
||||
//Reopen account drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// Simulate wallet changing to Hayden's account
|
||||
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
|
||||
|
||||
// Hayden's ENS name should be shown
|
||||
cy.contains(haydenENS).should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,9 +94,7 @@ describe('mini-portfolio activity history', () => {
|
||||
})
|
||||
|
||||
it('should deduplicate activity history by nonce', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' }).hardhat({
|
||||
automine: false,
|
||||
})
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`).hardhat({ automine: false })
|
||||
|
||||
// Input swap info.
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
@@ -108,7 +106,7 @@ describe('mini-portfolio activity history', () => {
|
||||
|
||||
// Check activity history tab.
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-activity')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
|
||||
// Assert that the local pending transaction is replaced by a remote transaction with the same nonce.
|
||||
cy.contains('Swapping').should('not.exist')
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
||||
|
||||
describe('Testing nfts', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('should load nft leaderboard', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('nft-nav')).first().click()
|
||||
cy.get(getTestSelector('nft-nav')).first().should('exist')
|
||||
cy.get(getTestSelector('nft-nav')).first().click()
|
||||
@@ -16,7 +12,7 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should load pudgy penguin collection page', () => {
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-collection-asset')).should('exist')
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
@@ -24,13 +20,13 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should be able to navigate to activity', () => {
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-activity')).first().click()
|
||||
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
||||
})
|
||||
|
||||
it('should go to the details page', () => {
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||
cy.get(getTestSelector('nft-collection-asset')).first().click()
|
||||
@@ -41,7 +37,10 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should toggle buy now on details page', () => {
|
||||
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
|
||||
cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||
cy.get(getTestSelector('nft-collection-asset')).first().click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('exist')
|
||||
cy.get(getTestSelector('nft-details-description')).click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
||||
@@ -49,15 +48,11 @@ describe('Testing nfts', () => {
|
||||
cy.get(getTestSelector('nft-bag')).should('exist')
|
||||
})
|
||||
|
||||
it('should navigate to the owned nfts page', () => {
|
||||
it('should navigate to and from the owned nfts page', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('nft-view-self-nfts')).click()
|
||||
})
|
||||
|
||||
it('should close the sidebar when navigating to NFT details', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-nfts')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
|
||||
cy.contains('Buy crypto').should('not.be.visible')
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,216 +1,277 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { MaxUint160, MaxUint256 } from '@uniswap/permit2-sdk'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
|
||||
import { DAI, USDC_MAINNET } from '../../src/constants/tokens'
|
||||
import { DAI, USDC_MAINNET, USDT } from '../../src/constants/tokens'
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
/** Initiates a swap. */
|
||||
function initiateSwap() {
|
||||
// The swap button is re-rendered once enable, so we must wait until the original button is not disabled to re-select the appropriate button.
|
||||
// The swap button is re-rendered once enabled, so we must wait until the original button is not disabled to re-select the appropriate button.
|
||||
cy.get('#swap-button').should('not.be.disabled')
|
||||
// Completes the swap.
|
||||
cy.get('#swap-button').click()
|
||||
cy.get(getTestSelector('confirm-swap-button')).click()
|
||||
cy.contains('Confirm swap').click()
|
||||
}
|
||||
|
||||
describe('Permit2', () => {
|
||||
// The same tokens & swap-amount combination is used for all permit2 tests.
|
||||
const INPUT_TOKEN = DAI
|
||||
const OUTPUT_TOKEN = USDC_MAINNET
|
||||
const TEST_BALANCE_INCREMENT = 0.01
|
||||
|
||||
beforeEach(() => {
|
||||
// Sets up a swap between INPUT_TOKEN and OUTPUT_TOKEN.
|
||||
cy.visit(`/swap/?inputCurrency=${INPUT_TOKEN.address}&outputCurrency=${OUTPUT_TOKEN.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
})
|
||||
cy.get('#swap-currency-input .token-amount-input').type(TEST_BALANCE_INCREMENT.toString())
|
||||
})
|
||||
function setupInputs(inputToken: Token, outputToken: Token) {
|
||||
// Sets up a swap between inputToken and outputToken.
|
||||
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`)
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01')
|
||||
}
|
||||
|
||||
/** Asserts permit2 has a max approval for spend of the input token on-chain. */
|
||||
function expectTokenAllowanceForPermit2ToBeMax() {
|
||||
function expectTokenAllowanceForPermit2ToBeMax(inputToken: Token) {
|
||||
// check token approval
|
||||
return cy
|
||||
.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }))
|
||||
.should('deep.equal', MaxUint256)
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: inputToken }))
|
||||
.then((allowance) => {
|
||||
Cypress.log({ name: `Token allowance: ${allowance.toString()}` })
|
||||
cy.wrap(allowance).should('deep.equal', MaxUint256)
|
||||
})
|
||||
}
|
||||
|
||||
/** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */
|
||||
function expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime: number) {
|
||||
return cy
|
||||
.hardhat()
|
||||
.then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN }))
|
||||
function expectPermit2AllowanceForUniversalRouterToBeMax(inputToken: Token) {
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getPermit2Allowance({ owner: wallet, token: inputToken }))
|
||||
.then((allowance) => {
|
||||
cy.wrap(MaxUint160.eq(allowance.amount)).should('eq', true)
|
||||
Cypress.log({ name: `Permit2 allowance: ${allowance.amount.toString()}` })
|
||||
cy.wrap(allowance.amount).should('deep.equal', MaxUint160)
|
||||
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
|
||||
const expected = Math.floor((approvalTime + 2_592_000_000) / 1000)
|
||||
const THIRTY_DAYS_SECONDS = 2_592_000
|
||||
const expected = Math.floor(Date.now() / 1000 + THIRTY_DAYS_SECONDS)
|
||||
cy.wrap(allowance.expiration).should('be.closeTo', expected, 40)
|
||||
})
|
||||
}
|
||||
|
||||
it('swaps when user has already approved token and permit2', () => {
|
||||
cy.hardhat().then(({ approval, wallet }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
|
||||
beforeEach(() =>
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(DAI, 1e18))
|
||||
await hardhat.mine()
|
||||
})
|
||||
)
|
||||
|
||||
describe('approval process (with intermediate screens)', () => {
|
||||
// Turn off automine so that intermediate screens are available to assert on.
|
||||
beforeEach(() => cy.hardhat({ automine: false }))
|
||||
|
||||
it('swaps after completing full permit2 approval process', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
initiateSwap()
|
||||
|
||||
// verify that the modal retains its state when the window loses focus
|
||||
cy.window().trigger('blur')
|
||||
|
||||
// Verify token approval
|
||||
cy.contains('Enable spending DAI on Uniswap')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax(DAI)
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.contains('Allow DAI to be used for swapping')
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.contains('Swap submitted')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
|
||||
it('swaps with existing permit approval and missing token approval', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI })
|
||||
await hardhat.mine()
|
||||
})
|
||||
initiateSwap()
|
||||
|
||||
// Verify token approval
|
||||
cy.contains('Enable spending DAI on Uniswap')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax(DAI)
|
||||
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
/**
|
||||
* On mainnet, you have to revoke USDT approval before increasing it.
|
||||
* From the token contract:
|
||||
* To change the approve amount you first have to reduce the addresses`
|
||||
* allowance to zero by calling `approve(_spender, 0)` if it is not
|
||||
* already 0 to mitigate the race condition described here:
|
||||
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
|
||||
*/
|
||||
it('swaps USDT with existing permit, and existing but insufficient token approval', () => {
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6))
|
||||
await hardhat.mine()
|
||||
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDT }, 1e6)
|
||||
await hardhat.mine()
|
||||
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDT })
|
||||
await hardhat.mine()
|
||||
})
|
||||
setupInputs(USDT, USDC_MAINNET)
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('2')
|
||||
initiateSwap()
|
||||
|
||||
// Verify allowance revocation
|
||||
cy.contains('Reset USDT')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: USDT }))
|
||||
.should('deep.equal', BigNumber.from(0))
|
||||
|
||||
// Verify token approval
|
||||
cy.contains('Enable spending USDT on Uniswap')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax(USDT)
|
||||
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
it('swaps USDT with existing permit, and existing and sufficient token approval', () => {
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6))
|
||||
await hardhat.mine()
|
||||
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDT }, 1e6)
|
||||
await hardhat.mine()
|
||||
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDT })
|
||||
await hardhat.mine()
|
||||
})
|
||||
setupInputs(USDT, USDC_MAINNET)
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
initiateSwap()
|
||||
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
initiateSwap()
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
// Verifies that there is a successful swap notification.
|
||||
cy.contains('Swapped').should('exist')
|
||||
})
|
||||
|
||||
it('swaps after completing full permit2 approval process', () => {
|
||||
cy.hardhat().then(({ provider }) => {
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
})
|
||||
it('swaps when user has already approved token and permit2', () => {
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }),
|
||||
approval.setPermit2Allowance({ owner: wallet, token: DAI }),
|
||||
])
|
||||
)
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
initiateSwap()
|
||||
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
||||
cy.contains('Approved').should('exist')
|
||||
|
||||
cy.contains('Allow DAI to be used for swapping').should('exist')
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectTokenAllowanceForPermit2ToBeMax()
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
})
|
||||
// Verify transaction
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
it('swaps after handling user rejection of both approval and signature', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
const USER_REJECTION = { code: 4001 }
|
||||
cy.hardhat().then((hardhat) => {
|
||||
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction')
|
||||
tokenApprovalStub.rejects(USER_REJECTION) // reject token approval
|
||||
const permitApprovalStub = cy.stub(hardhat.provider, 'send')
|
||||
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // reject permit approval
|
||||
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
|
||||
|
||||
// Reject token approval
|
||||
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction').log(false)
|
||||
tokenApprovalStub.rejects(USER_REJECTION) // rejects token approval
|
||||
initiateSwap()
|
||||
|
||||
// tokenApprovalStub should reject here, and the modal should revert to the review state.
|
||||
cy.contains('Review swap').should('be.visible')
|
||||
|
||||
cy.then(() => {
|
||||
// The user is now allowing approval, but the permit2 signature will be rejected by the user (permitApprovalStub).
|
||||
tokenApprovalStub.restore() // allow token approval
|
||||
})
|
||||
|
||||
cy.get(getTestSelector('confirm-swap-button')).click()
|
||||
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
||||
cy.contains('Approved').should('exist')
|
||||
|
||||
// permitApprovalStub should reject here, and the modal should revert to the review state.
|
||||
// Verify token approval rejection
|
||||
cy.wrap(tokenApprovalStub).should('be.calledOnce')
|
||||
cy.contains('Review swap')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
permitApprovalStub.restore() // allow permit approval
|
||||
})
|
||||
|
||||
cy.get(getTestSelector('confirm-swap-button')).click()
|
||||
// Allow token approval
|
||||
cy.then(() => tokenApprovalStub.restore())
|
||||
|
||||
// The swap should now be able to proceed, as the permit2 signature will be accepted by the user.
|
||||
const approvalTime = Date.now()
|
||||
// Reject permit2 approval
|
||||
const permitApprovalStub = cy.stub(hardhat.provider, 'send').log(false)
|
||||
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // rejects permit approval
|
||||
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
// Verify token approval
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax(DAI)
|
||||
|
||||
expectTokenAllowanceForPermit2ToBeMax()
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
})
|
||||
})
|
||||
// Verify permit2 approval rejection
|
||||
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
|
||||
cy.contains('Review swap')
|
||||
|
||||
it('swaps with existing token approval and missing permit approval', () => {
|
||||
cy.hardhat().then(({ approval, wallet, provider }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
})
|
||||
cy.then(() => initiateSwap())
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
// Allow permit2 approval
|
||||
cy.then(() => permitApprovalStub.restore())
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
})
|
||||
})
|
||||
|
||||
it('swaps with existing permit approval and missing token approval', () => {
|
||||
cy.hardhat().then(({ approval, wallet }) => approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }))
|
||||
cy.then(() => {
|
||||
initiateSwap()
|
||||
})
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
})
|
||||
})
|
||||
|
||||
it('prompts signature when existing permit approval is expired', () => {
|
||||
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
|
||||
|
||||
cy.hardhat().then(({ approval, wallet, provider }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, expiredAllowance)
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
})
|
||||
cy.then(() => {
|
||||
initiateSwap()
|
||||
})
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
})
|
||||
})
|
||||
|
||||
it('prompts signature when existing permit approval amount is too low', () => {
|
||||
const smallAllowance = { amount: 1 }
|
||||
|
||||
cy.hardhat().then(({ approval, wallet, provider }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, smallAllowance)
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
initiateSwap()
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
// Verify permit2 approval
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
})
|
||||
|
||||
it('prompts token approval when existing approval amount is too low', () => {
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => {
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }, 1)
|
||||
})
|
||||
.then(() => {
|
||||
initiateSwap()
|
||||
const approvalTime = Date.now()
|
||||
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setPermit2Allowance({ owner: wallet, token: DAI }),
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }, 1),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
// Verify token approval
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
})
|
||||
it('prompts signature when existing permit approval is expired', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }),
|
||||
approval.setPermit2Allowance({ owner: wallet, token: DAI }, expiredAllowance),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
|
||||
it('prompts signature when existing permit approval amount is too low', () => {
|
||||
setupInputs(DAI, USDC_MAINNET)
|
||||
const smallAllowance = { amount: 1 }
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }),
|
||||
approval.setPermit2Allowance({ owner: wallet, token: DAI }, smallAllowance),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
import { ChainId, MaxUint256, UNI_ADDRESSES } from '@uniswap/sdk-core'
|
||||
|
||||
const UNI_MAINNET = UNI_ADDRESSES[ChainId.MAINNET]
|
||||
|
||||
describe('Remove Liquidity', () => {
|
||||
it('eth remove', () => {
|
||||
cy.visit('/remove/v2/ETH/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
it('loads the token pair in v2', () => {
|
||||
cy.visit(`/remove/v2/ETH/${UNI_MAINNET}`)
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('eth remove swap order', () => {
|
||||
cy.visit('/remove/v2/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'UNI')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
|
||||
it('loads the token pair in v3', () => {
|
||||
cy.visit(`/remove/1`)
|
||||
cy.get('#remove-liquidity-tokens').should('contain.text', 'UNI/ETH')
|
||||
|
||||
cy.get('#remove-pooled-tokena-symbol').should('contain.text', 'Pooled UNI')
|
||||
cy.get('#remove-pooled-tokenb-symbol').should('contain.text', 'Pooled ETH')
|
||||
})
|
||||
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||
})
|
||||
it('should redirect to error pages if pool does not exist', () => {
|
||||
// Duplicate-token v2 pools redirect to position unavailable
|
||||
cy.visit(`/remove/v2/ETH/ETH`)
|
||||
cy.contains('Position unavailable')
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
|
||||
// Single-token pools don't exist
|
||||
cy.visit('/remove/v2/ETH')
|
||||
cy.url().should('match', /\/not-found/)
|
||||
|
||||
// Nonexistent v3 pool
|
||||
cy.visit(`/remove/${MaxUint256}`)
|
||||
cy.contains('Position unavailable')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,68 +1,81 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
import { InterfaceSectionName } from '@uniswap/analytics-events'
|
||||
import { CurrencyAmount } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
|
||||
import { DAI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
|
||||
describe('Swap errors', () => {
|
||||
it('wallet rejection', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Stub the wallet to reject any transaction.
|
||||
cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled'))
|
||||
|
||||
// Attempt to swap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
|
||||
cy.contains('Review swap').should('exist')
|
||||
cy.get('body').click('topRight')
|
||||
cy.contains('Review swap').should('not.exist')
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_estimateGas')
|
||||
|
||||
// Verify rejection
|
||||
cy.contains('Review swap')
|
||||
cy.contains('Confirm swap')
|
||||
})
|
||||
})
|
||||
|
||||
it('transaction past deadline', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Set deadline to minimum. (1 minute)
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('transaction-deadline-settings')).click()
|
||||
cy.get(getTestSelector('deadline-input')).clear().type('1') // 1 minute
|
||||
|
||||
// Click outside of modal to dismiss it.
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('deadline-input')).should('not.exist')
|
||||
|
||||
// Attempt to swap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Swap submitted')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine(1, /* 10 minutes */ 1000 * 60 * 10)) // mines past the deadline
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
// Remove the transaction from the mempool, so that it doesn't fail but it is past the deadline.
|
||||
// This should result in it being removed from pending transactions, without a failure notificiation.
|
||||
const transactions = await hardhat.send('eth_pendingTransactions', [])
|
||||
await hardhat.send('hardhat_dropTransaction', [transactions[0].hash])
|
||||
// Mine past the deadline
|
||||
await hardhat.mine(1, DEFAULT_DEADLINE_FROM_NOW + 1)
|
||||
})
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction did not occur
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
|
||||
|
||||
// Verify the balance is unchanged.
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should('have.text', `Balance: ${initialBalance}`)
|
||||
cy.get(getTestSelector('popups')).should('not.contain', 'Swap failed')
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${initialBalance}`)
|
||||
getBalance(USDC_MAINNET).should('eq', initialBalance)
|
||||
})
|
||||
})
|
||||
|
||||
it('slippage failure', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
cy.hardhat({ automine: false }).then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 500e6))
|
||||
await hardhat.mine()
|
||||
await Promise.all([
|
||||
hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDC_MAINNET }),
|
||||
hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDC_MAINNET }),
|
||||
])
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
getBalance(DAI).then((initialBalance) => {
|
||||
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
|
||||
cy.hardhat().then((hardhat) => {
|
||||
const send = cy.stub(hardhat.provider, 'send').log(false)
|
||||
@@ -74,43 +87,49 @@ describe('Swap errors', () => {
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('max-slippage-settings')).click()
|
||||
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
|
||||
|
||||
// Click outside of modal to dismiss it.
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('toggle-uniswap-x-button')).click() // turn off uniswapx
|
||||
cy.get('body').click('topRight') // close modal
|
||||
cy.get(getTestSelector('slippage-input')).should('not.exist')
|
||||
|
||||
// Swap 2 times.
|
||||
const AMOUNT_TO_SWAP = 200
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.clear()
|
||||
.type(AMOUNT_TO_SWAP.toString())
|
||||
.should('have.value', AMOUNT_TO_SWAP.toString())
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.clear()
|
||||
.type(AMOUNT_TO_SWAP.toString())
|
||||
.should('have.value', AMOUNT_TO_SWAP.toString())
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
// Submit 2 transactions
|
||||
for (let i = 0; i < 2; i++) {
|
||||
cy.get('#swap-currency-input .token-amount-input').type('200').should('have.value', '200')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Swap submitted')
|
||||
if (i === 0) {
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
}
|
||||
}
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
|
||||
|
||||
// Mine transactions
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
cy.contains('Swap failed')
|
||||
|
||||
// Verify only 1 transaction occurred
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
|
||||
|
||||
// Assert that the transactions were unsuccessful by checking on-chain balance.
|
||||
getBalance(UNI_MAINNET).should('equal', initialBalance)
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
cy.get(getTestSelector('popups')).contains('Swap failed')
|
||||
getBalance(DAI).should('be.closeTo', initialBalance + 200, 1)
|
||||
})
|
||||
})
|
||||
|
||||
it('insufficient liquidity', () => {
|
||||
// The API response is too variable so stubbing a 404.
|
||||
cy.intercept('POST', 'https://api.uniswap.org/v2/quote', {
|
||||
statusCode: 404,
|
||||
fixture: 'insufficientLiquidity.json',
|
||||
})
|
||||
|
||||
cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
cy.get('#swap-currency-output .token-amount-input').type('100000000000000').should('have.value', '100000000000000') // 100 trillion
|
||||
cy.contains('Insufficient liquidity for this trade.')
|
||||
cy.get('#swap-button').should('not.exist')
|
||||
cy.get(getTestSelector(`fiat-value-${InterfaceSectionName.CURRENCY_OUTPUT_PANEL}`)).contains('-')
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,10 +5,30 @@ describe('Swap settings', () => {
|
||||
cy.visit('/swap')
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.get(getTestSelector('mobile-settings-menu')).should('not.exist')
|
||||
cy.contains('Max. slippage').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.contains('Auto Router API').should('exist')
|
||||
cy.contains('UniswapX').should('exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Settings').should('not.exist')
|
||||
})
|
||||
|
||||
it('should open the mobile settings menu', () => {
|
||||
// Set viewport to iPhone 6
|
||||
cy.viewport('iphone-6')
|
||||
cy.visit('/swap')
|
||||
|
||||
// Click the button to open the settings dialog
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click({ waitForAnimations: true })
|
||||
|
||||
// Verify the mobile settings menu and its contents
|
||||
cy.get(getTestSelector('mobile-settings-menu'))
|
||||
.should('exist')
|
||||
.within(() => {
|
||||
cy.contains('Max. slippage').should('exist')
|
||||
cy.contains('UniswapX').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.get(getTestSelector('mobile-settings-close')).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
const UNI_MAINNET = UNI[ChainId.MAINNET]
|
||||
|
||||
describe('Swap', () => {
|
||||
describe('Swap on main page', () => {
|
||||
@@ -37,33 +38,60 @@ describe('Swap', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
})
|
||||
|
||||
it('resets the dependent input when the independent input is cleared', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`)
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('have.value', '')
|
||||
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value', '')
|
||||
cy.get('#swap-currency-input .token-amount-input').clear()
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
|
||||
cy.window().trigger('blur')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
})
|
||||
|
||||
it('swaps ETH for USDC', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.visit('/swap')
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Select USDC
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
|
||||
cy.contains('USDC').click()
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
cy.get(getTestSelector('token-search-input')).type(USDC_MAINNET.address)
|
||||
cy.get(getTestSelector('common-base-USDC')).click()
|
||||
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Verify logging
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_RECEIVED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'quote_latency_milliseconds')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.gte', 0)
|
||||
})
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Review swap')
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Swap submitted')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.contains('Swap submitted').should('not.exist')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swapped')
|
||||
|
||||
// Verify the balance is updated.
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialBalance + 1}`
|
||||
)
|
||||
getBalance(USDC_MAINNET).should('eq', initialBalance + 1)
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
const finalBalance = initialBalance + 1
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(USDC_MAINNET).should('eq', finalBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -1,18 +1,16 @@
|
||||
import { CurrencyAmount, SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { ChainId, CurrencyAmount, WETH9 } from '@uniswap/sdk-core'
|
||||
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const WETH = WETH9[SupportedChainId.MAINNET]
|
||||
const WETH = WETH9[ChainId.MAINNET]
|
||||
|
||||
describe('Swap wrap', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`, { ethereum: 'hardhat' }).hardhat({
|
||||
automine: false,
|
||||
})
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`).hardhat({ automine: false })
|
||||
})
|
||||
|
||||
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01').should('have.value', '0.01')
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
|
||||
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('0.02').should('have.value', '0.02')
|
||||
@@ -20,31 +18,28 @@ describe('Swap wrap', () => {
|
||||
})
|
||||
|
||||
it('should be able to wrap ETH', () => {
|
||||
getBalance(WETH).then((initialWethBalance) => {
|
||||
getBalance(WETH).then((initialBalance) => {
|
||||
cy.contains('Enter ETH amount')
|
||||
|
||||
// Enter the amount to wrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
|
||||
// This also ensures we don't click "Wrap" before the UI has caught up.
|
||||
// Enter amount to wrap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1)
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
|
||||
|
||||
// Click the wrap button.
|
||||
// Submit transaction
|
||||
cy.contains('Wrap').click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Wrapped')
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('1.00 ETH for 1.00 WETH')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-output').should('contain', `Balance: ${initialWethBalance + 1}`)
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
getBalance(WETH).should('equal', initialWethBalance + 1)
|
||||
cy.get(getTestSelector('popups')).contains('Wrapped')
|
||||
const finalBalance = initialBalance + 1
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(WETH).should('equal', finalBalance)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,33 +49,30 @@ describe('Swap wrap', () => {
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
getBalance(WETH).then((initialWethBalance) => {
|
||||
// Swap input/output to unwrap WETH.
|
||||
getBalance(WETH).then((initialBalance) => {
|
||||
// Swap input/output to unwrap WETH
|
||||
cy.get(getTestSelector('swap-currency-button')).click()
|
||||
cy.contains('Enter WETH amount')
|
||||
|
||||
// Enter the amount to unwrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
|
||||
// This also ensures we don't click "Wrap" before the UI has caught up.
|
||||
// Enter the amount to unwrap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1)
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
|
||||
|
||||
// Click the unwrap button.
|
||||
// Submit transaction
|
||||
cy.contains('Unwrap').click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Unwrapped')
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('1.00 WETH for 1.00 ETH')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-input').should('contain', `Balance: ${initialWethBalance - 1}`)
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
getBalance(WETH).should('equal', initialWethBalance - 1)
|
||||
cy.get(getTestSelector('popups')).contains('Unwrapped')
|
||||
const finalBalance = initialBalance - 1
|
||||
cy.get('#swap-currency-input').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(WETH).should('equal', finalBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { ChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeatureFlag } from 'featureFlags'
|
||||
|
||||
import { UNI } from '../../src/constants/tokens'
|
||||
import { ARB, UNI } from '../../src/constants/tokens'
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
const UNI_MAINNET = UNI[ChainId.MAINNET]
|
||||
|
||||
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
|
||||
|
||||
@@ -14,8 +15,9 @@ describe('Token details', () => {
|
||||
|
||||
it('Uniswap token should have all information populated', () => {
|
||||
// Uniswap token
|
||||
cy.visit(`/tokens/ethereum/${UNI_ADDRESS}`)
|
||||
|
||||
cy.visit(`/tokens/ethereum/${UNI_ADDRESS}`, {
|
||||
featureFlags: [{ name: FeatureFlag.infoTDP, value: false }],
|
||||
})
|
||||
// Price chart should be filled in
|
||||
cy.get('[data-cy="chart-header"]').should('include.text', '$')
|
||||
cy.get('[data-cy="price-chart"]').should('exist')
|
||||
@@ -47,41 +49,50 @@ describe('Token details', () => {
|
||||
cy.contains(UNI_ADDRESS).should('exist')
|
||||
})
|
||||
|
||||
it('token with warning and low trading volume should have all information populated', () => {
|
||||
// Shiba predator token, low trading volume and also has warning modal
|
||||
cy.visit('/tokens/ethereum/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||
it('Uniswap token should have correct stats boxes if infoTDP flag on', () => {
|
||||
// Uniswap token
|
||||
cy.visit(`/tokens/ethereum/${UNI_ADDRESS}`, {
|
||||
featureFlags: [{ name: FeatureFlag.infoTDP, value: true }],
|
||||
})
|
||||
|
||||
// Should have missing price chart when price unavailable (expected for this token)
|
||||
if (cy.get('[data-cy="chart-header"]').contains('Price Unavailable')) {
|
||||
cy.get('[data-cy="missing-chart"]').should('exist')
|
||||
}
|
||||
// Stats should have: TVL, 24H Volume, 52W low, 52W high
|
||||
// Stats should have: TVL, FDV, market cap, 24H volume
|
||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||
cy.get('[data-cy="tvl"]').should('exist')
|
||||
cy.get('[data-cy="volume-24h"]').should('exist')
|
||||
cy.get('[data-cy="52w-low"]').should('exist')
|
||||
cy.get('[data-cy="52w-high"]').should('exist')
|
||||
cy.get('[data-cy="tvl"]').should('include.text', '$')
|
||||
cy.get('[data-cy="fdv"]').should('include.text', '$')
|
||||
cy.get('[data-cy="market-cap"]').should('include.text', '$')
|
||||
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
|
||||
})
|
||||
})
|
||||
|
||||
it('token with warning and low trading volume should have all information populated', () => {
|
||||
// Null token created for this test, 0 trading volume and has warning modal
|
||||
cy.visit('/tokens/ethereum/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||
|
||||
// Should have missing price chart when price unavailable (expected for this token)
|
||||
if (cy.get('[data-cy="chart-header"]').contains('Price unavailable')) {
|
||||
cy.get('[data-cy="missing-chart"]').should('exist')
|
||||
}
|
||||
|
||||
// Stats should not exist
|
||||
cy.get(getTestSelector('token-details-stats')).should('not.exist')
|
||||
|
||||
// About section should have description of token
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.contains('QOM is the Shiba Predator').should('exist')
|
||||
cy.contains('No token information available').should('exist')
|
||||
|
||||
// Links section should link out to Etherscan, More analytics, Website, Twitter
|
||||
// Links section should link out to Etherscan, More analytics
|
||||
cy.get('[data-cy="resources-container"]').within(() => {
|
||||
cy.contains('Etherscan')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', 'etherscan.io/address/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||
.and('include', 'etherscan.io/address/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||
cy.contains('More analytics')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', 'info.uniswap.org/#/tokens/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||
cy.contains('Website').should('have.attr', 'href').and('include', 'qom')
|
||||
cy.contains('Twitter').should('have.attr', 'href').and('include', 'twitter.com/ShibaPredator1')
|
||||
.and('include', 'info.uniswap.org/#/tokens/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||
})
|
||||
|
||||
// Contract address should be displayed
|
||||
cy.contains('0xa71d0588EAf47f12B13cF8eC750430d21DF04974').should('exist')
|
||||
cy.contains('0x1eFBB78C8b917f67986BcE54cE575069c0143681').should('exist')
|
||||
|
||||
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
|
||||
cy.get('[data-cy="token-safety-message"]')
|
||||
@@ -93,9 +104,7 @@ describe('Token details', () => {
|
||||
beforeEach(() => {
|
||||
// On mobile widths, we just link back to /swap instead of rendering the swap component.
|
||||
cy.viewport(1200, 800)
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
}).then(() => {
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`).then(() => {
|
||||
cy.wait('@eth_blockNumber')
|
||||
cy.scrollTo('top')
|
||||
})
|
||||
@@ -110,12 +119,12 @@ describe('Token details', () => {
|
||||
|
||||
it('should automatically navigate to the new TDP', () => {
|
||||
cy.get(`#swap-currency-output .open-currency-select-button`).click()
|
||||
cy.contains('WETH').click()
|
||||
cy.get('[data-reach-dialog-content]').contains('WETH').click()
|
||||
cy.url().should('include', `${WETH9[1].address}`)
|
||||
cy.url().should('not.include', `${UNI_MAINNET.address}`)
|
||||
})
|
||||
|
||||
it.only('should not share swap state with the main swap page', () => {
|
||||
it('should not share swap state with the main swap page', () => {
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||
cy.get(`#swap-currency-input .open-currency-select-button`).click()
|
||||
cy.contains('WETH').click()
|
||||
@@ -145,12 +154,13 @@ describe('Token details', () => {
|
||||
})
|
||||
|
||||
it('should show a L2 token even if the user is connected to a different network', () => {
|
||||
cy.visit('/tokens', { ethereum: 'hardhat' })
|
||||
cy.visit('/tokens')
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
|
||||
cy.get(getTestSelector('token-table-row-ARB')).click()
|
||||
cy.get(getTestSelector(`token-table-row-${ARB.address.toLowerCase()}`)).click()
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'ARB')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).should('be.disabled')
|
||||
cy.contains('Connect to Arbitrum').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,83 +1,30 @@
|
||||
describe.skip('Token explore filter', () => {
|
||||
before(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('should filter correctly by uni search term', () => {
|
||||
describe('Token explore filter', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/tokens')
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
const filteredByUni = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('uni'))
|
||||
cy.wrap(filteredByUni).as('filteredByUni')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||
.clear()
|
||||
.type('uni')
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||
cy.get('@filteredByUni').then((filteredByUni) => {
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
expect(tokenNames.length).to.equal(filteredByUni.length)
|
||||
tokenNames.forEach((tokenName) => {
|
||||
expect(filteredByUni).to.include(tokenName)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function aliasFilteredTokens(filter: string) {
|
||||
cy.get('[data-cy="token-name"]').then((tokens) => {
|
||||
cy.wrap(Array.from(tokens).filter((token) => token.innerText.toLowerCase().includes(filter))).as('filteredTokens')
|
||||
})
|
||||
}
|
||||
|
||||
function searchFor(filter: string) {
|
||||
cy.get('[data-cy="explore-tokens-search-input"]').clear().type(filter).type('{enter}')
|
||||
// wait for it to finish the filtered render
|
||||
cy.get('[data-cy="token-name"]').first().contains(filter, {
|
||||
matchCase: false,
|
||||
})
|
||||
}
|
||||
|
||||
it('should filter correctly by dao search term', () => {
|
||||
cy.visit('/tokens')
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
const filteredByDao = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('dao'))
|
||||
cy.wrap(filteredByDao).as('filteredByDao')
|
||||
})
|
||||
aliasFilteredTokens('dao')
|
||||
searchFor('dao')
|
||||
|
||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||
.clear()
|
||||
.type('dao')
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||
cy.get('@filteredByDao').then((filteredByDao) => {
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
expect(tokenNames.length).to.equal(filteredByDao.length)
|
||||
tokenNames.forEach((tokenName) => {
|
||||
expect(filteredByDao).to.include(tokenName)
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('@filteredTokens').then((filteredTokens) => {
|
||||
cy.get('[data-cy="token-name"]').then((tokens) => {
|
||||
cy.wrap(Array.from(tokens)).should('deep.equal', Array.from(filteredTokens))
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter correctly by ax search term', () => {
|
||||
cy.visit('/tokens')
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
const filteredByAx = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('ax'))
|
||||
cy.wrap(filteredByAx).as('filteredByAx')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||
.clear()
|
||||
.type('ax')
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||
cy.get('@filteredByAx').then((filteredByAx) => {
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
expect(tokenNames.length).to.equal(filteredByAx.length)
|
||||
tokenNames.forEach((tokenName) => {
|
||||
expect(filteredByAx).to.include(tokenName)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,11 +10,11 @@ describe('Token explore', () => {
|
||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.greaterThan', 0)
|
||||
// check sorted svg icon is present in volume cell, since tokens are sorted by volume by default
|
||||
cy.get(getTestSelector('header-row')).find(getTestSelector('volume-cell')).find('svg').should('exist')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('name-cell')).should('include.text', 'Ether')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('volume-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('price-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('tvl-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-ETH'))
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('name-cell')).should('include.text', 'Ether')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('volume-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('price-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('tvl-cell')).should('include.text', '$')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE'))
|
||||
.find(getTestSelector('percent-change-cell'))
|
||||
.should('include.text', '%')
|
||||
cy.get(getTestSelector('header-row')).find(getTestSelector('price-cell')).click()
|
||||
@@ -24,24 +24,24 @@ describe('Token explore', () => {
|
||||
it('should update when time window toggled', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelector('time-selector')).should('contain', '1D')
|
||||
cy.get(getTestSelector('token-table-row-ETH'))
|
||||
cy.get(getTestSelector('token-table-row-NATIVE'))
|
||||
.find(getTestSelector('volume-cell'))
|
||||
.then(function ($elem) {
|
||||
cy.wrap($elem.text()).as('dailyEthVol')
|
||||
})
|
||||
cy.get(getTestSelector('time-selector')).click()
|
||||
cy.get(getTestSelector('1Y')).click()
|
||||
cy.get(getTestSelector('token-table-row-ETH'))
|
||||
cy.get(getTestSelector('token-table-row-NATIVE'))
|
||||
.find(getTestSelector('volume-cell'))
|
||||
.then(function ($elem) {
|
||||
cy.wrap($elem.text()).as('yearlyEthVol')
|
||||
})
|
||||
expect(cy.get('@dailyEthVol')).to.not.equal(cy.get('@yearlyEthVol'))
|
||||
cy.get('@dailyEthVol').should('not.equal', cy.get('@yearlyEthVol'))
|
||||
})
|
||||
|
||||
it('should navigate to token detail page when row clicked', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).click()
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).click()
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||
cy.get(getTestSelector('token-info-container')).should('exist')
|
||||
@@ -53,13 +53,13 @@ describe('Token explore', () => {
|
||||
it('should update when global network changed', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Ethereum')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).should('exist')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).should('exist')
|
||||
|
||||
// note: cannot switch global chain via UI because we cannot approve the network switch
|
||||
// in metamask modal using plain cypress. this is a workaround.
|
||||
cy.visit('/tokens/polygon')
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Polygon')
|
||||
cy.get(getTestSelector('token-table-row-MATIC')).should('exist')
|
||||
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('name-cell')).should('include.text', 'Matic')
|
||||
})
|
||||
|
||||
it('should update when token explore table network changed', () => {
|
||||
@@ -67,8 +67,6 @@ describe('Token explore', () => {
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-optimism')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
||||
cy.reload()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
||||
cy.get(getTestSelector('chain-selector-logo')).invoke('attr', 'alt').should('eq', 'Ethereum')
|
||||
cy.get(getTestSelector('chain-selector-logo')).find('title').should('include.text', 'Ethereum logo')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,64 +1,72 @@
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
import { UNI } from 'constants/tokens'
|
||||
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const UNI_ADDRESS = UNI[ChainId.MAINNET].address.toLowerCase()
|
||||
|
||||
describe('Universal search bar', () => {
|
||||
before(() => {
|
||||
function openSearch() {
|
||||
// can't just type "/" because on mobile it doesn't respond to that
|
||||
cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="magnifying-icon"]')
|
||||
.parent()
|
||||
.then(($navIcon) => {
|
||||
$navIcon.click()
|
||||
})
|
||||
})
|
||||
|
||||
it('should yield clickable result for regular token or nft collection search term', () => {
|
||||
// Search for uni token by name.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]')
|
||||
function getSearchBar() {
|
||||
return cy.get('[data-cy="search-bar-input"]').last()
|
||||
}
|
||||
|
||||
it('should yield clickable result that is then added to recent searches', () => {
|
||||
// Search for UNI token by name.
|
||||
openSearch()
|
||||
getSearchBar().clear().type('uni')
|
||||
|
||||
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||
.should('contain.text', 'Uniswap')
|
||||
.and('contain.text', 'UNI')
|
||||
.and('contain.text', '$')
|
||||
.and('contain.text', '%')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]').first().click()
|
||||
.click()
|
||||
cy.location('pathname').should('equal', '/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984')
|
||||
|
||||
cy.get('div').contains('Uniswap').should('exist')
|
||||
// Stats should have: TVL, 24H Volume, 52W low, 52W high.
|
||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||
cy.get('[data-cy="tvl"]').should('include.text', '$')
|
||||
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
|
||||
cy.get('[data-cy="52w-low"]').should('include.text', '$')
|
||||
cy.get('[data-cy="52w-high"]').should('include.text', '$')
|
||||
})
|
||||
|
||||
// About section should have description of token.
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.contains('UNI is the governance token for Uniswap').should('exist')
|
||||
})
|
||||
|
||||
it.skip('should show recent tokens and popular tokens with empty search term', () => {
|
||||
cy.get('[data-cy="magnifying-icon"]')
|
||||
.parent()
|
||||
.then(($navIcon) => {
|
||||
$navIcon.click()
|
||||
})
|
||||
// Recently searched UNI token should exist.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear()
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Recent searches')
|
||||
.find('[data-cy="searchbar-token-row-UNI"]')
|
||||
openSearch()
|
||||
cy.get(getTestSelector('searchbar-dropdown'))
|
||||
.contains(getTestSelector('searchbar-dropdown'), 'Recent searches')
|
||||
.find(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||
.should('exist')
|
||||
|
||||
// Most popular 3 tokens should be shown.
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Popular tokens')
|
||||
.find('[data-cy^="searchbar-token-row"]')
|
||||
.its('length')
|
||||
.should('be.eq', 3)
|
||||
})
|
||||
|
||||
it.skip('should show blocked badge when blocked token is searched for', () => {
|
||||
// Search for mTSLA, which is a blocked token.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
|
||||
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
|
||||
})
|
||||
it(
|
||||
'should go to the selected result when recent results are shown',
|
||||
// this test is experiencing flake despite being correct, i can see the right value in DOM
|
||||
// but for some reason cypress doesn't find it, so adding retries for now :/
|
||||
{
|
||||
// @ts-ignore see https://uniswapteam.slack.com/archives/C047U65H422/p1691455547556309
|
||||
// basically cypress has bad types due to overlap with jest and you just have to deal with it
|
||||
// i tried removing jest types but still happens
|
||||
retries: {
|
||||
runMode: 3,
|
||||
openMode: 3,
|
||||
},
|
||||
},
|
||||
() => {
|
||||
// Seed recent results with UNI.
|
||||
openSearch()
|
||||
getSearchBar().type('uni')
|
||||
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||
getSearchBar().clear().type('{esc}')
|
||||
|
||||
// Search a different token by name.
|
||||
openSearch()
|
||||
getSearchBar().type('eth')
|
||||
cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE'))
|
||||
|
||||
// Validate that we go to the searched/selected result.
|
||||
cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE')).click()
|
||||
cy.url().should('contain', 'tokens/ethereum/NATIVE')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
41
cypress/e2e/wallet-connection/connect.test.ts
Normal file
41
cypress/e2e/wallet-connection/connect.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getTestSelector } from '../../utils'
|
||||
import { DISCONNECTED_WALLET_USER_STATE } from '../../utils/user-state'
|
||||
|
||||
describe('disconnect wallet', () => {
|
||||
it('should clear state', () => {
|
||||
cy.visit('/swap')
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
|
||||
// Verify wallet is connected
|
||||
cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)))
|
||||
cy.contains('Balance:')
|
||||
|
||||
// Disconnect the wallet
|
||||
cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)).click())
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).contains('Disconnect')
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
|
||||
// Verify wallet has disconnected
|
||||
cy.contains('Connect a wallet').should('exist')
|
||||
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect')
|
||||
cy.contains('Connect wallet')
|
||||
|
||||
// Verify swap input is cleared
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('connect wallet', () => {
|
||||
it('should load state', () => {
|
||||
cy.visit('/swap', { userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
|
||||
// Connect the wallet
|
||||
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect').click()
|
||||
cy.contains('MetaMask').click()
|
||||
|
||||
// Verify wallet is connected
|
||||
cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)))
|
||||
cy.contains('Balance:')
|
||||
})
|
||||
})
|
||||
144
cypress/e2e/wallet-connection/switch-network.test.ts
Normal file
144
cypress/e2e/wallet-connection/switch-network.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { createDeferredPromise } from '../../../src/test-utils/promise'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
function waitsForActiveChain(chain: string) {
|
||||
cy.get(getTestSelector('chain-selector-logo')).find('title').should('include.text', `${chain} logo`)
|
||||
}
|
||||
|
||||
function switchChain(chain: string) {
|
||||
cy.get(getTestSelector('chain-selector')).eq(1).click()
|
||||
cy.contains(chain).click()
|
||||
}
|
||||
|
||||
describe('network switching', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
})
|
||||
|
||||
function rejectsNetworkSwitchWith(rejection: unknown) {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Reject network switch
|
||||
const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
|
||||
sendStub.withArgs('wallet_switchEthereumChain').rejects(rejection)
|
||||
sendStub.callThrough() // allows other calls to return non-stubbed values
|
||||
})
|
||||
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify rejected network switch
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Ethereum')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
}
|
||||
|
||||
it('should not display message on user rejection', () => {
|
||||
const USER_REJECTION = { code: 4001 }
|
||||
rejectsNetworkSwitchWith(USER_REJECTION)
|
||||
cy.get(getTestSelector('popups')).should('not.contain', 'Failed to switch networks')
|
||||
})
|
||||
|
||||
it('should display message on unknown error', () => {
|
||||
rejectsNetworkSwitchWith(new Error('Unknown error'))
|
||||
cy.get(getTestSelector('popups')).contains('Failed to switch networks')
|
||||
})
|
||||
|
||||
it('should add missing chain', () => {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// https://docs.metamask.io/guide/rpc-api.html#unrestricted-methods
|
||||
const CHAIN_NOT_ADDED = { code: 4902 } // missing message in useSelectChain
|
||||
|
||||
// Reject network switch with CHAIN_NOT_ADDED
|
||||
const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
|
||||
let added = false
|
||||
sendStub
|
||||
.withArgs('wallet_switchEthereumChain')
|
||||
.callsFake(() => (added ? Promise.resolve(null) : Promise.reject(CHAIN_NOT_ADDED)))
|
||||
sendStub.withArgs('wallet_addEthereumChain').callsFake(() => {
|
||||
added = true
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
sendStub.callThrough() // allows other calls to return non-stubbed values
|
||||
})
|
||||
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify the network was added
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_addEthereumChain', [
|
||||
{
|
||||
blockExplorerUrls: ['https://polygonscan.com/'],
|
||||
chainId: '0x89',
|
||||
chainName: 'Polygon',
|
||||
nativeCurrency: { name: 'Polygon Matic', symbol: 'MATIC', decimals: 18 },
|
||||
rpcUrls: ['https://polygon-rpc.com/'],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should not disconnect while switching', () => {
|
||||
const promise = createDeferredPromise()
|
||||
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Reject network switch with CHAIN_NOT_ADDED
|
||||
const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
|
||||
sendStub.withArgs('wallet_switchEthereumChain').returns(promise)
|
||||
sendStub.callThrough() // allows other calls to return non-stubbed values
|
||||
})
|
||||
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify there is no disconnection
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
|
||||
cy.contains('Connecting to Polygon')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('be.disabled')
|
||||
promise.resolve()
|
||||
})
|
||||
|
||||
it('should switch networks', () => {
|
||||
// Select an output currency
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('USDC').click()
|
||||
|
||||
// Populate input/output fields
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Switch network
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify network switch
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.url().should('contain', 'chain=polygon')
|
||||
|
||||
// Verify that the input/output fields were reset
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'MATIC')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
|
||||
})
|
||||
})
|
||||
|
||||
describe('network switching from URL param', () => {
|
||||
it('should switch network from URL param', () => {
|
||||
cy.visit('/swap?chain=polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
})
|
||||
|
||||
it('should be able to switch network after loading from URL param', () => {
|
||||
cy.visit('/swap?chain=polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
|
||||
// switching to another chain clears query param
|
||||
switchChain('Ethereum')
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Ethereum')
|
||||
cy.url().should('not.contain', 'chain=polygon')
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,10 @@
|
||||
import { FeatureFlag } from 'featureFlags'
|
||||
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Wallet Dropdown', () => {
|
||||
function itShouldChangeTheTheme() {
|
||||
it('should change the theme', () => {
|
||||
function itChangesTheme() {
|
||||
it('should change theme', () => {
|
||||
cy.get(getTestSelector('theme-lightmode')).click()
|
||||
|
||||
cy.get(getTestSelector('theme-lightmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
@@ -21,13 +23,21 @@ describe('Wallet Dropdown', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function itShouldChangeTheLanguage() {
|
||||
it('should select a language', () => {
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
|
||||
function itChangesLocale({ featureFlag = false }: { featureFlag?: boolean } = {}) {
|
||||
it('should change locale', () => {
|
||||
cy.contains('Uniswap available in: English').should('not.exist')
|
||||
|
||||
if (featureFlag) {
|
||||
cy.get(getTestSelector('language-settings-button')).click()
|
||||
}
|
||||
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
|
||||
cy.location('search').should('match', /\?lng=af-ZA$/)
|
||||
cy.contains('Uniswap available in: English')
|
||||
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
cy.location('search').should('match', /\?lng=en-US$/)
|
||||
cy.contains('Uniswap available in: English').should('not.exist')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,19 +47,76 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
itShouldChangeTheTheme()
|
||||
itShouldChangeTheLanguage()
|
||||
itChangesTheme()
|
||||
itChangesLocale()
|
||||
|
||||
it('should not show buy crypto button in uk', () => {
|
||||
cy.document().then((doc) => {
|
||||
const meta = document.createElement('meta')
|
||||
meta.setAttribute('property', 'x:blocked-paths')
|
||||
meta.setAttribute('content', '/,/nfts,/buy')
|
||||
doc.head.appendChild(meta)
|
||||
})
|
||||
cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('do not render buy button when /buy is blocked', () => {
|
||||
beforeEach(() => {
|
||||
cy.document().then((doc) => {
|
||||
const meta = document.createElement('meta')
|
||||
meta.setAttribute('property', 'x:blocked-paths')
|
||||
meta.setAttribute('content', '/buy')
|
||||
doc.head.appendChild(meta)
|
||||
})
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
|
||||
it('should not render buy button', () => {
|
||||
cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('should change locale with feature flag', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
itChangesLocale({ featureFlag: true })
|
||||
})
|
||||
|
||||
describe('testnet toggle', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
it('should toggle testnet visibility', () => {
|
||||
cy.get(getTestSelector('chain-selector')).last().click()
|
||||
cy.get(getTestSelector('chain-selector-options')).should('not.contain.text', 'Sepolia')
|
||||
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get('#testnets-toggle').click()
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
cy.get(getTestSelector('chain-selector')).last().click()
|
||||
cy.get(getTestSelector('chain-selector-options')).should('contain.text', 'Sepolia')
|
||||
})
|
||||
})
|
||||
|
||||
describe('disconnected', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
// click twice, first time to show confirmation, second to confirm
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect')
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
itShouldChangeTheTheme()
|
||||
itShouldChangeTheLanguage()
|
||||
itChangesTheme()
|
||||
itChangesLocale()
|
||||
})
|
||||
|
||||
describe('with color theme', () => {
|
||||
@@ -76,7 +143,7 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(152, 161, 192)')
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(155, 155, 155)')
|
||||
})
|
||||
|
||||
it('should properly use light system theme when auto theme setting is selected', () => {
|
||||
@@ -84,7 +151,7 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(125, 125, 125)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -105,4 +172,34 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('local currency', () => {
|
||||
it('loads local currency from the query param', () => {
|
||||
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.contains('USD')
|
||||
|
||||
cy.visit('/?cur=AUD', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.contains('AUD')
|
||||
})
|
||||
|
||||
it('loads local currency from menu', () => {
|
||||
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.contains('USD')
|
||||
|
||||
cy.get(getTestSelector('local-currency-settings-button')).click()
|
||||
cy.get(getTestSelector('wallet-local-currency-item')).contains('AUD').click({ force: true })
|
||||
cy.location('search').should('match', /\?cur=AUD$/)
|
||||
cy.contains('AUD')
|
||||
|
||||
cy.get(getTestSelector('wallet-local-currency-item')).contains('USD').click({ force: true })
|
||||
cy.location('search').should('match', /\?cur=USD$/)
|
||||
cy.contains('USD')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
5
cypress/fixtures/insufficientLiquidity.json
Normal file
5
cypress/fixtures/insufficientLiquidity.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"errorCode": "QUOTE_ERROR",
|
||||
"detail": "No quotes available",
|
||||
"id": "63363cc1-d474-4584-b386-7c356814b79f"
|
||||
}
|
||||
12
cypress/fixtures/mini-portfolio/empty_activity.json
Normal file
12
cypress/fixtures/mini-portfolio/empty_activity.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"data": {
|
||||
"portfolios": [
|
||||
{
|
||||
"id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==",
|
||||
"assetActivities": [],
|
||||
"__typename": "Portfolio"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
580
cypress/fixtures/mini-portfolio/full_activity.json
Normal file
580
cypress/fixtures/mini-portfolio/full_activity.json
Normal file
@@ -0,0 +1,580 @@
|
||||
{
|
||||
"data": {
|
||||
"portfolios": [
|
||||
{
|
||||
"id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==",
|
||||
"assetActivities": [
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnM09EQm1NamcwTURSak1qRXpPRGd6TVRVM00yRXdOakJtTVRaaE1UQTNaV0ZtTW1Jd01qazFZbUZqTmpjNU5tUm1ZamN5TW1WbVl6VmpPVE5tTmpRM1h6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEQXdNREF3TURBek1HWTBPV0ptTW1Vd01ESmxOakJqTm1Wa01UWTJNV1ppTWpNME5tUTRPREk9",
|
||||
"timestamp": 1684364195,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"__typename": "TransactionDetails",
|
||||
"id": "VHJhbnNhY3Rpb246MHg3ODBmMjg0MDRjMjEzODgzMTU3M2EwNjBmMTZhMTA3ZWFmMmIwMjk1YmFjNjc5NmRmYjcyMmVmYzVjOTNmNjQ3XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDAwMDAwMDAzMGY0OWJmMmUwMDJlNjBjNmVkMTY2MWZiMjM0NmQ4ODI=",
|
||||
"type": "UNKNOWN",
|
||||
"blockNumber": 17282434,
|
||||
"hash": "0x780f28404c2138831573a060f16a107eaf2b0295bac6796dfb722efc5c93f647",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0x000000030f49bf2e002e60c6ed1661fb2346d882",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 465,
|
||||
"assetChanges": []
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhobFl6QTROMkpoTjJJMk4yUTFPVEEwTVdNMU5XTXdObU16TkdNNVlXVmhPVEUxTkRreVpUYzRNRFl4WldRd016TTBNMlprWmprMU1qa3dPR1U0WTJSa1h6QjRaR0psWmpNM05HWmtaamhrTnpNMVpUYzFPRGxoT1dFNVpUSmpOV0V3T1RGbFlqSmtZbVUxTjE4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684364135,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"__typename": "TransactionDetails",
|
||||
"id": "VHJhbnNhY3Rpb246MHhlYzA4N2JhN2I2N2Q1OTA0MWM1NWMwNmMzNGM5YWVhOTE1NDkyZTc4MDYxZWQwMzM0M2ZkZjk1MjkwOGU4Y2RkXzB4ZGJlZjM3NGZkZjhkNzM1ZTc1ODlhOWE5ZTJjNWEwOTFlYjJkYmU1N18weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17282429,
|
||||
"hash": "0xec087ba7b67d59041c55c06c34c9aea915492e78061ed03343fdf952908e8cdd",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0xdbef374fdf8d735e7589a9a9e2c5a091eb2dbe57",
|
||||
"nonce": 66,
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGRiZWYzNzRmZGY4ZDczNWU3NTg5YTlhOWUyYzVhMDkxZWIyZGJlNTdfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4ZWMwODdiYTdiNjdkNTkwNDFjNTVjMDZjMzRjOWFlYTkxNTQ5MmU3ODA2MWVkMDMzNDNmZGY5NTI5MDhlOGNkZA==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.001",
|
||||
"sender": "0xdbef374fdf8d735e7589a9a9e2c5a091eb2dbe57",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjEuODI5NjcwMDAwMDAwMDAwMV9VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 1.8296700000000001,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhoaE9URXdPVFEwT1Rka01UVmpNelpsWWprd1pXUXpZVEkwWW1Wa09ESTBOalpqWmpKaU9URXpNV1l4WkRVMk1EUmlNelppWW1aallqRTBOMkUzTURnNFh6QjRaV1JoTldVeE9ERXhORFppTVdZNVlUZG1OREJtT0RWak1HUmhNek0wT1RNNE5ESXdaRFV4TkY4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684319903,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHhhOTEwOTQ0OTdkMTVjMzZlYjkwZWQzYTI0YmVkODI0NjZjZjJiOTEzMWYxZDU2MDRiMzZiYmZjYjE0N2E3MDg4XzB4ZWRhNWUxODExNDZiMWY5YTdmNDBmODVjMGRhMzM0OTM4NDIwZDUxNF8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17278819,
|
||||
"hash": "0xa91094497d15c36eb90ed3a24bed82466cf2b9131f1d5604b36bbfcb147a7088",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0xeda5e181146b1f9a7f40f85c0da334938420d514",
|
||||
"nonce": 5,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGVkYTVlMTgxMTQ2YjFmOWE3ZjQwZjg1YzBkYTMzNDkzODQyMGQ1MTRfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4YTkxMDk0NDk3ZDE1YzM2ZWI5MGVkM2EyNGJlZDgyNDY2Y2YyYjkxMzFmMWQ1NjA0YjM2YmJmY2IxNDdhNzA4OA==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.15",
|
||||
"sender": "0xeda5e181146b1f9a7f40f85c0da334938420d514",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjI3NC40NTA1X1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 274.4505,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnMFkyUm1Nell6T0dRME1ERXdOV1U1WkRZMVlUZGxObUV6WVdFMlpHTXpNREZpWVRNNVpHTXlNV1ppT0dGaE5USTBNVFppT1ROaE5tWXhOVEUwTWpReVh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VHUmxOR1F6WVRJME5XUXlZall4WW1WaE1tTmlaREl4TmpVNE1XVXlaR1ZrTmpWbFl6azFNRFE9",
|
||||
"timestamp": 1684319903,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg0Y2RmMzYzOGQ0MDEwNWU5ZDY1YTdlNmEzYWE2ZGMzMDFiYTM5ZGMyMWZiOGFhNTI0MTZiOTNhNmYxNTE0MjQyXzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weGRlNGQzYTI0NWQyYjYxYmVhMmNiZDIxNjU4MWUyZGVkNjVlYzk1MDQ=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17278819,
|
||||
"hash": "0x4cdf3638d40105e9d65a7e6a3aa6dc301ba39dc21fb8aa52416b93a6f1514242",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xde4d3a245d2b61bea2cbd216581e2ded65ec9504",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 464,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHhkZTRkM2EyNDVkMmI2MWJlYTJjYmQyMTY1ODFlMmRlZDY1ZWM5NTA0XzB4NGNkZjM2MzhkNDAxMDVlOWQ2NWE3ZTZhM2FhNmRjMzAxYmEzOWRjMjFmYjhhYTUyNDE2YjkzYTZmMTUxNDI0Mg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.00134999999999999",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0xde4d3a245d2b61bea2cbd216581e2ded65ec9504",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjIuNDcwMDU0NDk5OTk5OTgyX1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 2.470054499999982,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnM04yRXhPVGRoWmpjek9EUXpNRFk0WVRCaVlqUmlaV1V6WWpabFptWmxaakpsTkdZMFptTXlNR1UxWVRGbVltSTBOak14WXpoak1UQTROMk15WWpjM1h6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VHWTFZekZoTnpCbU5qY3pPV0k1TW1ZNU4yTmtOVE5qTXpFMk1ETTJNbU14TXpBMVpUa3hZVGc9",
|
||||
"timestamp": 1684202579,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg3N2ExOTdhZjczODQzMDY4YTBiYjRiZWUzYjZlZmZlZjJlNGY0ZmMyMGU1YTFmYmI0NjMxYzhjMTA4N2MyYjc3XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weGY1YzFhNzBmNjczOWI5MmY5N2NkNTNjMzE2MDM2MmMxMzA1ZTkxYTg=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17269191,
|
||||
"hash": "0x77a197af73843068a0bb4bee3b6effef2e4f4fc20e5a1fbb4631c8c1087c2b77",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf5c1a70f6739b92f97cd53c3160362c1305e91a8",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 463,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHhmNWMxYTcwZjY3MzliOTJmOTdjZDUzYzMxNjAzNjJjMTMwNWU5MWE4XzB4NzdhMTk3YWY3Mzg0MzA2OGEwYmI0YmVlM2I2ZWZmZWYyZTRmNGZjMjBlNWExZmJiNDYzMWM4YzEwODdjMmI3Nw==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.001216034894406018",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0xf5c1a70f6739b92f97cd53c3160362c1305e91a8",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjIuMjI0OTQyNTY1MjQ3ODU5X1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 2.224942565247859,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnMlpXSmtZbVJrTVRZMk0yVmxNV1ZrT0RVeE16TXlZelUyWmpkall6YzJaV1ZqTVROaE5qTm1PVEkxTldOa1ltWXlZVEUxWWpReFl6azBPVGhrWW1Wa1h6QjROREZpTXpBNU1qTTJZemczWWpGaVl6Wm1ZVGhsWWpnMk5UZ3pNMlUwTkRFMU9HWmhPVGt4WVY4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684202579,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg2ZWJkYmRkMTY2M2VlMWVkODUxMzMyYzU2ZjdjYzc2ZWVjMTNhNjNmOTI1NWNkYmYyYTE1YjQxYzk0OThkYmVkXzB4NDFiMzA5MjM2Yzg3YjFiYzZmYThlYjg2NTgzM2U0NDE1OGZhOTkxYV8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17269191,
|
||||
"hash": "0x6ebdbdd1663ee1ed851332c56f7cc76eec13a63f9255cdbf2a15b41c9498dbed",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0x41b309236c87b1bc6fa8eb865833e44158fa991a",
|
||||
"nonce": 111266,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDQxYjMwOTIzNmM4N2IxYmM2ZmE4ZWI4NjU4MzNlNDQxNThmYTk5MWFfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4NmViZGJkZDE2NjNlZTFlZDg1MTMzMmM1NmY3Y2M3NmVlYzEzYTYzZjkyNTVjZGJmMmExNWI0MWM5NDk4ZGJlZA==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.00275365",
|
||||
"sender": "0x41b309236c87b1bc6fa8eb865833e44158fa991a",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjUuMDM4MjcwNzk1NV9VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 5.0382707955,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNU5EUmlNR00wTVROa1l6QmpNekU0TUdFelkyTTNZakUyT1RCbVlqZzBNRFExWm1FME9UTXpObUV5WmprNE16VmpORFpqTURsak1UY3lObUUzTm1aalh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEWXlNakJsTURoak9XUTJNMkZpTjJKaE1tVTFOalk0TXpsbU5ESTVaV1ZsWm1VeE9UbGlOMlU9",
|
||||
"timestamp": 1684171943,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg5NDRiMGM0MTNkYzBjMzE4MGEzY2M3YjE2OTBmYjg0MDQ1ZmE0OTMzNmEyZjk4MzVjNDZjMDljMTcyNmE3NmZjXzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDYyMjBlMDhjOWQ2M2FiN2JhMmU1NjY4MzlmNDI5ZWVlZmUxOTliN2U=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17266680,
|
||||
"hash": "0x944b0c413dc0c3180a3cc7b1690fb84045fa49336a2f9835c46c09c1726a76fc",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 462,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHg2MjIwZTA4YzlkNjNhYjdiYTJlNTY2ODM5ZjQyOWVlZWZlMTk5YjdlXzB4OTQ0YjBjNDEzZGMwYzMxODBhM2NjN2IxNjkwZmI4NDA0NWZhNDkzMzZhMmY5ODM1YzQ2YzA5YzE3MjZhNzZmYw==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.003476850926189204",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjYuMzYxNDg5ODM0MTIwNjAxX1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 6.361489834120601,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhneE0yRTRNRGxsT1RZd05USmhOVGxrWlRjNU56WXhObVZrTlRjME1qTTVNakV3WkRJMVpUY3hNRGhqTkRjek9EbG1NbVJoTnpjeU5qTXhZbVZpTUdZMlh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEWXlNakJsTURoak9XUTJNMkZpTjJKaE1tVTFOalk0TXpsbU5ESTVaV1ZsWm1VeE9UbGlOMlU9",
|
||||
"timestamp": 1684171943,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHgxM2E4MDllOTYwNTJhNTlkZTc5NzYxNmVkNTc0MjM5MjEwZDI1ZTcxMDhjNDczODlmMmRhNzcyNjMxYmViMGY2XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDYyMjBlMDhjOWQ2M2FiN2JhMmU1NjY4MzlmNDI5ZWVlZmUxOTliN2U=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17266680,
|
||||
"hash": "0x13a809e96052a59de797616ed574239210d25e7108c47389f2da772631beb0f6",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 461,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHg2MjIwZTA4YzlkNjNhYjdiYTJlNTY2ODM5ZjQyOWVlZWZlMTk5YjdlXzB4MTNhODA5ZTk2MDUyYTU5ZGU3OTc2MTZlZDU3NDIzOTIxMGQyNWU3MTA4YzQ3Mzg5ZjJkYTc3MjYzMWJlYjBmNg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.000900000000000318",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjEuNjQ2NzAzMDAwMDAwNTgxOF9VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 1.6467030000005818,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhobFkyRTJNVEZrTlRVME1EZGxPVGt6WlRFM1lqWmtaVGhpWVRJMFlqWXlOREpqWVRSbFlXWTBORGN3TkRKbFpHRmtNRFE0TTJNNFptSTJabUU0WkRJNVh6QjROekU0WVRVeE5ESXhNR0kwTnpWaU9USXhOVGd6WldGaU5ERXlaV0ptTUdaaVlXUm1NMkl6T1Y4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684171931,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHhlY2E2MTFkNTU0MDdlOTkzZTE3YjZkZThiYTI0YjYyNDJjYTRlYWY0NDcwNDJlZGFkMDQ4M2M4ZmI2ZmE4ZDI5XzB4NzE4YTUxNDIxMGI0NzViOTIxNTgzZWFiNDEyZWJmMGZiYWRmM2IzOV8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17266679,
|
||||
"hash": "0xeca611d55407e993e17b6de8ba24b6242ca4eaf447042edad0483c8fb6fa8d29",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0x718a514210b475b921583eab412ebf0fbadf3b39",
|
||||
"nonce": 92,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDcxOGE1MTQyMTBiNDc1YjkyMTU4M2VhYjQxMmViZjBmYmFkZjNiMzlfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4ZWNhNjExZDU1NDA3ZTk5M2UxN2I2ZGU4YmEyNGI2MjQyY2E0ZWFmNDQ3MDQyZWRhZDA0ODNjOGZiNmZhOGQyOQ==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.01",
|
||||
"sender": "0x718a514210b475b921583eab412ebf0fbadf3b39",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjE4LjI5NjdfVVNE",
|
||||
"currency": "USD",
|
||||
"value": 18.2967,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnMllqTTJNelEwT1daaU1HWTROems0TkRnM1pqWmlOREkwTkRjMFkySXdNbVF5WlRVNE1EZ3dPVEpoWVRneE1EVm1ObUU0T1dOalpHTTBORGRsTURSa1h6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEQXdNREF3TURBek1HWTBPV0ptTW1Vd01ESmxOakJqTm1Wa01UWTJNV1ppTWpNME5tUTRPREk9",
|
||||
"timestamp": 1684085063,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg2YjM2MzQ0OWZiMGY4Nzk4NDg3ZjZiNDI0NDc0Y2IwMmQyZTU4MDgwOTJhYTgxMDVmNmE4OWNjZGM0NDdlMDRkXzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDAwMDAwMDAzMGY0OWJmMmUwMDJlNjBjNmVkMTY2MWZiMjM0NmQ4ODI=",
|
||||
"type": "UNKNOWN",
|
||||
"blockNumber": 17259555,
|
||||
"hash": "0x6b363449fb0f8798487f6b424474cb02d2e5808092aa8105f6a89ccdc447e04d",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0x000000030f49bf2e002e60c6ed1661fb2346d882",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 460,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": []
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNFlXRTVNVFJqTkRjeU5qWTNNVGxqWkRFeE1EYzNOMkprTnpZek0yVTFOV1kyWkdWbVpXRmpPVEV4TlRjd09EZzNZVEEyWXpNNE5UTmxaV0kyTldZeVh6QjRaR0V4TTJRMk5HVmpPVFZqWkRZM056VXlPVEZpTVdNek1qRXdNamN4TWpGaVpUSXdPV1JtTUY4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9",
|
||||
"timestamp": 1684085051,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg4YWE5MTRjNDcyNjY3MTljZDExMDc3N2JkNzYzM2U1NWY2ZGVmZWFjOTExNTcwODg3YTA2YzM4NTNlZWI2NWYyXzB4ZGExM2Q2NGVjOTVjZDY3NzUyOTFiMWMzMjEwMjcxMjFiZTIwOWRmMF8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=",
|
||||
"type": "RECEIVE",
|
||||
"blockNumber": 17259554,
|
||||
"hash": "0x8aa914c47266719cd110777bd7633e55f6defeac911570887a06c3853eeb65f2",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"from": "0xda13d64ec95cd6775291b1c321027121be209df0",
|
||||
"nonce": 832,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGRhMTNkNjRlYzk1Y2Q2Nzc1MjkxYjFjMzIxMDI3MTIxYmUyMDlkZjBfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4OGFhOTE0YzQ3MjY2NzE5Y2QxMTA3NzdiZDc2MzNlNTVmNmRlZmVhYzkxMTU3MDg4N2EwNmMzODUzZWViNjVmMg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
"address": null,
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
|
||||
"url": "https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "NATIVE",
|
||||
"quantity": "0.00129866",
|
||||
"sender": "0xda13d64ec95cd6775291b1c321027121be209df0",
|
||||
"recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjIuMzc2MTE5MjQyMl9VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 2.3761192422,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
},
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnM00yTXdZMlJpTnpReU9UVTJZVFUxWXpZd016YzBOemd6TkRRNVpUSmpNbVZtTURnM1lqUTVPRFl4TVdGak5EZ3dZalJrTVRFMU1UbGhZemRpTXpZNVh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VHUXpaR1UwTkRneE5qTXlNakl5TURVME9UazJZVE0yTlRsaE5UTXlNR0k1TWpWbU5qUXhNR1k9",
|
||||
"timestamp": 1684006019,
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"id": "VHJhbnNhY3Rpb246MHg3M2MwY2RiNzQyOTU2YTU1YzYwMzc0NzgzNDQ5ZTJjMmVmMDg3YjQ5ODYxMWFjNDgwYjRkMTE1MTlhYzdiMzY5XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weGQzZGU0NDgxNjMyMjIyMDU0OTk2YTM2NTlhNTMyMGI5MjVmNjQxMGY=",
|
||||
"type": "SEND",
|
||||
"blockNumber": 17253116,
|
||||
"hash": "0x73c0cdb742956a55c60374783449e2c2ef087b498611ac480b4d11519ac7b369",
|
||||
"status": "CONFIRMED",
|
||||
"to": "0xd3de4481632222054996a3659a5320b925f6410f",
|
||||
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"nonce": 459,
|
||||
"__typename": "TransactionDetails",
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHhiZTgyODI1NjRlYzJiNzAwMDlmMmQ2ODk1NDAxMmViMDlmNDhiYzhkXzB4NzNjMGNkYjc0Mjk1NmE1NWM2MDM3NDc4MzQ0OWUyYzJlZjA4N2I0OTg2MTFhYzQ4MGI0ZDExNTE5YWM3YjM2OQ==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fMHhkM2RlNDQ4MTYzMjIyMjA1NDk5NmEzNjU5YTUzMjBiOTI1ZjY0MTBm",
|
||||
"name": "EL CHAPO",
|
||||
"symbol": "CHAPO",
|
||||
"address": "0xd3de4481632222054996a3659a5320b925f6410f",
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4ZDNkZTQ0ODE2MzIyMjIwNTQ5OTZhMzY1OWE1MzIwYjkyNWY2NDEwZg==",
|
||||
"isSpam": true,
|
||||
"logo": null,
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "ERC20",
|
||||
"quantity": "50000000000000.002683081102196736",
|
||||
"sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"recipient": "0xbe8282564ec2b70009f2d68954012eb09f48bc8d",
|
||||
"direction": "OUT",
|
||||
"transactedValue": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"__typename": "AssetActivity"
|
||||
}
|
||||
],
|
||||
"__typename": "Portfolio"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
1
cypress/fixtures/mini-portfolio/nfts.json
Normal file
1
cypress/fixtures/mini-portfolio/nfts.json
Normal file
File diff suppressed because one or more lines are too long
1
cypress/fixtures/mini-portfolio/pools.json
Normal file
1
cypress/fixtures/mini-portfolio/pools.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
cypress/fixtures/mini-portfolio/tokens.json
Normal file
1
cypress/fixtures/mini-portfolio/tokens.json
Normal file
File diff suppressed because one or more lines are too long
102
cypress/fixtures/mini-portfolio/uniswapx_activity.json
Normal file
102
cypress/fixtures/mini-portfolio/uniswapx_activity.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"data": {
|
||||
"portfolios": [
|
||||
{
|
||||
"id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==",
|
||||
"assetActivities": [
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNE9EZGpOemN5TlRRNU1qWTVNVEkwWVRkbVpUTXlNams1TjJJNU0yUTJabUV3TjJObE1UQXhOamxrTjJJd1pXUXhObUV6TldabU16SmtOMk13TWpBeVh6QjRaREkzTXpnek1EUTRaalF4WldZMlpXRXhaV1EzWWpBeFltVTVOemRqTjJVME1HSXdaRGswTmw4d2VEUTNZVFF5TVdKalpXTTJORE5oWWpSallURmpZamc0TmpOaU4yWm1PV0ppWm1SaU5HVmlNVE09",
|
||||
"timestamp": 1691001923,
|
||||
"type": "SWAP_ORDER",
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"__typename": "TransactionDetails",
|
||||
"id": "VHJhbnNhY3Rpb246MHg4ODdjNzcyNTQ5MjY5MTI0YTdmZTMyMjk5N2I5M2Q2ZmEwN2NlMTAxNjlkN2IwZWQxNmEzNWZmMzJkN2MwMjAyXzB4ZDI3MzgzMDQ4ZjQxZWY2ZWExZWQ3YjAxYmU5NzdjN2U0MGIwZDk0Nl8weDQ3YTQyMWJjZWM2NDNhYjRjYTFjYjg4NjNiN2ZmOWJiZmRiNGViMTM=",
|
||||
"type": "SWAP_ORDER",
|
||||
"from": "0xd27383048f41ef6ea1ed7b01be977c7e40b0d946",
|
||||
"to": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13",
|
||||
"hash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"nonce": 439,
|
||||
"status": "CONFIRMED"
|
||||
},
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDgwYmVjYjgwOGJmYWRlNDE0MzE4M2U1OGQxOGYyMDgwZTg0ZTU3YTFfMHg0N2E0MjFiY2VjNjQzYWI0Y2ExY2I4ODYzYjdmZjliYmZkYjRlYjEzXzB4ODg3Yzc3MjU0OTI2OTEyNGE3ZmUzMjI5OTdiOTNkNmZhMDdjZTEwMTY5ZDdiMGVkMTZhMzVmZjMyZDdjMDIwMg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fMHhhMGI4Njk5MWM2MjE4YjM2YzFkMTlkNGEyZTllYjBjZTM2MDZlYjQ4",
|
||||
"name": "USD Coin",
|
||||
"symbol": "USDC",
|
||||
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
"decimals": 6,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YTBiODY5OTFjNjIxOGIzNmMxZDE5ZDRhMmU5ZWIwY2UzNjA2ZWI0OA==",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL1VuaXN3YXAvYXNzZXRzL21hc3Rlci9ibG9ja2NoYWlucy9ldGhlcmV1bS9hc3NldHMvMHhBMGI4Njk5MWM2MjE4YjM2YzFkMTlENGEyZTlFYjBjRTM2MDZlQjQ4L2xvZ28ucG5n",
|
||||
"url": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "ERC20",
|
||||
"quantity": "300.0",
|
||||
"sender": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"recipient": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjMwMC4xNDkxNTIwOTE5NDE2M19VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 300.14915209194163,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDQ3YTQyMWJjZWM2NDNhYjRjYTFjYjg4NjNiN2ZmOWJiZmRiNGViMTNfMHg4MGJlY2I4MDhiZmFkZTQxNDMxODNlNThkMThmMjA4MGU4NGU1N2ExXzB4ODg3Yzc3MjU0OTI2OTEyNGE3ZmUzMjI5OTdiOTNkNmZhMDdjZTEwMTY5ZDdiMGVkMTZhMzVmZjMyZDdjMDIwMg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fMHg2YjE3NTQ3NGU4OTA5NGM0NGRhOThiOTU0ZWVkZWFjNDk1MjcxZDBm",
|
||||
"name": "Dai Stablecoin",
|
||||
"symbol": "DAI",
|
||||
"address": "0x6b175474e89094c44da98b954eedeac495271d0f",
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4NmIxNzU0NzRlODkwOTRjNDRkYTk4Yjk1NGVlZGVhYzQ5NTI3MWQwZg==",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL1VuaXN3YXAvYXNzZXRzL21hc3Rlci9ibG9ja2NoYWlucy9ldGhlcmV1bS9hc3NldHMvMHg2QjE3NTQ3NEU4OTA5NEM0NERhOThiOTU0RWVkZUFDNDk1MjcxZDBGL2xvZ28ucG5n",
|
||||
"url": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "ERC20",
|
||||
"quantity": "280.573117586837733376",
|
||||
"sender": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13",
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjI4MC42ODc3OTU0NTg2ODE4X1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 280.6877954586818,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
],
|
||||
"__typename": "AssetActivity"
|
||||
}
|
||||
],
|
||||
"__typename": "Portfolio"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
26
cypress/fixtures/uniswapx/expiredStatusResponse.json
Normal file
26
cypress/fixtures/uniswapx/expiredStatusResponse.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"orderStatus": "expired",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
562
cypress/fixtures/uniswapx/feeQuote.json
Normal file
562
cypress/fixtures/uniswapx/feeQuote.json
Normal file
@@ -0,0 +1,562 @@
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
|
||||
"nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633",
|
||||
"deadline": 1697481666,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1697481594,
|
||||
"decayEndTime": 1697481654,
|
||||
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "200000000",
|
||||
"endAmount": "200000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": "123803169993201727",
|
||||
"endAmount": "117908377342236273",
|
||||
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
|
||||
},
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": "185983730585681",
|
||||
"endAmount": "177128258400955",
|
||||
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327",
|
||||
"quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890",
|
||||
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
|
||||
"orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9",
|
||||
"startTimeBufferSecs": 45,
|
||||
"auctionPeriodSecs": 60,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
|
||||
},
|
||||
"deadline": 1697481666,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
|
||||
},
|
||||
"deadline": 1697481666,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1697481594,
|
||||
"decayEndTime": 1697481654,
|
||||
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x01b7d653c183183f"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x01a2e50b6386d671"
|
||||
},
|
||||
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
|
||||
},
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0xa926b6321051"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0xa118e2ebf2bb"
|
||||
},
|
||||
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"portionBips": 15,
|
||||
"portionAmount": "185983730585681",
|
||||
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
},
|
||||
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
|
||||
"allQuotes": [
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
|
||||
"nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633",
|
||||
"deadline": 1697481666,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1697481594,
|
||||
"decayEndTime": 1697481654,
|
||||
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "200000000",
|
||||
"endAmount": "200000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": "123803169993201727",
|
||||
"endAmount": "117908377342236273",
|
||||
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
|
||||
},
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": "185983730585681",
|
||||
"endAmount": "177128258400955",
|
||||
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327",
|
||||
"quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890",
|
||||
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
|
||||
"orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9",
|
||||
"startTimeBufferSecs": 45,
|
||||
"auctionPeriodSecs": 60,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
|
||||
},
|
||||
"deadline": 1697481666,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
|
||||
},
|
||||
"deadline": 1697481666,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1697481594,
|
||||
"decayEndTime": 1697481654,
|
||||
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0bebc200"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x01b7d653c183183f"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x01a2e50b6386d671"
|
||||
},
|
||||
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
|
||||
},
|
||||
{
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0xa926b6321051"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0xa118e2ebf2bb"
|
||||
},
|
||||
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"portionBips": 15,
|
||||
"portionAmount": "185983730585681",
|
||||
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
|
||||
}
|
||||
},
|
||||
{
|
||||
"routing": "CLASSIC",
|
||||
"quote": {
|
||||
"methodParameters": {
|
||||
"calldata": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000652d85d0000000000000000000000000000000000000000000000000000000000000000308060c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000001bdf1285753b47400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000037a8f295612602f2774d331e562be9e61b83a327000000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000001bd45ea74e458eb",
|
||||
"value": "0x00",
|
||||
"to": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"
|
||||
},
|
||||
"blockNumber": "18364784",
|
||||
"amount": "200000000",
|
||||
"amountDecimals": "200",
|
||||
"quote": "126149127803342909",
|
||||
"quoteDecimals": "0.126149127803342909",
|
||||
"quoteGasAdjusted": "122888348391508943",
|
||||
"quoteGasAdjustedDecimals": "0.122888348391508943",
|
||||
"quoteGasAndPortionAdjusted": "122699124699803928",
|
||||
"quoteGasAndPortionAdjustedDecimals": "0.122699124699803928",
|
||||
"gasUseEstimateQuote": "3260779411833966",
|
||||
"gasUseEstimateQuoteDecimals": "0.003260779411833966",
|
||||
"gasUseEstimate": "240911",
|
||||
"gasUseEstimateUSD": "5.153332510477604328",
|
||||
"simulationStatus": "SUCCESS",
|
||||
"simulationError": false,
|
||||
"gasPriceWei": "13535203506",
|
||||
"route": [
|
||||
[
|
||||
{
|
||||
"type": "v2-pool",
|
||||
"address": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
|
||||
"tokenIn": {
|
||||
"chainId": 1,
|
||||
"decimals": "6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"symbol": "USDC"
|
||||
},
|
||||
"tokenOut": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"symbol": "WETH"
|
||||
},
|
||||
"reserve0": {
|
||||
"token": {
|
||||
"chainId": 1,
|
||||
"decimals": "6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"symbol": "USDC"
|
||||
},
|
||||
"quotient": "27487668611269"
|
||||
},
|
||||
"reserve1": {
|
||||
"token": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"symbol": "WETH"
|
||||
},
|
||||
"quotient": "17390022942803382004255"
|
||||
},
|
||||
"amountIn": "200000000",
|
||||
"amountOut": "125959904111637894"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routeString": "[V2] 100.00% = USDC -- [0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc] --> WETH",
|
||||
"quoteId": "f46cf31c-251e-470c-bd57-13209015694e",
|
||||
"portionBips": 15,
|
||||
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327",
|
||||
"portionAmount": "189223691705014",
|
||||
"portionAmountDecimals": "0.000189223691705014",
|
||||
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
|
||||
"tradeType": "EXACT_INPUT",
|
||||
"slippage": 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
114
cypress/fixtures/uniswapx/fillTransactionReceipt.json
Normal file
114
cypress/fixtures/uniswapx/fillTransactionReceipt.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"to": "0xbD7F9D0239f81C94b728d827a87b9864972661eC",
|
||||
"from": "0xa17Fbb0b5a251A7ACA3BD7377e7eCC4F700A2C09",
|
||||
"contractAddress": null,
|
||||
"transactionIndex": 61,
|
||||
"gasUsed": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x03e0c8"
|
||||
},
|
||||
"logsBloom":
|
||||
"0x00000000000000000000008000200100000020000000000000000000000000000000000000000000000000010000000000000000000020000000000001000000000280000000000808000008000000000000000000000000000000000000200010000000100000000008000000000004402000080000000000000010000800000000000000000800000800000000000000000000010000000000000000000000000000000000200000000000005000000000000000000000000000000000000000000002000000000000000000000000040002000000000000000100000000090000000400000000000400000020080000000000000000000000000000000000",
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c",
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"logs": [
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000005f5e100",
|
||||
"logIndex": 103,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0xbD7F9D0239f81C94b728d827a87b9864972661eC",
|
||||
"topics": [
|
||||
"0x78ad7ec0e9f89e74012afa58738b6b661c024cb0fd185ee2f616c0a28924bd66",
|
||||
"0xd10e1d90145460003d98ba4b788564e9549cc93c65a12c9b297720a9d6a586de",
|
||||
"0x000000000000000000000000a17fbb0b5a251a7aca3bd7377e7ecc4f700a2c09",
|
||||
"0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1"
|
||||
],
|
||||
"data": "0x8e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de00",
|
||||
"logIndex": 104,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x0000000000000000000000005777d92f208679db4b9778590fa3cab3ac9e2168",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000056b9a675be430b502",
|
||||
"logIndex": 105,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf",
|
||||
"0x0000000000000000000000005777d92f208679db4b9778590fa3cab3ac9e2168"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000005f5e100",
|
||||
"logIndex": 106,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168",
|
||||
"topics": [
|
||||
"0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
|
||||
"0x00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf"
|
||||
],
|
||||
"data": "0xfffffffffffffffffffffffffffffffffffffffffffffffa946598a41bcf4afe0000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000010c7063b90a5e90d13830000000000000000000000000000000000000000000071b57cb2bb0b5b28224ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc89c",
|
||||
"logIndex": 107,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf",
|
||||
"0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1"
|
||||
],
|
||||
"data": "0x000000000000000000000000000000000000000000000004f409bcc7a52b6358",
|
||||
"logIndex": 108,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
}
|
||||
],
|
||||
"blockNumber": 17444757,
|
||||
"confirmations": 392238,
|
||||
"cumulativeGasUsed": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x4065ac"
|
||||
},
|
||||
"effectiveGasPrice": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x04aa792df0"
|
||||
},
|
||||
"status": 1,
|
||||
"type": 2,
|
||||
"byzantium": true
|
||||
}
|
||||
33
cypress/fixtures/uniswapx/filledStatusResponse.json
Normal file
33
cypress/fixtures/uniswapx/filledStatusResponse.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"settledAmounts": [
|
||||
{
|
||||
"tokenOut": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"amountOut": "91371770080538616664"
|
||||
}
|
||||
],
|
||||
"orderStatus": "filled",
|
||||
"txHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"orderStatus": "insufficient-funds",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
cypress/fixtures/uniswapx/openStatusResponse.json
Normal file
26
cypress/fixtures/uniswapx/openStatusResponse.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"orderStatus": "open",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
cypress/fixtures/uniswapx/orderResponse.json
Normal file
1
cypress/fixtures/uniswapx/orderResponse.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19"}
|
||||
493
cypress/fixtures/uniswapx/quote1.json
Normal file
493
cypress/fixtures/uniswapx/quote1.json
Normal file
@@ -0,0 +1,493 @@
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": "57335948072881703373319552024074512292695687510330025934414357004397546394368",
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": "0",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "300000000",
|
||||
"endAmount": "300000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "289951120815684452958",
|
||||
"endAmount": "267060007981523637666",
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064c91e6e0000000000000000000000000000000000000000000000000000000064c91eaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000000000000011e1a30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e37953000000000000000000000000000000000000000000000000000000000064c91eb6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000fb7e15027ad3e025e00000000000000000000000000000000000000000000000e7a33be508bb395a200000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd",
|
||||
"quoteId": "f9f47cd7-a62c-4622-9ac7-51d0e662245a",
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"auctionPeriodSecs": 60,
|
||||
"startTimeBufferSecs": 30,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x00"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0fb7e15027ad3e025e"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0e7a33be508bb395a2"
|
||||
},
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"allQuotes": [
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": "57335948072881703373319552024074512292695687510330025934414357004397546394368",
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": "0",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "300000000",
|
||||
"endAmount": "300000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "289951120815684452958",
|
||||
"endAmount": "267060007981523637666",
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064c91e6e0000000000000000000000000000000000000000000000000000000064c91eaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000000000000011e1a30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e37953000000000000000000000000000000000000000000000000000000000064c91eb6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000fb7e15027ad3e025e00000000000000000000000000000000000000000000000e7a33be508bb395a200000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd",
|
||||
"quoteId": "f9f47cd7-a62c-4622-9ac7-51d0e662245a",
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"auctionPeriodSecs": 60,
|
||||
"startTimeBufferSecs": 30,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x00"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0fb7e15027ad3e025e"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0e7a33be508bb395a2"
|
||||
},
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"routing": "CLASSIC",
|
||||
"quote": {
|
||||
"blockNumber": "17820918",
|
||||
"amount": "300000000",
|
||||
"amountDecimals": "300",
|
||||
"quote": "299952256425393549464",
|
||||
"quoteDecimals": "299.952256425393549464",
|
||||
"quoteGasAdjusted": "289922128602824170541",
|
||||
"quoteGasAdjustedDecimals": "289.922128602824170541",
|
||||
"gasUseEstimateQuote": "10030127822569378922",
|
||||
"gasUseEstimateQuoteDecimals": "10.030127822569378922",
|
||||
"gasUseEstimate": "128000",
|
||||
"gasUseEstimateUSD": "10.031724",
|
||||
"simulationStatus": "UNATTEMPTED",
|
||||
"simulationError": false,
|
||||
"gasPriceWei": "42803167855",
|
||||
"route": [
|
||||
[
|
||||
{
|
||||
"type": "v3-pool",
|
||||
"address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168",
|
||||
"tokenIn": {
|
||||
"chainId": 1,
|
||||
"decimals": "6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"symbol": "USDC"
|
||||
},
|
||||
"tokenOut": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"symbol": "DAI"
|
||||
},
|
||||
"fee": "100",
|
||||
"liquidity": "534676532046235168447130",
|
||||
"sqrtRatioX96": "79230505815006815109584",
|
||||
"tickCurrent": "-276324",
|
||||
"amountIn": "300000000",
|
||||
"amountOut": "299952256425393549464"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routeString": "[V3] 100.00% = USDC -- 0.01% [0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168] --> DAI",
|
||||
"quoteId": "1dd3bd14-780e-41c6-88e1-30a763f97482",
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"tradeType": "EXACT_INPUT",
|
||||
"slippage": 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
493
cypress/fixtures/uniswapx/quote2.json
Normal file
493
cypress/fixtures/uniswapx/quote2.json
Normal file
@@ -0,0 +1,493 @@
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": "1993350209834725680308575292465150260730647098062962750049345504775310970881",
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"startAmount": "1000000000000000000",
|
||||
"endAmount": "1000000000000000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "929502510517534478575",
|
||||
"endAmount": "919795986077127665276",
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064cd4f240000000000000000000000000000000000000000000000000000000064cd4f60000000000000000000000000165d98de005d2818176b99b1a93b9325dbe581810000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000000000000000000000000000000000000000000000468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d8090010000000000000000000000000000000000000000000000000000000064cd4f6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000092d6c1e31e14520e676a687f0a93788b716beff500000000000000000000000000000000000000000000003263704899af6e50ef000000000000000000000000000000000000000000000031dcbbc80c9555e67c0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"quoteId": "09ce28b7-1ddf-4317-a28d-d21092be9f84",
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"auctionPeriodSecs": 60,
|
||||
"startTimeBufferSecs": 30,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x3263704899af6e50ef"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x31dcbbc80c9555e67c"
|
||||
},
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"allQuotes": [
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": "1993350209834725680308575292465150260730647098062962750049345504775310970881",
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"startAmount": "1000000000000000000",
|
||||
"endAmount": "1000000000000000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "929502510517534478575",
|
||||
"endAmount": "919795986077127665276",
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064cd4f240000000000000000000000000000000000000000000000000000000064cd4f60000000000000000000000000165d98de005d2818176b99b1a93b9325dbe581810000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000000000000000000000000000000000000000000000468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d8090010000000000000000000000000000000000000000000000000000000064cd4f6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000092d6c1e31e14520e676a687f0a93788b716beff500000000000000000000000000000000000000000000003263704899af6e50ef000000000000000000000000000000000000000000000031dcbbc80c9555e67c0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"quoteId": "09ce28b7-1ddf-4317-a28d-d21092be9f84",
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"auctionPeriodSecs": 60,
|
||||
"startTimeBufferSecs": 30,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x3263704899af6e50ef"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x31dcbbc80c9555e67c"
|
||||
},
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"routing": "CLASSIC",
|
||||
"quote": {
|
||||
"blockNumber": "17843654",
|
||||
"amount": "1000000000000000000",
|
||||
"amountDecimals": "1",
|
||||
"quote": "931181529570145926787",
|
||||
"quoteDecimals": "931.181529570145926787",
|
||||
"quoteGasAdjusted": "929033336026294051828",
|
||||
"quoteGasAdjustedDecimals": "929.033336026294051828",
|
||||
"gasUseEstimateQuote": "2148193543851874958",
|
||||
"gasUseEstimateQuoteDecimals": "2.148193543851874958",
|
||||
"gasUseEstimate": "128000",
|
||||
"gasUseEstimateUSD": "4.174934",
|
||||
"simulationStatus": "UNATTEMPTED",
|
||||
"simulationError": false,
|
||||
"gasPriceWei": "17811260539",
|
||||
"route": [
|
||||
[
|
||||
{
|
||||
"type": "v3-pool",
|
||||
"address": "0xD8de6af55F618a7Bc69835D55DDC6582220c36c0",
|
||||
"tokenIn": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"symbol": "WETH"
|
||||
},
|
||||
"tokenOut": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"symbol": "DAI"
|
||||
},
|
||||
"fee": "3000",
|
||||
"liquidity": "62287359628325896425115",
|
||||
"sqrtRatioX96": "2591813593283507889384697884",
|
||||
"tickCurrent": "-68403",
|
||||
"amountIn": "1000000000000000000",
|
||||
"amountOut": "931181529570145926787"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routeString": "[V3] 100.00% = WETH -- 0.3% [0xD8de6af55F618a7Bc69835D55DDC6582220c36c0] --> DAI",
|
||||
"quoteId": "414e5f1c-120a-4e35-9760-c54d4b09e91d",
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"tradeType": "EXACT_INPUT",
|
||||
"slippage": 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
19
cypress/staging/t9n.test.ts
Normal file
19
cypress/staging/t9n.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('translations', () => {
|
||||
it('loads locale from the query param', () => {
|
||||
cy.visit('/?lng=fr-FR')
|
||||
cy.contains('Échanger')
|
||||
cy.contains('Uniswap disponible en : English')
|
||||
})
|
||||
|
||||
it('loads locale from menu', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('français').click({ force: true })
|
||||
cy.location('search').should('match', /\?lng=fr-FR$/)
|
||||
cy.contains('Échanger')
|
||||
cy.contains('Uniswap disponible en : English')
|
||||
})
|
||||
})
|
||||
@@ -2,10 +2,9 @@ import 'cypress-hardhat/lib/browser'
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
|
||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
import { injected } from './ethereum'
|
||||
import { FeatureFlag } from '../../src/featureFlags'
|
||||
import { initialState, UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE, setInitialUserState } from '../utils/user-state'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@@ -13,15 +12,19 @@ declare global {
|
||||
interface ApplicationWindow {
|
||||
ethereum: Eip1193Bridge
|
||||
}
|
||||
interface Chainable<Subject> {
|
||||
/**
|
||||
* Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event.
|
||||
*
|
||||
* @param {string} eventName - The type of the event to search for e.g. SwapEventName.SWAP_TRANSACTION_COMPLETED
|
||||
* @param {number} timeout - The maximum amount of time (in ms) to wait for the event.
|
||||
* @returns {Chainable<Subject>}
|
||||
*/
|
||||
waitForAmplitudeEvent(eventName: string, timeout?: number): Chainable<Subject>
|
||||
}
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
/**
|
||||
* The mock ethereum provider to inject into the page.
|
||||
* @default 'goerli'
|
||||
*/
|
||||
// TODO(INFRA-175): Migrate all usage of 'goerli' to 'hardhat'.
|
||||
ethereum?: 'goerli' | 'hardhat'
|
||||
featureFlags?: Array<{ name: FeatureFlag; value: boolean }>
|
||||
/**
|
||||
* Initial user state.
|
||||
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
|
||||
@@ -38,43 +41,51 @@ Cypress.Commands.overwrite(
|
||||
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
|
||||
if (typeof url !== 'string') throw new Error('Invalid arguments. The first argument to cy.visit must be the path.')
|
||||
|
||||
// Add a hash in the URL if it is not present (to use hash-based routing correctly with queryParams).
|
||||
let hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
|
||||
if (options?.ethereum === 'goerli') hashUrl += `${url.includes('?') ? '&' : '?'}chain=goerli`
|
||||
|
||||
return cy
|
||||
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
|
||||
.provider()
|
||||
.then((provider) =>
|
||||
original({
|
||||
...options,
|
||||
url: hashUrl,
|
||||
url,
|
||||
onBeforeLoad(win) {
|
||||
options?.onBeforeLoad?.(win)
|
||||
|
||||
// We want to test from a clean state, so we clear the local storage (which clears redux).
|
||||
win.localStorage.clear()
|
||||
|
||||
// Set initial user state.
|
||||
win.localStorage.setItem(
|
||||
'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
|
||||
JSON.stringify(options?.userState ?? CONNECTED_WALLET_USER_STATE)
|
||||
)
|
||||
setInitialUserState(win, {
|
||||
...initialState,
|
||||
...CONNECTED_WALLET_USER_STATE,
|
||||
...(options?.userState ?? {}),
|
||||
})
|
||||
|
||||
// Set feature flags, if configured.
|
||||
if (options?.featureFlags) {
|
||||
const featureFlags = options.featureFlags.reduce((flags, flag) => ({ ...flags, [flag]: 'enabled' }), {})
|
||||
const featureFlags = options.featureFlags.reduce(
|
||||
(flags, flag) => ({ ...flags, [flag.name]: flag.value ? 'enabled' : 'control' }),
|
||||
{}
|
||||
)
|
||||
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
|
||||
}
|
||||
|
||||
// Inject the mock ethereum provider.
|
||||
if (options?.ethereum === 'hardhat') {
|
||||
win.ethereum = provider
|
||||
} else {
|
||||
win.ethereum = injected
|
||||
}
|
||||
win.ethereum = provider
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Cypress.Commands.add('waitForAmplitudeEvent', (eventName) => {
|
||||
function checkRequest() {
|
||||
return cy.wait('@amplitude').then((interception) => {
|
||||
const events = interception.request.body.events
|
||||
const event = events.find((event: any) => event.event_type === eventName)
|
||||
|
||||
if (event) {
|
||||
return cy.wrap(event)
|
||||
} else {
|
||||
return checkRequest()
|
||||
}
|
||||
})
|
||||
}
|
||||
return checkRequest()
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
import '@cypress/code-coverage/support'
|
||||
import './commands'
|
||||
import './setupTests'
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Updates cy.visit() to include an injected window.ethereum provider.
|
||||
*/
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
import { Wallet } from '@ethersproject/wallet'
|
||||
|
||||
import { SupportedChainId } from '../../src/constants/chains'
|
||||
|
||||
// todo: figure out how env vars actually work in CI
|
||||
// const TEST_PRIVATE_KEY = Cypress.env('INTEGRATION_TEST_PRIVATE_KEY')
|
||||
const TEST_PRIVATE_KEY = '0xe580410d7c37d26c6ad1a837bbae46bc27f9066a466fb3a66e770523b4666d19'
|
||||
|
||||
// address of the above key
|
||||
const TEST_ADDRESS_NEVER_USE = new Wallet(TEST_PRIVATE_KEY).address
|
||||
const CHAIN_ID = SupportedChainId.GOERLI
|
||||
const HEXLIFIED_CHAIN_ID = `0x${CHAIN_ID.toString(16)}`
|
||||
|
||||
const provider = new JsonRpcProvider('https://goerli.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 5)
|
||||
const signer = new Wallet(TEST_PRIVATE_KEY, provider)
|
||||
export const injected = new (class extends Eip1193Bridge {
|
||||
chainId = CHAIN_ID
|
||||
|
||||
async sendAsync(...args: any[]) {
|
||||
console.debug('sendAsync called', ...args)
|
||||
return this.send(...args)
|
||||
}
|
||||
async send(...args: any[]) {
|
||||
console.debug('send called', ...args)
|
||||
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
|
||||
let callback
|
||||
let method
|
||||
let params
|
||||
if (isCallbackForm) {
|
||||
callback = args[1]
|
||||
method = args[0].method
|
||||
params = args[0].params
|
||||
} else {
|
||||
method = args[0]
|
||||
params = args[1]
|
||||
}
|
||||
if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
|
||||
if (isCallbackForm) {
|
||||
callback({ result: [TEST_ADDRESS_NEVER_USE] })
|
||||
} else {
|
||||
return Promise.resolve([TEST_ADDRESS_NEVER_USE])
|
||||
}
|
||||
}
|
||||
if (method === 'eth_chainId') {
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result: HEXLIFIED_CHAIN_ID })
|
||||
} else {
|
||||
return Promise.resolve(HEXLIFIED_CHAIN_ID)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await super.send(method, params)
|
||||
console.debug('result received', method, params, result)
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result })
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
if (isCallbackForm) {
|
||||
callback(error, null)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
})(signer, provider)
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-ignore
|
||||
import TokenListJSON from '@uniswap/default-token-list'
|
||||
import { CyHttpMessages } from 'cypress/types/net-stubbing'
|
||||
|
||||
beforeEach(() => {
|
||||
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
|
||||
@@ -8,28 +9,34 @@ beforeEach(() => {
|
||||
req.headers['origin'] = 'https://app.uniswap.org'
|
||||
})
|
||||
|
||||
// Infura uses a test endpoint, which allow-lists http://localhost:3000 instead.
|
||||
cy.intercept(/infura.io/, (req) => {
|
||||
req.headers['referer'] = 'http://localhost:3000'
|
||||
req.headers['origin'] = 'http://localhost:3000'
|
||||
req.alias = req.body.method
|
||||
req.continue()
|
||||
})
|
||||
// Network RPCs are disabled for cypress tests - calls should be routed through the connected wallet instead.
|
||||
cy.intercept(/infura.io/, { statusCode: 404 })
|
||||
cy.intercept(/quiknode.pro/, { statusCode: 404 })
|
||||
|
||||
// Log requests to hardhat.
|
||||
cy.intercept(/:8545/, logJsonRpc)
|
||||
|
||||
// Mock analytics responses to avoid analytics in tests.
|
||||
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const requestBody = JSON.stringify(req.body)
|
||||
const byteSize = new Blob([requestBody]).size
|
||||
req.alias = 'amplitude'
|
||||
req.reply(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
server_upload_time: Date.now(),
|
||||
payload_size_bytes: byteSize,
|
||||
events_ingested: req.body.events.length,
|
||||
})
|
||||
}),
|
||||
{
|
||||
'origin-country': 'US',
|
||||
}
|
||||
)
|
||||
}).intercept('https://*.sentry.io', { statusCode: 200 })
|
||||
|
||||
// Mock statsig to allow us to mock flags.
|
||||
cy.intercept(/statsig/, { statusCode: 409 })
|
||||
|
||||
// Mock our own token list responses to avoid the latency of IPFS.
|
||||
cy.intercept('https://gateway.ipfs.io/ipns/tokens.uniswap.org', TokenListJSON)
|
||||
.intercept('https://gateway.ipfs.io/ipns/extendedtokens.uniswap.org', { statusCode: 404 })
|
||||
@@ -39,3 +46,21 @@ beforeEach(() => {
|
||||
// This resets the fork, as well as options like automine.
|
||||
cy.hardhat().then((hardhat) => hardhat.reset())
|
||||
})
|
||||
|
||||
function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) {
|
||||
req.alias = req.body.method
|
||||
const log = Cypress.log({
|
||||
autoEnd: false,
|
||||
name: req.body.method,
|
||||
message: req.body.params?.map((param: any) =>
|
||||
typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10)
|
||||
),
|
||||
})
|
||||
req.on('after:response', (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
log.end()
|
||||
} else {
|
||||
log.error(new Error(`${res.statusCode}: ${res.statusMessage}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"composite": false,
|
||||
"incremental": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"target": "ES5",
|
||||
"isolatedModules": false,
|
||||
"noImplicitAny": false,
|
||||
"target": "ES6",
|
||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
|
||||
"types": ["cypress", "node"]
|
||||
"types": ["cypress", "node"],
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["**/*.ts"],
|
||||
"watchOptions": {
|
||||
"excludeDirectories": ["node_modules"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,42 @@
|
||||
import { connectionMetaKey } from '../../src/connection/meta'
|
||||
import { ConnectionType } from '../../src/connection/types'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
|
||||
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: 'INJECTED' }
|
||||
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: ConnectionType.INJECTED }
|
||||
|
||||
export const DISCONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: undefined }
|
||||
|
||||
/**
|
||||
* This sets the initial value of the "user" slice in IndexedDB.
|
||||
* Other persisted slices are not set, so they will be filled with their respective initial values
|
||||
* when the app runs.
|
||||
*/
|
||||
export function setInitialUserState(win: Cypress.AUTWindow, state: UserState) {
|
||||
// Selected wallet should also be reflected in localStorage, so that eager connections work.
|
||||
if (state.selectedWallet) {
|
||||
win.localStorage.setItem(
|
||||
connectionMetaKey,
|
||||
JSON.stringify({
|
||||
type: state.selectedWallet,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
win.indexedDB.deleteDatabase('redux')
|
||||
const dbRequest = win.indexedDB.open('redux')
|
||||
dbRequest.onsuccess = function () {
|
||||
const db = dbRequest.result
|
||||
const transaction = db.transaction('keyvaluepairs', 'readwrite')
|
||||
const store = transaction.objectStore('keyvaluepairs')
|
||||
store.put(
|
||||
{
|
||||
user: state,
|
||||
},
|
||||
'persist:interface'
|
||||
)
|
||||
}
|
||||
dbRequest.onupgradeneeded = function () {
|
||||
const db = dbRequest.result
|
||||
db.createObjectStore('keyvaluepairs')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'enforce use of retry() for dynamic imports',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportExpression(node) {
|
||||
const grandParent = node.parent.parent
|
||||
if (
|
||||
!(
|
||||
grandParent &&
|
||||
grandParent.type === 'CallExpression' &&
|
||||
// Technically, we are only checking that a function named `retry` wraps the dynamic import.
|
||||
// We do not go as far as enforcing that it is import('utils/retry').retry
|
||||
grandParent.callee.name === 'retry' &&
|
||||
grandParent.arguments.length === 1 &&
|
||||
grandParent.arguments[0].type === 'ArrowFunctionExpression'
|
||||
)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Dynamic import should be wrapped in retry (see `utils/retry.ts`): `retry(() => import(...))`',
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
50
functions/README.md
Normal file
50
functions/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Cloudflare Cloud Functions
|
||||
|
||||
## Purpose
|
||||
|
||||
These functions utilize Cloudflare Functions to dynamically inject meta tags server side for richer link sharing capabilities.
|
||||
|
||||
## Functions
|
||||
|
||||
Currently, there are 2 types of cloudflare functions developed
|
||||
|
||||
- Meta Data Injectors - Workers that inject [Open Graph](https://ogp.me/) standardized meta tags into the `header` of specific webpages.
|
||||
- Currently we support this functionaltiy for three separate webpages: NFT Assets, NFT Collections, and Token Detail Pages
|
||||
- These functions query data from GraphQL and then formats them into HTML `meta` tags to be injected
|
||||
- Dynamically Generated Images - Utilizes Vercel's [Open Graph Image Generation Library](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) to create custom thumbnails for specific webpages
|
||||
- Currently supports NFT Assets, NFT Collections, and Token Detail Pages
|
||||
- These functions query data from GraphQL, and utilize `Satori` to convert HTML into a png image response which is then returned when the api is called.
|
||||
- Can be found in the `api/image` folder.
|
||||
|
||||
## Testing
|
||||
|
||||
Testing is done utilizing a custom jest environment as well as Cloudflare's local tester: `wrangler`. Wrangler enables testing locally by running a proxy to wrap `localhost`. Tests run against a proxy server, so you'll need to start it before running tests:
|
||||
- Manually run `yarn start:cloud` to setup wrangler on `localhost:3000`
|
||||
- Run unit tests with `yarn test:cloud`
|
||||
|
||||
## Deployment
|
||||
|
||||
Functions will be deployed to Cloudlfare where they will be ran automatically when the appropriate route is hit.
|
||||
|
||||
## Miscellaneous
|
||||
- Caching: In order to speed up webpage requests, repeated GraphQL queries will be saved and pulled using Cloudflare's Cache API.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `yarn start:cloud` (NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --node-compat --proxy=3001 --port=3000 -- yarn start), script to start local wrangler environment
|
||||
- `npx wrangler pages dev`: this basis of this command which starts a local instance of wrangler to test cloud functions
|
||||
- `--node-compat`: wrangler option that enables compatibility with Node.js modules
|
||||
- `--proxy:3001`: telling the proxy to listen on port 3001
|
||||
- `--port=3000`: telling wrangler to run our proxy on port 3000
|
||||
- `NODE_OPTIONS=--dns-result-order=ipv4first`: wrangler still serves to IPv4 which isn't compatible with Node 18 which default resolves to IPv6 so we need to specify to serve to IPv4
|
||||
- `PORT-3001 --yarn start`: runs default yarn start on port 3001
|
||||
- `yarn test:cloud` (NODE_OPTIONS=--experimental-vm-modules yarn jest functions --watch --config=functions/jest.config.json), script to test cloud functions with jest
|
||||
- `NODE_OPTIONS=--experimental-vm-modules`: support for ES Modules and Web Assembly
|
||||
- `--config=functions/jest.config.json`: specifying which config file to use
|
||||
|
||||
## Additional Documents
|
||||
- [Open Graph Protocol](https://ogp.me/)
|
||||
- [Open Graph Image Generation](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation)
|
||||
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
||||
- [HTML Rewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/)
|
||||
- [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/)
|
||||
18
functions/[[index]].ts
Normal file
18
functions/[[index]].ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { MetaTagInjector } from './components/metaTagInjector'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ request, next }) => {
|
||||
const imageUri = new URL(request.url).origin + '/images/1200x630_Rich_Link_Preview_Image.png'
|
||||
const data = {
|
||||
title: 'Uniswap Interface',
|
||||
image: imageUri,
|
||||
url: request.url,
|
||||
description: 'Swap or provide liquidity on the Uniswap Protocol',
|
||||
}
|
||||
const res = next()
|
||||
try {
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(data, request)).transform(await res)
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
76
functions/api/image/nfts/asset/[[index]].tsx
Normal file
76
functions/api/image/nfts/asset/[[index]].tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist'
|
||||
import { WATERMARK_URL } from '../../../../constants'
|
||||
import getAsset from '../../../../utils/getAsset'
|
||||
import getFont from '../../../../utils/getFont'
|
||||
import { getRequest } from '../../../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
try {
|
||||
const origin = new URL(request.url).origin
|
||||
const { index } = params
|
||||
const collectionAddress = index[0]?.toString()
|
||||
const tokenId = index[1]?.toString()
|
||||
const cacheUrl = origin + '/nfts/asset/' + collectionAddress + '/' + tokenId
|
||||
|
||||
if (blocklistedCollections.includes(collectionAddress)) {
|
||||
return new Response('Collection unsupported.', { status: 404 })
|
||||
}
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getAsset(collectionAddress, tokenId, cacheUrl),
|
||||
(data): data is NonNullable<Awaited<ReturnType<typeof getAsset>>> => Boolean(data.ogImage)
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return new Response('Asset not found.', { status: 404 })
|
||||
}
|
||||
|
||||
const fontData = await getFont(origin)
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
width: '1200px',
|
||||
height: '630px',
|
||||
}}
|
||||
>
|
||||
<img src={data.ogImage} alt={data.title} width="1200px" />
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '72px',
|
||||
right: '72px',
|
||||
display: 'flex',
|
||||
gap: '24px',
|
||||
}}
|
||||
>
|
||||
<img src={WATERMARK_URL} alt="Uniswap" height="72px" width="324px" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
) as Response
|
||||
} catch (error: any) {
|
||||
return new Response(error.message || error.toString(), { status: 500 })
|
||||
}
|
||||
}
|
||||
29
functions/api/image/nfts/asset/nftImage.test.ts
Normal file
29
functions/api/image/nfts/asset/nftImage.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
const assetImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/804',
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947',
|
||||
]
|
||||
|
||||
test.each(assetImageUrl)('assetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('image/png')
|
||||
})
|
||||
|
||||
const invalidAssetImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/10001',
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/44700',
|
||||
]
|
||||
|
||||
test.each(invalidAssetImageUrl)('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
|
||||
const blockedAssetImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xd4d871419714b778ebec2e22c7c53572b573706e/276',
|
||||
]
|
||||
|
||||
test.each(blockedAssetImageUrl)('blockedAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
122
functions/api/image/nfts/collection/[index].tsx
Normal file
122
functions/api/image/nfts/collection/[index].tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist'
|
||||
import { getColor } from '../../../../../src/utils/getColor'
|
||||
import { CHECK_URL, WATERMARK_URL } from '../../../../constants'
|
||||
import getCollection from '../../../../utils/getCollection'
|
||||
import getFont from '../../../../utils/getFont'
|
||||
import { getRequest } from '../../../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
try {
|
||||
const origin = new URL(request.url).origin
|
||||
const { index } = params
|
||||
const collectionAddress = index?.toString()
|
||||
const cacheUrl = origin + '/nfts/collection/' + collectionAddress
|
||||
|
||||
if (blocklistedCollections.includes(collectionAddress)) {
|
||||
return new Response('Collection unsupported.', { status: 404 })
|
||||
}
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getCollection(collectionAddress, cacheUrl),
|
||||
(data): data is NonNullable<Awaited<ReturnType<typeof getCollection>>> =>
|
||||
Boolean(data.ogImage && data.name && data.isVerified)
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return new Response('Collection not found.', { status: 404 })
|
||||
}
|
||||
|
||||
const [fontData, palette] = await Promise.all([getFont(origin), getColor(data.ogImage)])
|
||||
|
||||
// Split name into words to wrap them since satori does not support inline text wrapping
|
||||
const words = data.name.split(' ')
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
display: 'flex',
|
||||
width: '1200px',
|
||||
height: '630px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: `rgba(${palette[0]}, ${palette[1]}, ${palette[2]}, 0.75)`,
|
||||
padding: '72px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
gap: '48px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={data.ogImage}
|
||||
alt={data.name}
|
||||
width="500px"
|
||||
height="500px"
|
||||
style={{
|
||||
borderRadius: '60px',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '32px',
|
||||
width: '45%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
gap: '12px',
|
||||
fontSize: '72px',
|
||||
fontFamily: 'Inter',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{words.map((word: string) => (
|
||||
<text key={word + index}>{word}</text>
|
||||
))}
|
||||
{data.isVerified && <img src={CHECK_URL} height="54px" />}
|
||||
</div>
|
||||
<img src={WATERMARK_URL} alt="Uniswap" height="72px" width="324px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
) as Response
|
||||
} catch (error: any) {
|
||||
return new Response(error.message || error.toString(), { status: 500 })
|
||||
}
|
||||
}
|
||||
34
functions/api/image/nfts/collection/collectionImage.test.ts
Normal file
34
functions/api/image/nfts/collection/collectionImage.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as matchers from 'jest-extended'
|
||||
expect.extend(matchers)
|
||||
|
||||
const collectionImageUrls = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
|
||||
]
|
||||
|
||||
test.each([...collectionImageUrls])('collectionImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('image/png')
|
||||
})
|
||||
|
||||
const nonexistentImageUrls = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
]
|
||||
|
||||
const invalidCollectionImageUrls = ['http://127.0.0.1:3000/api/image/nfts/collection/0xd3adb33f']
|
||||
|
||||
test.each([...invalidCollectionImageUrls, ...nonexistentImageUrls])('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBeOneOf([404, 500])
|
||||
})
|
||||
|
||||
const blockedCollectionImageUrls = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xd4d871419714b778ebec2e22c7c53572b573706e',
|
||||
]
|
||||
|
||||
test.each(blockedCollectionImageUrls)('blockedCollectionImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBeOneOf([404, 500])
|
||||
})
|
||||
177
functions/api/image/tokens/[[index]].tsx
Normal file
177
functions/api/image/tokens/[[index]].tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { getColor } from '../../../../src/utils/getColor'
|
||||
import { WATERMARK_URL } from '../../../constants'
|
||||
import getFont from '../../../utils/getFont'
|
||||
import getNetworkLogoUrl from '../../../utils/getNetworkLogoURL'
|
||||
import { getRequest } from '../../../utils/getRequest'
|
||||
import getToken from '../../../utils/getToken'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
try {
|
||||
const origin = new URL(request.url).origin
|
||||
const { index } = params
|
||||
const networkName = String(index[0])
|
||||
const tokenAddress = String(index[1])
|
||||
|
||||
const cacheUrl = origin + '/tokens/' + networkName + '/' + tokenAddress
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getToken(networkName, tokenAddress, cacheUrl),
|
||||
(data): data is NonNullable<Awaited<ReturnType<typeof getToken>>> => Boolean(data.symbol && data.name)
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return new Response('Token not found.', { status: 404 })
|
||||
}
|
||||
|
||||
const [fontData, palette] = await Promise.all([getFont(origin), getColor(data.ogImage, true)])
|
||||
|
||||
const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin)
|
||||
|
||||
// Capitalize name such that each word starts with a capital letter
|
||||
let words = data.name.split(' ')
|
||||
words = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
let name = words.join(' ')
|
||||
name = name.trim()
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
display: 'flex',
|
||||
width: '1200px',
|
||||
height: '630px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
backgroundColor: `rgba(${palette[0]}, ${palette[1]}, ${palette[2]})`,
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
padding: '72px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{data.ogImage ? (
|
||||
<img src={data.ogImage} width="144px" style={{ borderRadius: '100%' }}>
|
||||
{networkLogo != '' && (
|
||||
<img
|
||||
src={networkLogo}
|
||||
width="48px"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '2px',
|
||||
bottom: '0px',
|
||||
borderRadius: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</img>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: '144px',
|
||||
height: '144px',
|
||||
borderRadius: '100%',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.12)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '48px',
|
||||
lineHeight: '58px',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{data.name.slice(0, 3).toUpperCase()}
|
||||
</div>
|
||||
{networkLogo != '' && (
|
||||
<img
|
||||
src={networkLogo}
|
||||
width="48px"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '2px',
|
||||
bottom: '0px',
|
||||
borderRadius: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '72px',
|
||||
lineHeight: '72px',
|
||||
marginLeft: '-5px',
|
||||
marginTop: '24px',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '168px',
|
||||
lineHeight: '133px',
|
||||
marginLeft: '-13px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{data.symbol}
|
||||
</div>
|
||||
<img src={WATERMARK_URL} alt="Uniswap" height="72px" width="324px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
) as Response
|
||||
} catch (error: any) {
|
||||
return new Response(error.message || error.toString(), { status: 500 })
|
||||
}
|
||||
}
|
||||
22
functions/api/image/tokens/tokenImage.test.ts
Normal file
22
functions/api/image/tokens/tokenImage.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const tokenImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE',
|
||||
]
|
||||
|
||||
test.each(tokenImageUrl)('tokenImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('image/png')
|
||||
})
|
||||
|
||||
const invalidTokenImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb49',
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum',
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereun',
|
||||
'http://127.0.0.1:3000/api/image/tokens/potato/?potato=1',
|
||||
]
|
||||
|
||||
test.each(invalidTokenImageUrl)('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
1
functions/babel.config.js
Normal file
1
functions/babel.config.js
Normal file
@@ -0,0 +1 @@
|
||||
export const presets = ['@babel/preset-env']
|
||||
20
functions/client.ts
Normal file
20
functions/client.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client'
|
||||
const GRAPHQL_ENDPOINT = 'https://api.uniswap.org/v1/graphql'
|
||||
|
||||
//TODO: Figure out how to make ApolloClient global variable
|
||||
export default new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
uri: GRAPHQL_ENDPOINT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Origin: 'https://app.uniswap.org',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36',
|
||||
},
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
})
|
||||
60
functions/components/metaTagInjector.test.ts
Normal file
60
functions/components/metaTagInjector.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { MetaTagInjector } from './metaTagInjector'
|
||||
|
||||
test('should append meta tag to element', () => {
|
||||
const element = {
|
||||
append: jest.fn(),
|
||||
} as unknown as Element
|
||||
const property = 'property'
|
||||
const content = 'content'
|
||||
const injector = new MetaTagInjector(
|
||||
{
|
||||
title: 'test',
|
||||
url: 'testUrl',
|
||||
image: 'testImage',
|
||||
description: 'testDescription',
|
||||
},
|
||||
new Request('http://localhost')
|
||||
)
|
||||
injector.append(element, property, content)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="${property}" content="${content}"/>`, { html: true })
|
||||
|
||||
injector.element(element)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:title" content="test"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:description" content="testDescription"/>`, {
|
||||
html: true,
|
||||
})
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image" content="testImage"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image:width" content="1200"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image:height" content="630"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image:alt" content="test"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:type" content="website"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:url" content="testUrl"/>`, { html: true })
|
||||
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:card" content="summary_large_image"/>`, {
|
||||
html: true,
|
||||
})
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:title" content="test"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:image" content="testImage"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:image:alt" content="test"/>`, { html: true })
|
||||
|
||||
expect(element.append).toHaveBeenCalledTimes(13)
|
||||
})
|
||||
|
||||
test('should pass through header blocked paths', () => {
|
||||
const element = {
|
||||
append: jest.fn(),
|
||||
} as unknown as Element
|
||||
const request = new Request('http://localhost')
|
||||
request.headers.set('x-blocked-paths', '/')
|
||||
const injector = new MetaTagInjector(
|
||||
{
|
||||
title: 'test',
|
||||
url: 'testUrl',
|
||||
image: 'testImage',
|
||||
description: 'testDescription',
|
||||
},
|
||||
request
|
||||
)
|
||||
injector.element(element)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="x:blocked-paths" content="/"/>`, { html: true })
|
||||
})
|
||||
47
functions/components/metaTagInjector.ts
Normal file
47
functions/components/metaTagInjector.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
type MetaTagInjectorInput = {
|
||||
title: string
|
||||
image?: string
|
||||
url: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener class for Cloudflare's HTMLRewriter {@link https://developers.cloudflare.com/workers/runtime-apis/html-rewriter}
|
||||
* to inject meta tags into the <head> of an HTML document.
|
||||
*/
|
||||
export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
|
||||
constructor(private input: MetaTagInjectorInput, private request: Request) {}
|
||||
|
||||
append(element: Element, property: string, content: string) {
|
||||
element.append(`<meta property="${property}" content="${content}"/>`, { html: true })
|
||||
}
|
||||
|
||||
element(element: Element) {
|
||||
//Open Graph Tags
|
||||
this.append(element, 'og:title', this.input.title)
|
||||
if (this.input.description) {
|
||||
this.append(element, 'og:description', this.input.description)
|
||||
}
|
||||
if (this.input.image) {
|
||||
this.append(element, 'og:image', this.input.image)
|
||||
this.append(element, 'og:image:width', '1200')
|
||||
this.append(element, 'og:image:height', '630')
|
||||
this.append(element, 'og:image:alt', this.input.title)
|
||||
}
|
||||
this.append(element, 'og:type', 'website')
|
||||
this.append(element, 'og:url', this.input.url)
|
||||
|
||||
//Twitter Tags
|
||||
this.append(element, 'twitter:card', 'summary_large_image')
|
||||
this.append(element, 'twitter:title', this.input.title)
|
||||
if (this.input.image) {
|
||||
this.append(element, 'twitter:image', this.input.image)
|
||||
this.append(element, 'twitter:image:alt', this.input.title)
|
||||
}
|
||||
|
||||
const blockedPaths = this.request.headers.get('x-blocked-paths')
|
||||
if (blockedPaths) {
|
||||
this.append(element, 'x:blocked-paths', blockedPaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
2
functions/constants.ts
Normal file
2
functions/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const WATERMARK_URL = 'https://app.uniswap.org/images/324x74_App_Watermark.png'
|
||||
export const CHECK_URL = 'https://app.uniswap.org/images/54x54_Verified_Check.svg'
|
||||
22
functions/default.test.ts
Normal file
22
functions/default.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const defaultUrls = ['http://127.0.0.1:3000/', 'http://127.0.0.1:3000/swap', 'http://127.0.0.1:3000/pools']
|
||||
|
||||
test.each(defaultUrls)('should inject metadata for valid collections', async (defaultUrl) => {
|
||||
const body = await fetch(new Request(defaultUrl)).then((res) => res.text())
|
||||
expect(body).toContain(`<meta property="og:title" content="Uniswap Interface"/>`)
|
||||
expect(body).toContain(
|
||||
`<meta property="og:description" content="Swap or provide liquidity on the Uniswap Protocol"/>`
|
||||
)
|
||||
expect(body).toContain(
|
||||
`<meta property="og:image" content="http://127.0.0.1:3000/images/1200x630_Rich_Link_Preview_Image.png"/>`
|
||||
)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="Uniswap Interface"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="Uniswap Interface"/>`)
|
||||
expect(body).toContain(
|
||||
`<meta property="twitter:image" content="http://127.0.0.1:3000/images/1200x630_Rich_Link_Preview_Image.png"/>`
|
||||
)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="Uniswap Interface"/>`)
|
||||
})
|
||||
10
functions/jest.config.json
Normal file
10
functions/jest.config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"setupFilesAfterEnv": ["<rootDir>/setupAfterEnv.ts"],
|
||||
"preset": "ts-jest",
|
||||
"transform": {
|
||||
"'^.+\\.(ts|tsx)?$'": "ts-jest",
|
||||
"^.+\\.(js|jsx)$": "babel-jest"
|
||||
},
|
||||
"testTimeout": 360000,
|
||||
"cacheDirectory": "../node_modules/.cache/cloud-jest"
|
||||
}
|
||||
15
functions/nfts/asset/[[index]].ts
Normal file
15
functions/nfts/asset/[[index]].ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import getAsset from '../../utils/getAsset'
|
||||
import { getMetadataRequest } from '../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const collectionAddress = index[0]?.toString()
|
||||
const tokenId = index[1]?.toString()
|
||||
return getMetadataRequest(res, request, () => getAsset(collectionAddress, tokenId, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
445
functions/nfts/asset/__snapshots__/nft.test.ts.snap
Normal file
445
functions/nfts/asset/__snapshots__/nft.test.ts.snap
Normal file
@@ -0,0 +1,445 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for valid assets 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Azuki #2550"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki #2550"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki #2550"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="twitter:image:alt" content="Azuki #2550"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid assets 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club #3735"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club #3735"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club #3735"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club #3735"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid assets 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="CryptoPunk #3947"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CryptoPunk #3947"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CryptoPunk #3947"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="twitter:image:alt" content="CryptoPunk #3947"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
62
functions/nfts/asset/nft.test.ts
Normal file
62
functions/nfts/asset/nft.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
const assets = [
|
||||
{
|
||||
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
assetId: '2550',
|
||||
collectionName: 'Azuki',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550',
|
||||
},
|
||||
{
|
||||
address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
assetId: '3735',
|
||||
collectionName: 'Bored Ape Yacht Club',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735',
|
||||
},
|
||||
{
|
||||
address: '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb',
|
||||
assetId: '3947',
|
||||
collectionName: 'CryptoPunk',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947',
|
||||
},
|
||||
]
|
||||
|
||||
test.each(assets)('should inject metadata for valid assets', async (nft) => {
|
||||
const url = 'http://127.0.0.1:3000/nfts/asset/' + nft.address + '/' + nft.assetId
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).not.toContain(`<meta property="og:description"`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${nft.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${nft.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
})
|
||||
|
||||
const invalidAssets = [
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/100000',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/-1',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//2550',
|
||||
]
|
||||
|
||||
test.each(invalidAssets)('should not inject metadata for invalid asset calls', async (url) => {
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
})
|
||||
14
functions/nfts/collection/[index].ts
Normal file
14
functions/nfts/collection/[index].ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import getCollection from '../../utils/getCollection'
|
||||
import { getMetadataRequest } from '../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const collectionAddress = index?.toString()
|
||||
return getMetadataRequest(res, request, () => getCollection(collectionAddress, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
445
functions/nfts/collection/__snapshots__/collection.test.ts.snap
Normal file
445
functions/nfts/collection/__snapshots__/collection.test.ts.snap
Normal file
@@ -0,0 +1,445 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for collections 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Azuki on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="twitter:image:alt" content="Azuki on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for collections 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for collections 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Basel var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 535;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Medium.woff2') format('woff2'),
|
||||
url('/fonts/Basel-Medium.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Basel';
|
||||
font-weight: 485;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src:
|
||||
url('/fonts/Basel-Book.woff') format('woff2'),
|
||||
url('/fonts/Basel-Book.woff') format('woff');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Basel', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
text-rendering: optimizeLegibility !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="twitter:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user