Add MetaMask Swaps (#9482)
1
.storybook/images/0x.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M24.9,93.22a49.86,49.86,0,0,0,52-1.09A50.18,50.18,0,0,0,88.32,82.07c-2.72-3.79-5.62-7.7-8.68-11.68-.82-1.07-1.67-2.15-2.52-3.24A32.27,32.27,0,0,1,63.55,79.09L55,70.73Z"/><path d="M6.63,25.16A49.87,49.87,0,0,0,7.88,76.89a50.54,50.54,0,0,0,10,11.41c3.79-2.72,7.71-5.62,11.69-8.68l3.24-2.51A32.22,32.22,0,0,1,20.91,63.54l8.42-8.62Z"/><path d="M75.12,6.79a49.87,49.87,0,0,0-52,1.09,50.54,50.54,0,0,0-11.41,10c2.72,3.79,5.62,7.71,8.68,11.69.82,1.07,1.67,2.16,2.51,3.24A32.31,32.31,0,0,1,36.46,20.9h0l8.05,7.85Z"/><path d="M93.48,74.63a49.82,49.82,0,0,0-1.35-51.5A50.18,50.18,0,0,0,82.07,11.68c-3.79,2.72-7.7,5.62-11.68,8.68-1.07.82-2.15,1.67-3.24,2.52a32.27,32.27,0,0,1,12,13.6l0,.12L71.05,45Z"/></g></g></svg>
|
After Width: | Height: | Size: 872 B |
BIN
.storybook/images/AST.png
Normal file
After Width: | Height: | Size: 12 KiB |
1
.storybook/images/BAT_icon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140.85 136.33"><title>BAT_icon</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><polygon points="106.86 136.33 140.85 135.33 64.19 0 3.52 109.33 106.86 136.33" opacity="0.4"/><polygon points="127.05 110.66 63.78 74.22 0.72 110.66 127.05 110.66" fill="#662d91"/><polygon points="63.99 0.5 63.97 73.51 127.33 109.79 63.99 0.5" fill="#9e1f63"/><polygon points="0 109.94 63.45 73.81 63.61 0.8 0 109.94" fill="#ff5000"/><polygon points="63.84 44.79 38.4 88.48 89.86 88.48 63.84 44.79" fill="#fff" stroke="#ff5000" stroke-miterlimit="10" stroke-width="0.83"/></g></g></svg>
|
After Width: | Height: | Size: 652 B |
1
.storybook/images/CVL_token.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 91 91"><defs><style>.cls-1{fill:#231f20;stroke:#fff;stroke-miterlimit:10;}.cls-2{fill:#fff;}</style></defs><title>CVL_token</title><circle class="cls-1" cx="45.5" cy="45.5" r="45"/><g id="CIVIL_LOGO" data-name="CIVIL LOGO"><path class="cls-2" d="M29.2,45A17.57,17.57,0,0,1,47.05,27.06c6.48,0,11,2.54,14,6.76l-4.23,3c-2.35-3.1-5.26-4.79-10-4.79-7,0-12,5.54-12,13,0,7.61,5.16,13,12.21,13a12.5,12.5,0,0,0,10.52-5.07L61.8,55.8c-3.57,4.79-8.17,7.14-14.94,7.14A17.47,17.47,0,0,1,29.2,45Z"/></g></svg>
|
After Width: | Height: | Size: 580 B |
9
.storybook/images/gladius.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 328 328" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||||
|
<rect id="Page-5" serif:id="Page 5" x="-519" y="-228" width="1366" height="768" style="fill:none;"/>
|
||||||
|
<g id="Layer-1" serif:id="Layer 1">
|
||||||
|
<path d="M264.02,56.858c-31.022,-11.562 -64.566,-17.858 -99.594,-17.858c-35.029,0 -68.574,6.296 -99.595,17.858c-14.367,5.324 -28.162,11.733 -41.325,19.232c1.945,15.226 5.037,30.107 9.274,44.531c12.247,-7.899 25.299,-14.768 38.921,-20.377c28.561,-11.79 59.87,-18.316 92.725,-18.316c32.853,0 64.164,6.526 92.724,18.316c13.622,5.609 26.673,12.478 38.923,20.377c4.233,-14.424 7.326,-29.305 9.271,-44.531c-13.164,-7.499 -26.959,-13.908 -41.325,-19.232" style="fill:#0a2052;fill-rule:nonzero;"/>
|
||||||
|
<path d="M246.331,132.984l-0.056,0c-17.172,41.841 -45.734,77.901 -81.849,104.172c-36.117,-26.271 -64.68,-62.331 -81.852,-104.172l-0.055,0c-3.665,1.316 -7.27,2.69 -10.819,4.178c-9.731,4.007 -19.116,8.643 -28.161,13.853c17.915,42.813 46.02,80.362 81.221,109.609c12.364,10.303 25.642,19.575 39.666,27.647c14.022,-8.072 27.3,-17.344 39.664,-27.647c35.201,-29.247 63.305,-66.796 81.22,-109.609c-9.042,-5.21 -18.429,-9.846 -28.161,-13.853c-3.548,-1.488 -7.154,-2.862 -10.818,-4.178" style="fill:#31c8c4;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
49
.storybook/images/gnosis.svg
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="1055px"
|
||||||
|
height="680px"
|
||||||
|
viewBox="0 0 1055 680"
|
||||||
|
enable-background="new 0 0 1055 680"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="0.91 r13725"
|
||||||
|
sodipodi:docname="gnosis.svg"><metadata
|
||||||
|
id="metadata9"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs7" /><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="833"
|
||||||
|
inkscape:window-height="707"
|
||||||
|
id="namedview5"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.29383886"
|
||||||
|
inkscape:cx="527.5"
|
||||||
|
inkscape:cy="340"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="Layer_1" /><path
|
||||||
|
d="m 180.15745,575.08269 0,69.24629 c -18.04707,14.49955 -44.71746,24.43308 -69.10742,24.43308 -52.586837,0 -93.485772,-41.60718 -93.485772,-95.29343 0,-53.67766 41.415893,-94.75214 95.266662,-94.75214 25.40529,0 50.81059,10.20234 68.34766,26.31595 l -17.02592,23.3419 c -13.71275,-13.41573 -32.77515,-21.46517 -51.32174,-21.46517 -36.326471,0 -64.784723,29.25194 -64.784723,66.56069 0,37.57878 28.712665,66.84422 65.039133,66.84422 13.21438,0 28.20268,-4.83236 40.91171,-12.88916 l 0,-52.34223 26.16041,0 z m 178.59333,92.34271 -94.76364,-134.75262 0,134.75262 -29.96615,0 0,-187.90617 29.46777,0 95.26086,135.01652 0,-135.01652 29.47938,0 0,187.90617 -29.47822,0 z m 261.66675,-93.95677 c 0,53.68503 -41.14754,95.29343 -94.24552,95.29343 -53.09799,0 -94.25134,-41.60718 -94.25134,-95.29343 0,-53.94769 41.15451,-94.75214 94.25134,-94.75214 53.09798,0 94.24552,41.07448 94.24552,94.75214 z m -158.01492,0 c 0,37.32102 29.21801,66.84423 64.02381,66.84423 34.80696,0 63.2594,-29.52321 63.2594,-66.84423 0,-37.30874 -28.45244,-66.29679 -63.2594,-66.29679 -35.06138,0 -64.02381,28.98927 -64.02381,66.29679 z m 322.14853,-75.95293 -12.4616,27.90792 c -19.29938,-12.34788 -38.6127,-17.44905 -52.07452,-17.44905 -17.52546,0 -28.95198,6.98281 -28.95198,19.59581 0,41.06712 96.02177,19.06188 95.76852,86.7027 0,33.54915 -27.94246,54.21772 -67.06515,54.21772 -27.95291,0 -54.36191,-12.0717 -72.65874,-29.79078 l 12.95067,-27.38257 c 18.29218,17.7203 41.16845,27.38257 60.21109,27.38257 20.8398,0 33.2921,-8.31947 33.2921,-22.82024 0,-41.86985 -96.05197,-18.51077 -96.05197,-85.35377 0,-32.20634 26.17202,-52.34222 64.80331,-52.34222 23.10744,0 45.72233,7.77941 62.23827,19.33191 z m 48.7776,169.9097 0,-187.90617 29.97312,0 0,187.90617 -29.97312,0 z m 208.82086,-169.9097 -12.4616,27.90792 c -19.2994,-12.34788 -38.60573,-17.44905 -52.06755,-17.44905 -17.53243,0 -28.95197,6.98281 -28.95197,19.59581 0,41.06712 96.02172,19.06188 95.76622,86.7027 0,33.54915 -27.9471,54.21772 -67.06982,54.21772 -27.9471,0 -54.35495,-12.0717 -72.65875,-29.79078 l 12.95764,-27.38257 c 18.28522,17.7203 41.16845,27.38257 60.20645,27.38257 20.83168,0 33.29208,-8.31947 33.29208,-22.82024 0,-41.86985 -96.04033,-18.51077 -96.04033,-85.35377 0,-32.20634 26.16504,-52.34222 64.78936,-52.34222 23.11907,0 45.72927,7.77941 62.23827,19.33191 z M 673.44697,99.277115 660.77511,112.67812 c 10.21021,16.72364 13.20276,38.09921 6.34871,58.27927 -11.70533,34.44885 -47.62288,52.36799 -80.21679,40.00662 -4.60383,-1.73435 -8.89633,-3.99649 -12.84263,-6.67473 l -40.83272,43.13042 -35.95589,-37.98997 c -16.42416,11.61019 -37.64616,15.16358 -57.67045,7.56829 -33.54186,-12.70874 -50.96277,-51.72606 -38.94727,-87.14457 1.83665,-5.40312 4.26229,-10.38891 7.15145,-14.95124 l -15.32287,-16.189709 -2.96118,5.113439 c -16.0652,27.7103 -24.58978,59.55702 -24.66297,92.10582 -0.16961,97.82929 75.00655,177.56145 167.592,177.74679 l 0.3276,0 c 92.39841,0 167.72095,-79.43635 167.8952,-177.05943 0.0616,-32.55371 -8.34335,-64.44707 -24.27379,-92.21506 l -2.95654,-5.126945 z M 426.7813,134.95462 c -4.42725,6.07083 -7.08988,13.6649 -7.08988,21.92054 0,19.89039 15.25085,36.004 34.07625,36.004 7.81943,0 14.99527,-2.81448 20.74686,-7.49219 L 426.7813,134.95462 Z m 167.96258,47.47794 c 5.44955,3.88725 11.9911,6.14816 19.06355,6.14816 18.80682,0 34.07045,-16.11974 34.07045,-36.00399 0,-7.45905 -2.14683,-14.3854 -5.82014,-20.13465 l -47.31386,49.99048 z m -61.24501,38.08571 -138.568,-146.95321 5.3996,-6.059787 C 434.79008,28.82795 481.41738,7.5309355 531.60762,7.5309355 l 0.34038,0 C 582.66797,7.634039 631.27132,30.486199 665.33015,70.236289 l 5.20443,6.059787 L 533.49887,220.51827 Z M 417.69097,73.366218 533.54185,196.22757 647.94642,75.827201 C 617.32972,43.343454 575.48051,24.802003 531.91198,24.711174 l -0.30436,0 c -43.03299,0 -83.17217,17.206014 -113.91665,48.655044 z M 380.71975,315.13539 364.40479,314.00493 C 295.43096,309.26954 200.28512,291.3197 138.71716,232.71762 100.99781,196.82287 93.780142,162.70788 93.494363,161.26811 l -2.080611,-10.32508 251.008118,0 -1.24651,9.69541 c -1.14311,8.96264 -1.72745,18.02102 -1.75068,26.9137 -0.0674,40.43622 11.07916,79.57628 32.23959,113.19785 l 9.05548,14.3854 z m 304.05615,0 9.05548,-14.3854 c 21.04426,-33.40922 32.21404,-72.28539 32.28258,-112.41352 0.0116,-9.22654 -0.57156,-18.54145 -1.72629,-27.6968 l -1.2407,-9.69542 250.97792,0 -2.07364,10.32509 c -0.28578,1.43976 -7.51042,35.55476 -45.23441,71.44951 -61.56797,58.60085 -156.73937,76.55069 -225.72481,81.28731 l -16.31613,1.12923 z"
|
||||||
|
id="path3"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#00a6c4" /></svg>
|
After Width: | Height: | Size: 6.0 KiB |
1
.storybook/images/metamark.svg
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
.storybook/images/omg.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
29
.storybook/images/sai.svg
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="105px" height="105px" viewBox="0 0 105 105" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>icon</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<path d="M68.1775968,52.2619297 L53.250349,37.3346818 L38.3184692,52.2665616 L15.9241234,52.2700356 L53.2538229,14.9403361 L90.5719426,52.2584557 L68.1775968,52.2619297 Z" id="path-1"></path>
|
||||||
|
<filter x="-5.4%" y="-5.4%" width="110.7%" height="121.4%" filterUnits="objectBoundingBox" id="filter-2">
|
||||||
|
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="DAI_small" transform="translate(-37.000000, -37.000000)">
|
||||||
|
<g id="ICON_DAI" transform="translate(10.800000, 10.800000)">
|
||||||
|
<g id="icon" transform="translate(25.730000, 26.730000)">
|
||||||
|
<rect id="Rectangle" fill="#FFCC80" transform="translate(53.248033, 52.264246) rotate(-315.000000) translate(-53.248033, -52.264246) " x="16.3049794" y="15.3097285" width="73.8861073" height="73.9090343"></rect>
|
||||||
|
<polygon id="Rectangle-Copy" fill="#FFB74D" transform="translate(53.248033, 52.264246) rotate(-315.000000) translate(-53.248033, -52.264246) " points="16.3049794 15.3097285 90.1910867 15.3097285 66.3093749 65.3296405 16.3049794 89.2187628"></polygon>
|
||||||
|
<g id="Combined-Shape">
|
||||||
|
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
|
||||||
|
<use fill="#FCFCFC" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||||
|
</g>
|
||||||
|
<polygon id="Rectangle-Copy-4" fill-opacity="0.0299999993" fill="#000000" transform="translate(53.248033, 52.264246) rotate(-315.000000) translate(-53.248033, -52.264246) " points="16.3049794 15.3097285 90.1910867 15.3097285 90.1910867 89.2187628"></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
BIN
.storybook/images/tether_usd.png
Normal file
After Width: | Height: | Size: 913 B |
BIN
.storybook/images/wbtc.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
.storybook/images/wed.png
Normal file
After Width: | Height: | Size: 259 KiB |
@ -114,6 +114,10 @@
|
|||||||
"amount": {
|
"amount": {
|
||||||
"message": "Amount"
|
"message": "Amount"
|
||||||
},
|
},
|
||||||
|
"amountInEth": {
|
||||||
|
"message": "$1 ETH",
|
||||||
|
"description": "Displays an eth amount to the user. $1 is a decimal number"
|
||||||
|
},
|
||||||
"amountWithColon": {
|
"amountWithColon": {
|
||||||
"message": "Amount:"
|
"message": "Amount:"
|
||||||
},
|
},
|
||||||
@ -125,6 +129,9 @@
|
|||||||
"message": "MetaMask",
|
"message": "MetaMask",
|
||||||
"description": "The name of the application"
|
"description": "The name of the application"
|
||||||
},
|
},
|
||||||
|
"approvalTxGasCost": {
|
||||||
|
"message": "Approval Tx Gas Cost"
|
||||||
|
},
|
||||||
"approve": {
|
"approve": {
|
||||||
"message": "Approve spend limit"
|
"message": "Approve spend limit"
|
||||||
},
|
},
|
||||||
@ -639,6 +646,10 @@
|
|||||||
"externalExtension": {
|
"externalExtension": {
|
||||||
"message": "External Extension"
|
"message": "External Extension"
|
||||||
},
|
},
|
||||||
|
"extraApprovalGas": {
|
||||||
|
"message": "+$1 approval gas",
|
||||||
|
"description": "Expresses an additional gas amount the user will have to pay, on top of some other displayed amount. $1 is a decimal amount of gas"
|
||||||
|
},
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Failed"
|
"message": "Failed"
|
||||||
},
|
},
|
||||||
@ -943,6 +954,9 @@
|
|||||||
"metamaskDescription": {
|
"metamaskDescription": {
|
||||||
"message": "Connecting you to Ethereum and the Decentralized Web."
|
"message": "Connecting you to Ethereum and the Decentralized Web."
|
||||||
},
|
},
|
||||||
|
"metamaskSwapsOfflineDescription": {
|
||||||
|
"message": "MetaMask Swaps is undergoing maintenance. Please check back later."
|
||||||
|
},
|
||||||
"metamaskVersion": {
|
"metamaskVersion": {
|
||||||
"message": "MetaMask Version"
|
"message": "MetaMask Version"
|
||||||
},
|
},
|
||||||
@ -1069,6 +1083,9 @@
|
|||||||
"off": {
|
"off": {
|
||||||
"message": "Off"
|
"message": "Off"
|
||||||
},
|
},
|
||||||
|
"offlineForMaintenance": {
|
||||||
|
"message": "Offline for maintenance"
|
||||||
|
},
|
||||||
"ok": {
|
"ok": {
|
||||||
"message": "Ok"
|
"message": "Ok"
|
||||||
},
|
},
|
||||||
@ -1082,6 +1099,9 @@
|
|||||||
"onlyAddTrustedNetworks": {
|
"onlyAddTrustedNetworks": {
|
||||||
"message": "A malicious Ethereum network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust."
|
"message": "A malicious Ethereum network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust."
|
||||||
},
|
},
|
||||||
|
"onlyAvailableOnMainnet": {
|
||||||
|
"message": "Only available on mainnet"
|
||||||
|
},
|
||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Only connect with sites you trust."
|
"message": "Only connect with sites you trust."
|
||||||
},
|
},
|
||||||
@ -1471,6 +1491,9 @@
|
|||||||
"speedUpTransaction": {
|
"speedUpTransaction": {
|
||||||
"message": "Speed up this transaction"
|
"message": "Speed up this transaction"
|
||||||
},
|
},
|
||||||
|
"spendLimitInsufficient": {
|
||||||
|
"message": "Spend limit insufficient"
|
||||||
|
},
|
||||||
"spendLimitInvalid": {
|
"spendLimitInvalid": {
|
||||||
"message": "Spend limit invalid; must be a positive number"
|
"message": "Spend limit invalid; must be a positive number"
|
||||||
},
|
},
|
||||||
@ -1532,6 +1555,265 @@
|
|||||||
"supportCenter": {
|
"supportCenter": {
|
||||||
"message": "Visit our Support Center"
|
"message": "Visit our Support Center"
|
||||||
},
|
},
|
||||||
|
"swap": {
|
||||||
|
"message": "Swap"
|
||||||
|
},
|
||||||
|
"swapAdvancedSlippageInfo": {
|
||||||
|
"message": "If the price changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting."
|
||||||
|
},
|
||||||
|
"swapAggregator": {
|
||||||
|
"message": "Aggregator"
|
||||||
|
},
|
||||||
|
"swapAmountReceived": {
|
||||||
|
"message": "Guaranteed amount"
|
||||||
|
},
|
||||||
|
"swapAmountReceivedInfo": {
|
||||||
|
"message": "This is the minimum amount you will receive. You may receive more depending on slippage."
|
||||||
|
},
|
||||||
|
"swapApproval": {
|
||||||
|
"message": "Approve $1 for swaps",
|
||||||
|
"description": "Used in the transaction display list to describe a transaction that is an approve call on a token that is to be swapped.. $1 is the symbol of a token that has been approved."
|
||||||
|
},
|
||||||
|
"swapApproveNeedMoreTokens": {
|
||||||
|
"message": "You need $1 more $2 to complete this swap",
|
||||||
|
"description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol."
|
||||||
|
},
|
||||||
|
"swapBuildQuotePlaceHolderText": {
|
||||||
|
"message": "No tokens available matching $1",
|
||||||
|
"description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text"
|
||||||
|
},
|
||||||
|
"swapCheckingQuote": {
|
||||||
|
"message": "Checking $1",
|
||||||
|
"description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap."
|
||||||
|
},
|
||||||
|
"swapCustom": {
|
||||||
|
"message": "custom"
|
||||||
|
},
|
||||||
|
"swapDecentralizedExchange": {
|
||||||
|
"message": "Decentralized exchange"
|
||||||
|
},
|
||||||
|
"swapEditLimit": {
|
||||||
|
"message": "Edit limit"
|
||||||
|
},
|
||||||
|
"swapEnableDescription": {
|
||||||
|
"message": "This is required and gives MetaMask permission to swap your $1.",
|
||||||
|
"description": "Gives the user info about the required approval transaction for swaps. $1 will be the symbol of a token being approved for swaps."
|
||||||
|
},
|
||||||
|
"swapEstimatedNetworkFee": {
|
||||||
|
"message": "Estimated network fee"
|
||||||
|
},
|
||||||
|
"swapEstimatedNetworkFees": {
|
||||||
|
"message": "Estimated network fees"
|
||||||
|
},
|
||||||
|
"swapEstimatedNetworkFeesInfo": {
|
||||||
|
"message": "This is an estimate of the network fee that will be used to complete your swap. The actual amount may change according to network conditions."
|
||||||
|
},
|
||||||
|
"swapEstimatedTime": {
|
||||||
|
"message": "Estimated time:"
|
||||||
|
},
|
||||||
|
"swapEstimatedTimeCalculating": {
|
||||||
|
"message": "Calculating..."
|
||||||
|
},
|
||||||
|
"swapEstimatedTimeFull": {
|
||||||
|
"message": "$1 $2",
|
||||||
|
"description": "This message shows bolded swapEstimatedTime message, which is substited for $1, followed by either the estimated remaining transaction time in mm:ss, or the swapEstimatedTimeCalculating message, which are substituted for $2."
|
||||||
|
},
|
||||||
|
"swapFailedErrorDescription": {
|
||||||
|
"message": "Your funds are safe and still available in your wallet."
|
||||||
|
},
|
||||||
|
"swapFailedErrorTitle": {
|
||||||
|
"message": "Swap failed"
|
||||||
|
},
|
||||||
|
"swapFetchingQuotesErrorDescription": {
|
||||||
|
"message": "Hmmm... something went wrong. Try again, or if errors persist, contact customer support."
|
||||||
|
},
|
||||||
|
"swapFetchingQuotesErrorTitle": {
|
||||||
|
"message": "Error fetching quotes"
|
||||||
|
},
|
||||||
|
"swapFetchingTokens": {
|
||||||
|
"message": "Fetching tokens..."
|
||||||
|
},
|
||||||
|
"swapFinalizing": {
|
||||||
|
"message": "Finalizing..."
|
||||||
|
},
|
||||||
|
"swapGetQuotes": {
|
||||||
|
"message": "Get quotes"
|
||||||
|
},
|
||||||
|
"swapHighSlippageWarning": {
|
||||||
|
"message": "Slippage amount is very high. Make sure you know what you are doing!"
|
||||||
|
},
|
||||||
|
"swapIntroLearnMoreHeader": {
|
||||||
|
"message": "Want to learn more?"
|
||||||
|
},
|
||||||
|
"swapIntroLearnMoreLink": {
|
||||||
|
"message": "Learn more about MetaMask Swaps"
|
||||||
|
},
|
||||||
|
"swapIntroLiquiditySourcesLabel": {
|
||||||
|
"message": "Liquidity sources include:"
|
||||||
|
},
|
||||||
|
"swapIntroPopupSubTitle": {
|
||||||
|
"message": "You can now swap tokens directly in your MetaMask wallet. MetaMask Swaps agglomerates multiple decentralized exchange aggregators, professional market makers, and individual DEXs to ensure MetaMask users always get the best price with the lowest network fees."
|
||||||
|
},
|
||||||
|
"swapIntroPopupTitle": {
|
||||||
|
"message": "Token swapping is here!"
|
||||||
|
},
|
||||||
|
"swapLearnMoreContractsAuditReview": {
|
||||||
|
"message": "Review our official contracts audit"
|
||||||
|
},
|
||||||
|
"swapLowSlippageError": {
|
||||||
|
"message": "Transaction may fail, max slippage too low."
|
||||||
|
},
|
||||||
|
"swapMaxNetworkFeeInfo": {
|
||||||
|
"message": "The Max network fee is the most you’ll pay to complete your transaction. The max fee helps ensure your Swap has the best chance of succeeding. MetaMask does not profit from network fees."
|
||||||
|
},
|
||||||
|
"swapMaxNetworkFees": {
|
||||||
|
"message": "Max network fee"
|
||||||
|
},
|
||||||
|
"swapMaxSlippage": {
|
||||||
|
"message": "Max slippage"
|
||||||
|
},
|
||||||
|
"swapMetaMaskFee": {
|
||||||
|
"message": "MetaMask fee"
|
||||||
|
},
|
||||||
|
"swapMetaMaskFeeDescription": {
|
||||||
|
"message": "A service fee of $1% is automatically factored into each quote, which supports ongoing development to make MetaMask even better.",
|
||||||
|
"description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number."
|
||||||
|
},
|
||||||
|
"swapMinimumAmountReceivingInfoTooltip": {
|
||||||
|
"message": "This is the minimum amount you will receive. You may receive more depending on slippage."
|
||||||
|
},
|
||||||
|
"swapNQuotesAvailable": {
|
||||||
|
"message": "$1 quotes available",
|
||||||
|
"description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen"
|
||||||
|
},
|
||||||
|
"swapNewQuoteIn": {
|
||||||
|
"message": "New quotes in $1",
|
||||||
|
"description": "Tells the user the amount of time until the currently displayed quotes are update. $1 is a time that is counting down from 1:00 to 0:00"
|
||||||
|
},
|
||||||
|
"swapOnceTransactionHasProcess": {
|
||||||
|
"message": "Your $1 will be added to your account once this transaction has processed.",
|
||||||
|
"description": "This message communicates the token that is being transferred. It is shown on the awaiting swap screen. The $1 will be a token symbol."
|
||||||
|
},
|
||||||
|
"swapProcessing": {
|
||||||
|
"message": "Processing"
|
||||||
|
},
|
||||||
|
"swapQuoteDetails": {
|
||||||
|
"message": "Quote details"
|
||||||
|
},
|
||||||
|
"swapQuoteDetailsSlippageInfo": {
|
||||||
|
"message": "If the price changes between the time your order is placed and confirmed it’s called \"slippage\". Your Swap will automatically cancel if slippage exceeds your \"max slippage\" setting."
|
||||||
|
},
|
||||||
|
"swapQuoteNofN": {
|
||||||
|
"message": "Quote $1 of $2",
|
||||||
|
"description": "A count of loaded quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of quotes to load."
|
||||||
|
},
|
||||||
|
"swapQuoteSource": {
|
||||||
|
"message": "Quote source"
|
||||||
|
},
|
||||||
|
"swapQuotesAreRefreshed": {
|
||||||
|
"message": "Quotes are refreshed often to reflect current market conditions."
|
||||||
|
},
|
||||||
|
"swapQuotesExpiredErrorDescription": {
|
||||||
|
"message": "Please request new quotes to get the latest rates."
|
||||||
|
},
|
||||||
|
"swapQuotesExpiredErrorTitle": {
|
||||||
|
"message": "Quotes timeout"
|
||||||
|
},
|
||||||
|
"swapQuotesNotAvailableErrorDescription": {
|
||||||
|
"message": "Try adjusting the amount or slippage settings and try again."
|
||||||
|
},
|
||||||
|
"swapQuotesNotAvailableErrorTitle": {
|
||||||
|
"message": "No quotes available"
|
||||||
|
},
|
||||||
|
"swapRate": {
|
||||||
|
"message": "Rate"
|
||||||
|
},
|
||||||
|
"swapReceiving": {
|
||||||
|
"message": "Receiving"
|
||||||
|
},
|
||||||
|
"swapRequestForQuotation": {
|
||||||
|
"message": "Request for quotation"
|
||||||
|
},
|
||||||
|
"swapSearchForAToken": {
|
||||||
|
"message": "Search for a token"
|
||||||
|
},
|
||||||
|
"swapSelect": {
|
||||||
|
"message": "Select"
|
||||||
|
},
|
||||||
|
"swapSelectAQuote": {
|
||||||
|
"message": "Select a quote"
|
||||||
|
},
|
||||||
|
"swapSelectQuotePopoverDescription": {
|
||||||
|
"message": "Below are all the quotes gathered from multiple liquidity sources."
|
||||||
|
},
|
||||||
|
"swapSlippageTooLow": {
|
||||||
|
"message": "Slippage must be greater than zero"
|
||||||
|
},
|
||||||
|
"swapSource": {
|
||||||
|
"message": "Liquidity source"
|
||||||
|
},
|
||||||
|
"swapSourceInfo": {
|
||||||
|
"message": "We search multiple liquidity sources (exchanges, aggregators and professional market makers) to find the best rates and lowest network fees."
|
||||||
|
},
|
||||||
|
"swapStartSwapping": {
|
||||||
|
"message": "Start swapping"
|
||||||
|
},
|
||||||
|
"swapSwapFrom": {
|
||||||
|
"message": "Swap from"
|
||||||
|
},
|
||||||
|
"swapSwapTo": {
|
||||||
|
"message": "Swap to"
|
||||||
|
},
|
||||||
|
"swapThisWillAllowApprove": {
|
||||||
|
"message": "This will allow $1 to be swapped."
|
||||||
|
},
|
||||||
|
"swapTokenAvailable": {
|
||||||
|
"message": "Your $1 has been added to your account.",
|
||||||
|
"description": "This message is shown after a swap is successful and communicates the exact amount of tokens the user has received for a swap. The $1 is a decimal number of tokens followed by the token symbol."
|
||||||
|
},
|
||||||
|
"swapTokenToToken": {
|
||||||
|
"message": "Swap $1 to $2",
|
||||||
|
"description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap."
|
||||||
|
},
|
||||||
|
"swapTransactionComplete": {
|
||||||
|
"message": "Transaction complete"
|
||||||
|
},
|
||||||
|
"swapUnknown": {
|
||||||
|
"message": "Unknown"
|
||||||
|
},
|
||||||
|
"swapViewToken": {
|
||||||
|
"message": "View $1"
|
||||||
|
},
|
||||||
|
"swapYourTokenBalance": {
|
||||||
|
"message": "$1 $2 available to swap",
|
||||||
|
"description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol"
|
||||||
|
},
|
||||||
|
"swapZeroSlippage": {
|
||||||
|
"message": "0% Slippage"
|
||||||
|
},
|
||||||
|
"swapsAdvancedOptions": {
|
||||||
|
"message": "Advanced Options"
|
||||||
|
},
|
||||||
|
"swapsAlmostDone": {
|
||||||
|
"message": "Almost done..."
|
||||||
|
},
|
||||||
|
"swapsBestQuote": {
|
||||||
|
"message": "Best quote"
|
||||||
|
},
|
||||||
|
"swapsConvertToAbout": {
|
||||||
|
"message": "Convert $1 to about",
|
||||||
|
"description": "This message is part of a quote for a swap. The $1 is the amount being converted, and the amount it is being swapped for is below this message"
|
||||||
|
},
|
||||||
|
"swapsMaxSlippage": {
|
||||||
|
"message": "Max slippage"
|
||||||
|
},
|
||||||
|
"swapsNotEnoughForTx": {
|
||||||
|
"message": "Not enough $1 to complete this transaction",
|
||||||
|
"description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol"
|
||||||
|
},
|
||||||
|
"swapsViewInActivity": {
|
||||||
|
"message": "View in activity"
|
||||||
|
},
|
||||||
"switchNetworks": {
|
"switchNetworks": {
|
||||||
"message": "Switch Networks"
|
"message": "Switch Networks"
|
||||||
},
|
},
|
||||||
@ -1577,6 +1859,9 @@
|
|||||||
"terms": {
|
"terms": {
|
||||||
"message": "Terms of Use"
|
"message": "Terms of Use"
|
||||||
},
|
},
|
||||||
|
"termsOfService": {
|
||||||
|
"message": "Terms of Service"
|
||||||
|
},
|
||||||
"testFaucet": {
|
"testFaucet": {
|
||||||
"message": "Test Faucet"
|
"message": "Test Faucet"
|
||||||
},
|
},
|
||||||
|
9
app/images/black-eth-logo.svg
Normal file
After Width: | Height: | Size: 11 KiB |
3
app/images/icons/swap2.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="12" height="16" viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.35075 15.9844C3.77098 15.9844 4.07114 15.6722 4.07114 15.2351L4.07114 3.9961L5.51193 3.9961C6.1723 3.9961 6.47246 3.21561 6.02221 2.74732L3.62089 0.249757C3.35074 -0.0624391 2.9005 -0.0624391 2.63035 0.249757L0.229027 2.74732C-0.251237 3.21561 0.0789448 3.9961 0.709292 3.9961L2.15008 3.9961L2.15008 15.2351C2.15008 15.6722 2.48027 15.9844 2.87048 15.9844L3.35075 15.9844ZM7.91325 0.749269L7.91325 11.9883L6.47246 11.9883C5.84212 11.9883 5.51193 12.8 5.9922 13.2683L8.39352 15.7659C8.66367 16.078 9.11392 16.078 9.38406 15.7659L11.7854 13.2683C12.2356 12.8 11.9355 11.9883 11.2751 11.9883L9.83431 11.9883L9.83431 0.749269C9.83431 0.343415 9.53415 5.36924e-07 9.11391 5.55292e-07L8.63365 5.76285e-07C8.24344 5.93342e-07 7.91325 0.343415 7.91325 0.749269Z" fill="#D6D9DC"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 887 B |
132
app/images/source-logos-all.svg
Normal file
After Width: | Height: | Size: 118 KiB |
@ -239,6 +239,7 @@ function setupController (initState, initLangCode) {
|
|||||||
initLangCode,
|
initLangCode,
|
||||||
// platform specific api
|
// platform specific api
|
||||||
platform,
|
platform,
|
||||||
|
extension,
|
||||||
getRequestAccountTabIds: () => {
|
getRequestAccountTabIds: () => {
|
||||||
return requestAccountTabIds
|
return requestAccountTabIds
|
||||||
},
|
},
|
||||||
|
@ -22,6 +22,7 @@ export default class AppStateController extends EventEmitter {
|
|||||||
this.store = new ObservableStore({
|
this.store = new ObservableStore({
|
||||||
timeoutMinutes: 0,
|
timeoutMinutes: 0,
|
||||||
connectedStatusPopoverHasBeenShown: true,
|
connectedStatusPopoverHasBeenShown: true,
|
||||||
|
swapsWelcomeMessageHasBeenShown: false,
|
||||||
defaultHomeActiveTabName: null, ...initState,
|
defaultHomeActiveTabName: null, ...initState,
|
||||||
})
|
})
|
||||||
this.timer = null
|
this.timer = null
|
||||||
@ -110,6 +111,15 @@ export default class AppStateController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record that the user has seen the swap screen welcome message
|
||||||
|
*/
|
||||||
|
setSwapsWelcomeMessageHasBeenShown () {
|
||||||
|
this.store.updateState({
|
||||||
|
swapsWelcomeMessageHasBeenShown: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the last active time to the current time
|
* Sets the last active time to the current time
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
|
620
app/scripts/controllers/swaps.js
Normal file
@ -0,0 +1,620 @@
|
|||||||
|
import { ethers } from 'ethers'
|
||||||
|
import log from 'loglevel'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import ObservableStore from 'obs-store'
|
||||||
|
import { mapValues } from 'lodash'
|
||||||
|
import abi from 'human-standard-token-abi'
|
||||||
|
import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util'
|
||||||
|
import { calcGasTotal } from '../../../ui/app/pages/send/send.utils'
|
||||||
|
import { conversionUtil } from '../../../ui/app/helpers/utils/conversion-util'
|
||||||
|
import {
|
||||||
|
ETH_SWAPS_TOKEN_ADDRESS,
|
||||||
|
DEFAULT_ERC20_APPROVE_GAS,
|
||||||
|
QUOTES_EXPIRED_ERROR,
|
||||||
|
QUOTES_NOT_AVAILABLE_ERROR,
|
||||||
|
} from '../../../ui/app/helpers/constants/swaps'
|
||||||
|
import {
|
||||||
|
fetchTradesInfo as defaultFetchTradesInfo,
|
||||||
|
fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness,
|
||||||
|
} from '../../../ui/app/pages/swaps/swaps.util'
|
||||||
|
|
||||||
|
const METASWAP_ADDRESS = '0x016B4bf68d421147c06f1b8680602c5bf0Df91A8'
|
||||||
|
|
||||||
|
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
|
||||||
|
const MAX_GAS_LIMIT = 2500000
|
||||||
|
|
||||||
|
// To ensure that our serves are not spammed if MetaMask is left idle, we limit the number of fetches for quotes that are made on timed intervals.
|
||||||
|
// 3 seems to be an appropriate balance of giving users the time they need when MetaMask is not left idle, and turning polling off when it is.
|
||||||
|
const POLL_COUNT_LIMIT = 3
|
||||||
|
|
||||||
|
function calculateGasEstimateWithRefund (maxGas = MAX_GAS_LIMIT, estimatedRefund = 0, estimatedGas = 0) {
|
||||||
|
const maxGasMinusRefund = new BigNumber(
|
||||||
|
maxGas,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.minus(estimatedRefund, 10)
|
||||||
|
|
||||||
|
const gasEstimateWithRefund = maxGasMinusRefund.lt(
|
||||||
|
estimatedGas,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
? maxGasMinusRefund.toString(16)
|
||||||
|
: estimatedGas
|
||||||
|
|
||||||
|
return gasEstimateWithRefund
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the amount of time to wait, after successfully fetching quotes and their gas estimates, before fetching for new quotes
|
||||||
|
const QUOTE_POLLING_INTERVAL = 50 * 1000
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
swapsState: {
|
||||||
|
quotes: {},
|
||||||
|
fetchParams: null,
|
||||||
|
tokens: null,
|
||||||
|
tradeTxId: null,
|
||||||
|
approveTxId: null,
|
||||||
|
maxMode: false,
|
||||||
|
quotesLastFetched: null,
|
||||||
|
customMaxGas: '',
|
||||||
|
customGasPrice: null,
|
||||||
|
selectedAggId: null,
|
||||||
|
customApproveTxData: '',
|
||||||
|
errorKey: '',
|
||||||
|
topAggId: null,
|
||||||
|
routeState: '',
|
||||||
|
swapsFeatureIsLive: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SwapsController {
|
||||||
|
constructor ({
|
||||||
|
getBufferedGasLimit,
|
||||||
|
provider,
|
||||||
|
getProviderConfig,
|
||||||
|
tokenRatesStore,
|
||||||
|
fetchTradesInfo = defaultFetchTradesInfo,
|
||||||
|
fetchSwapsFeatureLiveness = defaultFetchSwapsFeatureLiveness,
|
||||||
|
}) {
|
||||||
|
this.store = new ObservableStore({
|
||||||
|
swapsState: { ...initialState.swapsState },
|
||||||
|
})
|
||||||
|
|
||||||
|
this._fetchTradesInfo = fetchTradesInfo
|
||||||
|
this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness
|
||||||
|
|
||||||
|
this.getBufferedGasLimit = getBufferedGasLimit
|
||||||
|
this.tokenRatesStore = tokenRatesStore
|
||||||
|
|
||||||
|
this.pollCount = 0
|
||||||
|
this.getProviderConfig = getProviderConfig
|
||||||
|
|
||||||
|
this.ethersProvider = new ethers.providers.Web3Provider(provider)
|
||||||
|
|
||||||
|
this._setupSwapsLivenessFetching()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once quotes are fetched, we poll for new ones to keep the quotes up to date. Market and aggregator contract conditions can change fast enough
|
||||||
|
// that quotes will no longer be available after 1 or 2 minutes. When fetchAndSetQuotes is first called it, receives fetch that parameters are stored in
|
||||||
|
// state. These stored parameters are used on subsequent calls made during polling.
|
||||||
|
// Note: we stop polling after 3 requests, until new quotes are explicitly asked for. The logic that enforces that maximum is in the body of fetchAndSetQuotes
|
||||||
|
pollForNewQuotes () {
|
||||||
|
this.pollingTimeout = setTimeout(() => {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.fetchAndSetQuotes(swapsState.fetchParams, swapsState.fetchParams.metaData, true)
|
||||||
|
}, QUOTE_POLLING_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPollingForQuotes () {
|
||||||
|
clearTimeout(this.pollingTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAndSetQuotes (fetchParams, fetchParamsMetaData = {}, isPolledRequest) {
|
||||||
|
if (!fetchParams) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every time we get a new request that is not from the polling, we reset the poll count so we can poll for up to three more sets of quotes with these new params.
|
||||||
|
if (!isPolledRequest) {
|
||||||
|
this.pollCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are any pending poll requests, clear them so that they don't get call while this new fetch is in process
|
||||||
|
clearTimeout(this.pollingTimeout)
|
||||||
|
|
||||||
|
if (!isPolledRequest) {
|
||||||
|
this.setSwapsErrorKey('')
|
||||||
|
}
|
||||||
|
let newQuotes = await this._fetchTradesInfo(fetchParams)
|
||||||
|
|
||||||
|
newQuotes = mapValues(newQuotes, (quote) => ({
|
||||||
|
...quote,
|
||||||
|
sourceTokenInfo: fetchParamsMetaData.sourceTokenInfo,
|
||||||
|
destinationTokenInfo: fetchParamsMetaData.destinationTokenInfo,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const quotesLastFetched = Date.now()
|
||||||
|
|
||||||
|
let approvalRequired = false
|
||||||
|
if (fetchParams.sourceToken !== ETH_SWAPS_TOKEN_ADDRESS && Object.values(newQuotes).length) {
|
||||||
|
const allowance = await this._getERC20Allowance(
|
||||||
|
fetchParams.sourceToken,
|
||||||
|
fetchParams.fromAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
// For a user to be able to swap a token, they need to have approved the MetaSwap contract to withdraw that token.
|
||||||
|
// _getERC20Allowance() returns the amount of the token they have approved for withdrawal. If that amount is greater
|
||||||
|
// than 0, it means that approval has already occured and is not needed. Otherwise, for tokens to be swapped, a new
|
||||||
|
// call of the ERC-20 approve method is required.
|
||||||
|
approvalRequired = allowance.eq(0)
|
||||||
|
if (!approvalRequired) {
|
||||||
|
newQuotes = mapValues(newQuotes, (quote) => ({
|
||||||
|
...quote,
|
||||||
|
approvalNeeded: null,
|
||||||
|
}))
|
||||||
|
} else if (!isPolledRequest) {
|
||||||
|
const { gasLimit: approvalGas } = await this.timedoutGasReturn({
|
||||||
|
...Object.values(newQuotes)[0].approvalNeeded,
|
||||||
|
gas: DEFAULT_ERC20_APPROVE_GAS,
|
||||||
|
})
|
||||||
|
|
||||||
|
newQuotes = mapValues(newQuotes, (quote) => ({
|
||||||
|
...quote,
|
||||||
|
approvalNeeded: {
|
||||||
|
...quote.approvalNeeded,
|
||||||
|
gas: approvalGas || DEFAULT_ERC20_APPROVE_GAS,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const topAggIdData = await this._findTopQuoteAggId(newQuotes)
|
||||||
|
let { topAggId } = topAggIdData
|
||||||
|
const { isBest } = topAggIdData
|
||||||
|
|
||||||
|
if (isBest) {
|
||||||
|
newQuotes[topAggId].isBestQuote = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can reduce time on the loading screen by only doing this after the
|
||||||
|
// loading screen and best quote have rendered.
|
||||||
|
if (!approvalRequired && !fetchParams?.balanceError) {
|
||||||
|
newQuotes = await this.getAllQuotesWithGasEstimates(newQuotes)
|
||||||
|
|
||||||
|
if (fetchParamsMetaData.maxMode && fetchParams.sourceToken === ETH_SWAPS_TOKEN_ADDRESS) {
|
||||||
|
newQuotes = await this._modifyValuesForMaxEthMode(newQuotes, fetchParamsMetaData.accountBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.values(newQuotes).length === 0) {
|
||||||
|
this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)
|
||||||
|
} else {
|
||||||
|
const {
|
||||||
|
topAggId: topAggIdAfterGasEstimates,
|
||||||
|
isBest: isBestAfterGasEstimates,
|
||||||
|
} = await this._findTopQuoteAggId(newQuotes)
|
||||||
|
topAggId = topAggIdAfterGasEstimates
|
||||||
|
if (isBestAfterGasEstimates) {
|
||||||
|
newQuotes = mapValues(newQuotes, (quote) => ({ ...quote, isBestQuote: false }))
|
||||||
|
newQuotes[topAggId].isBestQuote = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
let { selectedAggId } = swapsState
|
||||||
|
if (!newQuotes[selectedAggId]) {
|
||||||
|
selectedAggId = null
|
||||||
|
}
|
||||||
|
this.store.updateState({
|
||||||
|
swapsState: {
|
||||||
|
...swapsState,
|
||||||
|
quotes: newQuotes,
|
||||||
|
fetchParams: { ...fetchParams, metaData: fetchParamsMetaData },
|
||||||
|
quotesLastFetched,
|
||||||
|
selectedAggId,
|
||||||
|
topAggId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// We only want to do up to a maximum of three requests from polling.
|
||||||
|
this.pollCount += 1
|
||||||
|
if (this.pollCount < POLL_COUNT_LIMIT + 1) {
|
||||||
|
this.pollForNewQuotes()
|
||||||
|
} else {
|
||||||
|
this.resetPostFetchState()
|
||||||
|
this.setSwapsErrorKey(QUOTES_EXPIRED_ERROR)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [newQuotes, topAggId]
|
||||||
|
}
|
||||||
|
|
||||||
|
safeRefetchQuotes () {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
if (!this.pollingTimeout && swapsState.fetchParams) {
|
||||||
|
this.fetchAndSetQuotes(swapsState.fetchParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedQuoteAggId (selectedAggId) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({ swapsState: { ...swapsState, selectedAggId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
setSwapsTokens (tokens) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({ swapsState: { ...swapsState, tokens } })
|
||||||
|
}
|
||||||
|
|
||||||
|
setSwapsErrorKey (errorKey) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({ swapsState: { ...swapsState, errorKey } })
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllQuotesWithGasEstimates (quotes) {
|
||||||
|
const quoteGasData = await Promise.all(
|
||||||
|
Object.values(quotes).map(async (quote) => {
|
||||||
|
const { gasLimit, simulationFails } = await this.timedoutGasReturn({
|
||||||
|
...quote.trade,
|
||||||
|
gas: `0x${(quote.maxGas || MAX_GAS_LIMIT).toString(16)}`,
|
||||||
|
})
|
||||||
|
return [gasLimit, simulationFails, quote.aggregator]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const newQuotes = {}
|
||||||
|
quoteGasData.forEach(([gasLimit, simulationFails, aggId]) => {
|
||||||
|
if (gasLimit && !simulationFails) {
|
||||||
|
const gasEstimateWithRefund = calculateGasEstimateWithRefund(quotes[aggId].maxGas, quotes[aggId].estimatedRefund, gasLimit)
|
||||||
|
|
||||||
|
newQuotes[aggId] = {
|
||||||
|
...quotes[aggId],
|
||||||
|
gasEstimate: gasLimit,
|
||||||
|
gasEstimateWithRefund,
|
||||||
|
}
|
||||||
|
} else if (quotes[aggId].approvalNeeded) {
|
||||||
|
// If gas estimation fails, but an ERC-20 approve is needed, then we do not add any estimate property to the quote object
|
||||||
|
// Such quotes will rely on the maxGas and averageGas properties from the api
|
||||||
|
newQuotes[aggId] = quotes[aggId]
|
||||||
|
}
|
||||||
|
// If gas estimation fails and no approval is needed, then we filter that quote out, so that it is not shown to the user
|
||||||
|
})
|
||||||
|
return newQuotes
|
||||||
|
}
|
||||||
|
|
||||||
|
timedoutGasReturn (tradeTxParams) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let gasTimedOut = false
|
||||||
|
|
||||||
|
const gasTimeout = setTimeout(() => {
|
||||||
|
gasTimedOut = true
|
||||||
|
resolve({ gasLimit: null, simulationFails: true })
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
this.getBufferedGasLimit({ txParams: tradeTxParams }, 1)
|
||||||
|
.then(({ gasLimit, simulationFails }) => {
|
||||||
|
if (!gasTimedOut) {
|
||||||
|
clearTimeout(gasTimeout)
|
||||||
|
resolve({ gasLimit, simulationFails })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
log.error(e)
|
||||||
|
if (!gasTimedOut) {
|
||||||
|
clearTimeout(gasTimeout)
|
||||||
|
resolve({ gasLimit: null, simulationFails: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async setInitialGasEstimate (initialAggId, baseGasEstimate) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
|
||||||
|
const quoteToUpdate = { ...swapsState.quotes[initialAggId] }
|
||||||
|
|
||||||
|
const {
|
||||||
|
gasLimit: newGasEstimate,
|
||||||
|
simulationFails,
|
||||||
|
} = await this.timedoutGasReturn({
|
||||||
|
...quoteToUpdate.trade,
|
||||||
|
gas: baseGasEstimate,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (newGasEstimate && !simulationFails) {
|
||||||
|
const gasEstimateWithRefund = calculateGasEstimateWithRefund(quoteToUpdate.maxGas, quoteToUpdate.estimatedRefund, newGasEstimate)
|
||||||
|
|
||||||
|
quoteToUpdate.gasEstimate = newGasEstimate
|
||||||
|
quoteToUpdate.gasEstimateWithRefund = gasEstimateWithRefund
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.updateState({
|
||||||
|
swapsState: { ...swapsState, quotes: { ...swapsState.quotes, [initialAggId]: quoteToUpdate } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setApproveTxId (approveTxId) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({ swapsState: { ...swapsState, approveTxId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
setTradeTxId (tradeTxId) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({ swapsState: { ...swapsState, tradeTxId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaxMode (maxMode) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({ swapsState: { ...swapsState, maxMode } })
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuotesLastFetched (quotesLastFetched) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({ swapsState: { ...swapsState, quotesLastFetched } })
|
||||||
|
}
|
||||||
|
|
||||||
|
setSwapsTxGasPrice (gasPrice) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({
|
||||||
|
swapsState: { ...swapsState, customGasPrice: gasPrice },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setSwapsTxGasLimit (gasLimit) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({
|
||||||
|
swapsState: { ...swapsState, customMaxGas: gasLimit },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setCustomApproveTxData (data) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({
|
||||||
|
swapsState: { ...swapsState, customApproveTxData: data },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackgroundSwapRouteState (routeState) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({ swapsState: { ...swapsState, routeState } })
|
||||||
|
}
|
||||||
|
|
||||||
|
setSwapsLiveness (swapsFeatureIsLive) {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
this.store.updateState({ swapsState: { ...swapsState, swapsFeatureIsLive } })
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPostFetchState () {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
|
||||||
|
this.store.updateState({
|
||||||
|
swapsState: {
|
||||||
|
...initialState.swapsState,
|
||||||
|
tokens: swapsState.tokens,
|
||||||
|
fetchParams: swapsState.fetchParams,
|
||||||
|
swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
clearTimeout(this.pollingTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSwapsState () {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
|
||||||
|
this.store.updateState({
|
||||||
|
swapsState: { ...initialState.swapsState, tokens: swapsState.tokens, swapsFeatureIsLive: swapsState.swapsFeatureIsLive },
|
||||||
|
})
|
||||||
|
clearTimeout(this.pollingTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getEthersGasPrice () {
|
||||||
|
const ethersGasPrice = await this.ethersProvider.getGasPrice()
|
||||||
|
return ethersGasPrice.toHexString()
|
||||||
|
}
|
||||||
|
|
||||||
|
async _findTopQuoteAggId (quotes) {
|
||||||
|
const tokenConversionRates = this.tokenRatesStore.getState()
|
||||||
|
.contractExchangeRates
|
||||||
|
const {
|
||||||
|
swapsState: { customGasPrice },
|
||||||
|
} = this.store.getState()
|
||||||
|
|
||||||
|
if (!Object.values(quotes).length) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedGasPrice = customGasPrice || await this._getEthersGasPrice()
|
||||||
|
|
||||||
|
let topAggId = ''
|
||||||
|
let ethValueOfTradeForBestQuote = null
|
||||||
|
|
||||||
|
Object.values(quotes).forEach((quote) => {
|
||||||
|
const {
|
||||||
|
destinationAmount = 0,
|
||||||
|
destinationToken,
|
||||||
|
destinationTokenInfo,
|
||||||
|
trade,
|
||||||
|
approvalNeeded,
|
||||||
|
averageGas,
|
||||||
|
gasEstimate,
|
||||||
|
aggregator,
|
||||||
|
} = quote
|
||||||
|
const tradeGasLimitForCalculation = gasEstimate
|
||||||
|
? new BigNumber(gasEstimate, 16)
|
||||||
|
: new BigNumber(averageGas || MAX_GAS_LIMIT, 10)
|
||||||
|
const totalGasLimitForCalculation = tradeGasLimitForCalculation
|
||||||
|
.plus(approvalNeeded?.gas || '0x0', 16)
|
||||||
|
.toString(16)
|
||||||
|
const gasTotalInWeiHex = calcGasTotal(
|
||||||
|
totalGasLimitForCalculation,
|
||||||
|
usedGasPrice,
|
||||||
|
)
|
||||||
|
const totalEthCost = new BigNumber(gasTotalInWeiHex, 16).plus(
|
||||||
|
trade.value,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
const ethFee = conversionUtil(totalEthCost, {
|
||||||
|
fromCurrency: 'ETH',
|
||||||
|
fromDenomination: 'WEI',
|
||||||
|
toDenomination: 'ETH',
|
||||||
|
fromNumericBase: 'BN',
|
||||||
|
numberOfDecimals: 6,
|
||||||
|
})
|
||||||
|
|
||||||
|
const tokenConversionRate = tokenConversionRates[destinationToken]
|
||||||
|
const ethValueOfTrade =
|
||||||
|
destinationTokenInfo.symbol === 'ETH'
|
||||||
|
? calcTokenAmount(destinationAmount, 18).minus(ethFee, 10)
|
||||||
|
: new BigNumber(tokenConversionRate || 1, 10)
|
||||||
|
.times(
|
||||||
|
calcTokenAmount(
|
||||||
|
destinationAmount,
|
||||||
|
destinationTokenInfo.decimals,
|
||||||
|
),
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.minus(tokenConversionRate ? ethFee.toString(10) : 0, 10)
|
||||||
|
|
||||||
|
if (
|
||||||
|
ethValueOfTradeForBestQuote === null ||
|
||||||
|
ethValueOfTrade.gt(ethValueOfTradeForBestQuote)
|
||||||
|
) {
|
||||||
|
topAggId = aggregator
|
||||||
|
ethValueOfTradeForBestQuote = ethValueOfTrade
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isBest =
|
||||||
|
quotes[topAggId]?.destinationTokenInfo?.symbol === 'ETH' ||
|
||||||
|
Boolean(tokenConversionRates[quotes[topAggId]?.destinationToken])
|
||||||
|
|
||||||
|
return { topAggId, isBest }
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getERC20Allowance (contractAddress, walletAddress) {
|
||||||
|
const contract = new ethers.Contract(
|
||||||
|
contractAddress, abi, this.ethersProvider,
|
||||||
|
)
|
||||||
|
return await contract.allowance(walletAddress, METASWAP_ADDRESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the fetching of the swaps feature liveness flag from our API.
|
||||||
|
* Performs an initial fetch when called, then fetches on a 10-minute
|
||||||
|
* interval.
|
||||||
|
*
|
||||||
|
* If the browser goes offline, the interval is cleared and swaps are disabled
|
||||||
|
* until the value can be fetched again.
|
||||||
|
*/
|
||||||
|
_setupSwapsLivenessFetching () {
|
||||||
|
const TEN_MINUTES_MS = 10 * 60 * 1000
|
||||||
|
let intervalId = null
|
||||||
|
|
||||||
|
const fetchAndSetupInterval = () => {
|
||||||
|
if (window.navigator.onLine && intervalId === null) {
|
||||||
|
// Set the interval first to prevent race condition between listener and
|
||||||
|
// initial call to this function.
|
||||||
|
intervalId = setInterval(this._fetchAndSetSwapsLiveness, TEN_MINUTES_MS)
|
||||||
|
this._fetchAndSetSwapsLiveness()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('online', fetchAndSetupInterval)
|
||||||
|
window.addEventListener('offline', () => {
|
||||||
|
if (intervalId !== null) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
intervalId = null
|
||||||
|
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
if (swapsState.swapsFeatureIsLive) {
|
||||||
|
this.setSwapsLiveness(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fetchAndSetupInterval()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function should only be called via _setupSwapsLivenessFetching.
|
||||||
|
*
|
||||||
|
* Attempts to fetch the swaps feature liveness flag from our API. Tries
|
||||||
|
* to fetch three times at 5-second intervals before giving up, in which
|
||||||
|
* case the value defaults to 'false'.
|
||||||
|
*
|
||||||
|
* Only updates state if the fetched/computed flag value differs from current
|
||||||
|
* state.
|
||||||
|
*/
|
||||||
|
async _fetchAndSetSwapsLiveness () {
|
||||||
|
const { swapsState } = this.store.getState()
|
||||||
|
const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState
|
||||||
|
let swapsFeatureIsLive = false
|
||||||
|
let successfullyFetched = false
|
||||||
|
let numAttempts = 0
|
||||||
|
|
||||||
|
const fetchAndIncrementNumAttempts = async () => {
|
||||||
|
try {
|
||||||
|
swapsFeatureIsLive = Boolean(await this._fetchSwapsFeatureLiveness())
|
||||||
|
successfullyFetched = true
|
||||||
|
} catch (err) {
|
||||||
|
log.error(err)
|
||||||
|
numAttempts += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchAndIncrementNumAttempts()
|
||||||
|
|
||||||
|
// The loop conditions are modified by fetchAndIncrementNumAttempts.
|
||||||
|
// eslint-disable-next-line no-unmodified-loop-condition
|
||||||
|
while (!successfullyFetched && numAttempts < 3) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 5000) // 5 seconds
|
||||||
|
})
|
||||||
|
await fetchAndIncrementNumAttempts()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!successfullyFetched) {
|
||||||
|
log.error('Failed to fetch swaps feature flag 3 times. Setting to false and trying again next interval.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swapsFeatureIsLive !== oldSwapsFeatureIsLive) {
|
||||||
|
this.setSwapsLiveness(swapsFeatureIsLive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _modifyValuesForMaxEthMode (newQuotes, accountBalance) {
|
||||||
|
const {
|
||||||
|
swapsState: { customGasPrice },
|
||||||
|
} = this.store.getState()
|
||||||
|
|
||||||
|
const usedGasPrice = customGasPrice || await this._getEthersGasPrice()
|
||||||
|
|
||||||
|
const mappedNewQuotes = mapValues(newQuotes, (quote) => {
|
||||||
|
const oldSourceAmount = quote.sourceAmount
|
||||||
|
|
||||||
|
const gasTotalInWeiHex = calcGasTotal((new BigNumber(quote.maxGas, 10)).toString(16), usedGasPrice)
|
||||||
|
const newSourceAmount = (new BigNumber(accountBalance, 16)).minus(gasTotalInWeiHex, 16).toString(10)
|
||||||
|
|
||||||
|
const newOldRatio = (new BigNumber(newSourceAmount, 10)).div(oldSourceAmount, 10)
|
||||||
|
const oldNewDifference = (new BigNumber(oldSourceAmount, 10)).minus(newSourceAmount, 10)
|
||||||
|
|
||||||
|
const oldDestinationAmount = quote.destinationAmount
|
||||||
|
const newDestinationAmount = (new BigNumber(oldDestinationAmount, 10)).times(newOldRatio)
|
||||||
|
|
||||||
|
const oldValue = quote.trade.value
|
||||||
|
const newValue = (new BigNumber(oldValue, 16)).minus(oldNewDifference, 10)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...quote,
|
||||||
|
trade: {
|
||||||
|
...quote.trade,
|
||||||
|
value: newValue,
|
||||||
|
},
|
||||||
|
destinationAmount: newDestinationAmount,
|
||||||
|
sourceAmount: newSourceAmount,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return mappedNewQuotes
|
||||||
|
}
|
||||||
|
}
|
@ -15,10 +15,13 @@ import {
|
|||||||
SEND_ETHER_ACTION_KEY,
|
SEND_ETHER_ACTION_KEY,
|
||||||
DEPLOY_CONTRACT_ACTION_KEY,
|
DEPLOY_CONTRACT_ACTION_KEY,
|
||||||
CONTRACT_INTERACTION_KEY,
|
CONTRACT_INTERACTION_KEY,
|
||||||
|
SWAP,
|
||||||
} from '../../../../ui/app/helpers/constants/transactions'
|
} from '../../../../ui/app/helpers/constants/transactions'
|
||||||
import cleanErrorStack from '../../lib/cleanErrorStack'
|
import cleanErrorStack from '../../lib/cleanErrorStack'
|
||||||
import { hexToBn, bnToHex, BnMultiplyByFraction } from '../../lib/util'
|
import { hexToBn, bnToHex, BnMultiplyByFraction } from '../../lib/util'
|
||||||
import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys'
|
import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys'
|
||||||
|
import { getSwapsTokensReceivedFromTxMeta } from '../../../../ui/app/pages/swaps/swaps.util'
|
||||||
|
import { segment, METAMETRICS_ANONYMOUS_ID } from '../../lib/segment'
|
||||||
import TransactionStateManager from './tx-state-manager'
|
import TransactionStateManager from './tx-state-manager'
|
||||||
import TxGasUtil from './tx-gas-utils'
|
import TxGasUtil from './tx-gas-utils'
|
||||||
import PendingTransactionTracker from './pending-tx-tracker'
|
import PendingTransactionTracker from './pending-tx-tracker'
|
||||||
@ -72,11 +75,12 @@ export default class TransactionController extends EventEmitter {
|
|||||||
this.blockTracker = opts.blockTracker
|
this.blockTracker = opts.blockTracker
|
||||||
this.signEthTx = opts.signTransaction
|
this.signEthTx = opts.signTransaction
|
||||||
this.inProcessOfSigning = new Set()
|
this.inProcessOfSigning = new Set()
|
||||||
|
this.version = opts.version
|
||||||
|
|
||||||
this.memStore = new ObservableStore({})
|
this.memStore = new ObservableStore({})
|
||||||
this.query = new EthQuery(this.provider)
|
this.query = new EthQuery(this.provider)
|
||||||
this.txGasUtil = new TxGasUtil(this.provider)
|
|
||||||
|
|
||||||
|
this.txGasUtil = new TxGasUtil(this.provider)
|
||||||
this._mapMethods()
|
this._mapMethods()
|
||||||
this.txStateManager = new TransactionStateManager({
|
this.txStateManager = new TransactionStateManager({
|
||||||
initState: opts.initState,
|
initState: opts.initState,
|
||||||
@ -359,6 +363,11 @@ export default class TransactionController extends EventEmitter {
|
|||||||
type: TRANSACTION_TYPE_CANCEL,
|
type: TRANSACTION_TYPE_CANCEL,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (originalTxMeta.transactionCategory === SWAP) {
|
||||||
|
newTxMeta.sourceTokenSymbol = originalTxMeta.sourceTokenSymbol
|
||||||
|
newTxMeta.destinationTokenSymbol = originalTxMeta.destinationTokenSymbol
|
||||||
|
}
|
||||||
|
|
||||||
this.addTx(newTxMeta)
|
this.addTx(newTxMeta)
|
||||||
await this.approveTransaction(newTxMeta.id)
|
await this.approveTransaction(newTxMeta.id)
|
||||||
return newTxMeta
|
return newTxMeta
|
||||||
@ -521,6 +530,10 @@ export default class TransactionController extends EventEmitter {
|
|||||||
async publishTransaction (txId, rawTx) {
|
async publishTransaction (txId, rawTx) {
|
||||||
const txMeta = this.txStateManager.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
txMeta.rawTx = rawTx
|
txMeta.rawTx = rawTx
|
||||||
|
if (txMeta.transactionCategory === SWAP) {
|
||||||
|
const preTxBalance = await this.query.getBalance(txMeta.txParams.from)
|
||||||
|
txMeta.preTxBalance = preTxBalance.toString(16)
|
||||||
|
}
|
||||||
this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction')
|
this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction')
|
||||||
let txHash
|
let txHash
|
||||||
try {
|
try {
|
||||||
@ -566,6 +579,57 @@ export default class TransactionController extends EventEmitter {
|
|||||||
gasUsed,
|
gasUsed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (txMeta.transactionCategory === SWAP) {
|
||||||
|
const postTxBalance = await this.query.getBalance(txMeta.txParams.from)
|
||||||
|
txMeta.postTxBalance = postTxBalance.toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txMeta.swapMetaData) {
|
||||||
|
let { version } = this.platform
|
||||||
|
if (process.env.METAMASK_ENVIRONMENT !== 'production') {
|
||||||
|
version = `${version}-${process.env.METAMASK_ENVIRONMENT}`
|
||||||
|
}
|
||||||
|
const segmentContext = {
|
||||||
|
app: {
|
||||||
|
version,
|
||||||
|
name: 'MetaMask Extension',
|
||||||
|
},
|
||||||
|
locale: this.preferencesStore.getState().currentLocale.replace('_', '-'),
|
||||||
|
page: {
|
||||||
|
path: '/background-process',
|
||||||
|
title: 'Background Process',
|
||||||
|
url: '/background-process',
|
||||||
|
},
|
||||||
|
userAgent: window.navigator.userAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
const metametricsId = this.preferencesStore.getState().metaMetricsId
|
||||||
|
if (metametricsId && txMeta.swapMetaData && txReceipt.status !== '0x0') {
|
||||||
|
segment.track({ event: 'Swap Completed', userId: metametricsId, context: segmentContext, category: 'swaps' })
|
||||||
|
segment.track({
|
||||||
|
event: 'Swap Completed',
|
||||||
|
properties: {
|
||||||
|
...txMeta.swapMetaData,
|
||||||
|
token_to_amount_received: getSwapsTokensReceivedFromTxMeta(txMeta.destinationTokenSymbol, txMeta, txMeta.destinationTokenAddress, txMeta.txParams.from, txMeta.destinationTokenDecimals),
|
||||||
|
},
|
||||||
|
context: segmentContext,
|
||||||
|
anonymousId: METAMETRICS_ANONYMOUS_ID,
|
||||||
|
excludeMetaMetricsId: true,
|
||||||
|
category: 'swaps',
|
||||||
|
})
|
||||||
|
} else if (metametricsId && txMeta.swapMetaData) {
|
||||||
|
segment.track({ event: 'Swap Failed', userId: metametricsId, context: segmentContext, category: 'swaps' })
|
||||||
|
segment.track({
|
||||||
|
event: 'Swap Failed',
|
||||||
|
properties: { ...txMeta.swapMetaData },
|
||||||
|
anonymousId: METAMETRICS_ANONYMOUS_ID,
|
||||||
|
excludeMetaMetricsId: true,
|
||||||
|
context: segmentContext,
|
||||||
|
category: 'swaps',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.txStateManager.updateTx(txMeta, 'transactions#confirmTransaction - add txReceipt')
|
this.txStateManager.updateTx(txMeta, 'transactions#confirmTransaction - add txReceipt')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err)
|
log.error(err)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import EthQuery from 'ethjs-query'
|
import EthQuery from 'ethjs-query'
|
||||||
import log from 'loglevel'
|
import log from 'loglevel'
|
||||||
|
import ethUtil from 'ethereumjs-util'
|
||||||
import { hexToBn, BnMultiplyByFraction, bnToHex } from '../../lib/util'
|
import { hexToBn, BnMultiplyByFraction, bnToHex } from '../../lib/util'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,11 +70,11 @@ export default class TxGasUtil {
|
|||||||
@param {string} blockGasLimitHex - the block gas limit
|
@param {string} blockGasLimitHex - the block gas limit
|
||||||
@returns {string} - the buffered gas limit as a hex string
|
@returns {string} - the buffered gas limit as a hex string
|
||||||
*/
|
*/
|
||||||
addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
|
addGasBuffer (initialGasLimitHex, blockGasLimitHex, multiplier = 1.5) {
|
||||||
const initialGasLimitBn = hexToBn(initialGasLimitHex)
|
const initialGasLimitBn = hexToBn(initialGasLimitHex)
|
||||||
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
||||||
const upperGasLimitBn = blockGasLimitBn.muln(0.9)
|
const upperGasLimitBn = blockGasLimitBn.muln(0.9)
|
||||||
const bufferedGasLimitBn = initialGasLimitBn.muln(1.5)
|
const bufferedGasLimitBn = initialGasLimitBn.muln(multiplier)
|
||||||
|
|
||||||
// if initialGasLimit is above blockGasLimit, dont modify it
|
// if initialGasLimit is above blockGasLimit, dont modify it
|
||||||
if (initialGasLimitBn.gt(upperGasLimitBn)) {
|
if (initialGasLimitBn.gt(upperGasLimitBn)) {
|
||||||
@ -86,4 +87,12 @@ export default class TxGasUtil {
|
|||||||
// otherwise use blockGasLimit
|
// otherwise use blockGasLimit
|
||||||
return bnToHex(upperGasLimitBn)
|
return bnToHex(upperGasLimitBn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBufferedGasLimit (txMeta, multiplier) {
|
||||||
|
const { blockGasLimit, estimatedGasHex, simulationFails } = await this.analyzeGasUsage(txMeta)
|
||||||
|
|
||||||
|
// add additional gas buffer to our estimation for safety
|
||||||
|
const gasLimit = this.addGasBuffer(ethUtil.addHexPrefix(estimatedGasHex), blockGasLimit, multiplier)
|
||||||
|
return { gasLimit, simulationFails }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
27
app/scripts/lib/segment.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Analytics from 'analytics-node'
|
||||||
|
|
||||||
|
const inDevelopment = process.env.METAMASK_DEBUG || process.env.IN_TEST
|
||||||
|
|
||||||
|
const flushAt = inDevelopment ? 1 : undefined
|
||||||
|
|
||||||
|
const segmentNoop = {
|
||||||
|
track () {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
page () {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
identify () {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not want to track events on development builds unless specifically
|
||||||
|
// provided a SEGMENT_WRITE_KEY. This also holds true for test environments and
|
||||||
|
// E2E, which is handled in the build process by never providing the SEGMENT_WRITE_KEY
|
||||||
|
// which process.env.IN_TEST is true
|
||||||
|
export const segment = process.env.SEGMENT_WRITE_KEY
|
||||||
|
? new Analytics(process.env.SEGMENT_WRITE_KEY, { flushAt })
|
||||||
|
: segmentNoop
|
||||||
|
|
||||||
|
export const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'
|
@ -2,7 +2,6 @@ import EventEmitter from 'events'
|
|||||||
|
|
||||||
import pump from 'pump'
|
import pump from 'pump'
|
||||||
import Dnode from 'dnode'
|
import Dnode from 'dnode'
|
||||||
import extension from 'extensionizer'
|
|
||||||
import ObservableStore from 'obs-store'
|
import ObservableStore from 'obs-store'
|
||||||
import asStream from 'obs-store/lib/asStream'
|
import asStream from 'obs-store/lib/asStream'
|
||||||
import RpcEngine from 'json-rpc-engine'
|
import RpcEngine from 'json-rpc-engine'
|
||||||
@ -50,6 +49,7 @@ import TypedMessageManager from './lib/typed-message-manager'
|
|||||||
import TransactionController from './controllers/transactions'
|
import TransactionController from './controllers/transactions'
|
||||||
import TokenRatesController from './controllers/token-rates'
|
import TokenRatesController from './controllers/token-rates'
|
||||||
import DetectTokensController from './controllers/detect-tokens'
|
import DetectTokensController from './controllers/detect-tokens'
|
||||||
|
import SwapsController from './controllers/swaps'
|
||||||
import { PermissionsController } from './controllers/permissions'
|
import { PermissionsController } from './controllers/permissions'
|
||||||
import getRestrictedMethods from './controllers/permissions/restrictedMethods'
|
import getRestrictedMethods from './controllers/permissions/restrictedMethods'
|
||||||
import nodeify from './lib/nodeify'
|
import nodeify from './lib/nodeify'
|
||||||
@ -71,8 +71,10 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
|
|
||||||
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200)
|
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200)
|
||||||
this.opts = opts
|
this.opts = opts
|
||||||
|
this.extension = opts.extension
|
||||||
this.platform = opts.platform
|
this.platform = opts.platform
|
||||||
const initState = opts.initState || {}
|
const initState = opts.initState || {}
|
||||||
|
const version = this.platform.getVersion()
|
||||||
this.recordFirstTimeInfo(initState)
|
this.recordFirstTimeInfo(initState)
|
||||||
|
|
||||||
// this keeps track of how many "controllerStream" connections are open
|
// this keeps track of how many "controllerStream" connections are open
|
||||||
@ -92,6 +94,12 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
// lock to ensure only one vault created at once
|
// lock to ensure only one vault created at once
|
||||||
this.createVaultMutex = new Mutex()
|
this.createVaultMutex = new Mutex()
|
||||||
|
|
||||||
|
this.extension.runtime.onInstalled.addListener((details) => {
|
||||||
|
if (details.reason === 'update' && version === '8.1.0') {
|
||||||
|
this.platform.openExtensionInBrowser()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// next, we will initialize the controllers
|
// next, we will initialize the controllers
|
||||||
// controller initialization order matters
|
// controller initialization order matters
|
||||||
|
|
||||||
@ -210,7 +218,6 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
preferencesStore: this.preferencesController.store,
|
preferencesStore: this.preferencesController.store,
|
||||||
})
|
})
|
||||||
|
|
||||||
const version = this.platform.getVersion()
|
|
||||||
this.threeBoxController = new ThreeBoxController({
|
this.threeBoxController = new ThreeBoxController({
|
||||||
preferencesController: this.preferencesController,
|
preferencesController: this.preferencesController,
|
||||||
addressBookController: this.addressBookController,
|
addressBookController: this.addressBookController,
|
||||||
@ -230,6 +237,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
|
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
blockTracker: this.blockTracker,
|
blockTracker: this.blockTracker,
|
||||||
|
version: this.platform.getVersion(),
|
||||||
})
|
})
|
||||||
this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx())
|
this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx())
|
||||||
|
|
||||||
@ -267,6 +275,14 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager()
|
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager()
|
||||||
this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
|
this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
|
||||||
|
|
||||||
|
this.swapsController = new SwapsController({
|
||||||
|
getBufferedGasLimit: this.txController.txGasUtil.getBufferedGasLimit.bind(this.txController.txGasUtil),
|
||||||
|
provider: this.provider,
|
||||||
|
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
|
||||||
|
getProviderConfig: this.networkController.getProviderConfig.bind(this.networkController),
|
||||||
|
tokenRatesStore: this.tokenRatesController.store,
|
||||||
|
})
|
||||||
|
|
||||||
this.store.updateStructure({
|
this.store.updateStructure({
|
||||||
AppStateController: this.appStateController.store,
|
AppStateController: this.appStateController.store,
|
||||||
TransactionController: this.txController.store,
|
TransactionController: this.txController.store,
|
||||||
@ -306,6 +322,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
PermissionsController: this.permissionsController.permissions,
|
PermissionsController: this.permissionsController.permissions,
|
||||||
PermissionsMetadata: this.permissionsController.store,
|
PermissionsMetadata: this.permissionsController.store,
|
||||||
ThreeBoxController: this.threeBoxController.store,
|
ThreeBoxController: this.threeBoxController.store,
|
||||||
|
SwapsController: this.swapsController.store,
|
||||||
// ENS Controller
|
// ENS Controller
|
||||||
EnsController: this.ensController.store,
|
EnsController: this.ensController.store,
|
||||||
})
|
})
|
||||||
@ -426,6 +443,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
preferencesController,
|
preferencesController,
|
||||||
threeBoxController,
|
threeBoxController,
|
||||||
txController,
|
txController,
|
||||||
|
swapsController,
|
||||||
} = this
|
} = this
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -491,6 +509,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController),
|
setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController),
|
||||||
setDefaultHomeActiveTabName: nodeify(this.appStateController.setDefaultHomeActiveTabName, this.appStateController),
|
setDefaultHomeActiveTabName: nodeify(this.appStateController.setDefaultHomeActiveTabName, this.appStateController),
|
||||||
setConnectedStatusPopoverHasBeenShown: nodeify(this.appStateController.setConnectedStatusPopoverHasBeenShown, this.appStateController),
|
setConnectedStatusPopoverHasBeenShown: nodeify(this.appStateController.setConnectedStatusPopoverHasBeenShown, this.appStateController),
|
||||||
|
setSwapsWelcomeMessageHasBeenShown: nodeify(this.appStateController.setSwapsWelcomeMessageHasBeenShown, this.appStateController),
|
||||||
|
|
||||||
// EnsController
|
// EnsController
|
||||||
tryReverseResolveAddress: nodeify(this.ensController.reverseResolveAddress, this.ensController),
|
tryReverseResolveAddress: nodeify(this.ensController.reverseResolveAddress, this.ensController),
|
||||||
@ -512,6 +531,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
estimateGas: nodeify(this.estimateGas, this),
|
estimateGas: nodeify(this.estimateGas, this),
|
||||||
getPendingNonce: nodeify(this.getPendingNonce, this),
|
getPendingNonce: nodeify(this.getPendingNonce, this),
|
||||||
getNextNonce: nodeify(this.getNextNonce, this),
|
getNextNonce: nodeify(this.getNextNonce, this),
|
||||||
|
addUnapprovedTransaction: nodeify(txController.addUnapprovedTransaction, txController),
|
||||||
|
|
||||||
// messageManager
|
// messageManager
|
||||||
signMessage: nodeify(this.signMessage, this),
|
signMessage: nodeify(this.signMessage, this),
|
||||||
@ -558,6 +578,25 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
addPermittedAccount: nodeify(permissionsController.addPermittedAccount, permissionsController),
|
addPermittedAccount: nodeify(permissionsController.addPermittedAccount, permissionsController),
|
||||||
removePermittedAccount: nodeify(permissionsController.removePermittedAccount, permissionsController),
|
removePermittedAccount: nodeify(permissionsController.removePermittedAccount, permissionsController),
|
||||||
requestAccountsPermissionWithId: nodeify(permissionsController.requestAccountsPermissionWithId, permissionsController),
|
requestAccountsPermissionWithId: nodeify(permissionsController.requestAccountsPermissionWithId, permissionsController),
|
||||||
|
|
||||||
|
// swaps
|
||||||
|
fetchAndSetQuotes: nodeify(swapsController.fetchAndSetQuotes, swapsController),
|
||||||
|
setSelectedQuoteAggId: nodeify(swapsController.setSelectedQuoteAggId, swapsController),
|
||||||
|
resetSwapsState: nodeify(swapsController.resetSwapsState, swapsController),
|
||||||
|
setSwapsTokens: nodeify(swapsController.setSwapsTokens, swapsController),
|
||||||
|
setApproveTxId: nodeify(swapsController.setApproveTxId, swapsController),
|
||||||
|
setTradeTxId: nodeify(swapsController.setTradeTxId, swapsController),
|
||||||
|
setMaxMode: nodeify(swapsController.setMaxMode, swapsController),
|
||||||
|
setSwapsTxGasPrice: nodeify(swapsController.setSwapsTxGasPrice, swapsController),
|
||||||
|
setSwapsTxGasLimit: nodeify(swapsController.setSwapsTxGasLimit, swapsController),
|
||||||
|
safeRefetchQuotes: nodeify(swapsController.safeRefetchQuotes, swapsController),
|
||||||
|
stopPollingForQuotes: nodeify(swapsController.stopPollingForQuotes, swapsController),
|
||||||
|
setBackgroundSwapRouteState: nodeify(swapsController.setBackgroundSwapRouteState, swapsController),
|
||||||
|
resetPostFetchState: nodeify(swapsController.resetPostFetchState, swapsController),
|
||||||
|
setSwapsErrorKey: nodeify(swapsController.setSwapsErrorKey, swapsController),
|
||||||
|
setInitialGasEstimate: nodeify(swapsController.setInitialGasEstimate, swapsController),
|
||||||
|
setCustomApproveTxData: nodeify(swapsController.setCustomApproveTxData, swapsController),
|
||||||
|
setSwapsLiveness: nodeify(swapsController.setSwapsLiveness, swapsController),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1545,7 +1584,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
? 'metamask'
|
? 'metamask'
|
||||||
: (new URL(sender.url)).origin
|
: (new URL(sender.url)).origin
|
||||||
let extensionId
|
let extensionId
|
||||||
if (sender.id !== extension.runtime.id) {
|
if (sender.id !== this.extension.runtime.id) {
|
||||||
extensionId = sender.id
|
extensionId = sender.id
|
||||||
}
|
}
|
||||||
let tabId
|
let tabId
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"test:e2e:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-all.sh",
|
"test:e2e:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-all.sh",
|
||||||
"test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html",
|
"test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html",
|
||||||
"test:coverage:strict": "nyc --check-coverage yarn test:unit:strict",
|
"test:coverage:strict": "nyc --check-coverage yarn test:unit:strict",
|
||||||
|
"test:coverage:path": "nyc --check-coverage yarn test:unit:path",
|
||||||
"test:coveralls-upload": "if [ \"$COVERALLS_REPO_TOKEN\" ]; then nyc report --reporter=text-lcov | coveralls; fi",
|
"test:coveralls-upload": "if [ \"$COVERALLS_REPO_TOKEN\" ]; then nyc report --reporter=text-lcov | coveralls; fi",
|
||||||
"ganache:start": "./development/run-ganache",
|
"ganache:start": "./development/run-ganache",
|
||||||
"sentry:publish": "node ./development/sentry-publish.js",
|
"sentry:publish": "node ./development/sentry-publish.js",
|
||||||
@ -44,8 +45,8 @@
|
|||||||
"devtools:redux": "remotedev --hostname=localhost --port=8000",
|
"devtools:redux": "remotedev --hostname=localhost --port=8000",
|
||||||
"start:dev": "concurrently -k -n build,react,redux yarn:start yarn:devtools:react yarn:devtools:redux",
|
"start:dev": "concurrently -k -n build,react,redux yarn:start yarn:devtools:react yarn:devtools:redux",
|
||||||
"announce": "node development/announcer.js",
|
"announce": "node development/announcer.js",
|
||||||
"storybook": "start-storybook -p 6006 -c .storybook --static-dir ./app",
|
"storybook": "start-storybook -p 6006 -c .storybook --static-dir ./app, ./storybook/images",
|
||||||
"storybook:build": "build-storybook -c .storybook -o .out --static-dir ./app",
|
"storybook:build": "build-storybook -c .storybook -o .out --static-dir ./app, ./storybook/images",
|
||||||
"storybook:deploy": "storybook-to-ghpages --existing-output-dir .out --remote storybook --branch master",
|
"storybook:deploy": "storybook-to-ghpages --existing-output-dir .out --remote storybook --branch master",
|
||||||
"update-changelog": "./development/auto-changelog.sh",
|
"update-changelog": "./development/auto-changelog.sh",
|
||||||
"generate:migration": "./development/generate-migration.sh"
|
"generate:migration": "./development/generate-migration.sh"
|
||||||
|
@ -5918,5 +5918,12 @@
|
|||||||
],
|
],
|
||||||
"metametrics": {
|
"metametrics": {
|
||||||
"mockMetaMetricsResponse": true
|
"mockMetaMetricsResponse": true
|
||||||
|
},
|
||||||
|
"swaps": {
|
||||||
|
"featureFlag": {
|
||||||
|
"status": {
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -495,5 +495,46 @@
|
|||||||
},
|
},
|
||||||
"hasRetried": false,
|
"hasRetried": false,
|
||||||
"hasCancelled": false
|
"hasCancelled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"initialTransaction": {
|
||||||
|
"blockNumber": "6195527",
|
||||||
|
"id": 4243712234858467,
|
||||||
|
"metamaskNetworkId": "4",
|
||||||
|
"status": "confirmed",
|
||||||
|
"time": 1585088013000,
|
||||||
|
"txParams": {
|
||||||
|
"from": "0xee014609ef9e09776ac5fe00bdbfef57bcdefebb",
|
||||||
|
"gas": "0x5208",
|
||||||
|
"gasPrice": "0x77359400",
|
||||||
|
"nonce": "0x3",
|
||||||
|
"to": "0xabca64466f257793eaa52fcfff5066894b76a149",
|
||||||
|
"value": "0xde0b6b3a7640000"
|
||||||
|
},
|
||||||
|
"hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a",
|
||||||
|
"transactionCategory": "swap"
|
||||||
|
},
|
||||||
|
"primaryTransaction": {
|
||||||
|
"blockNumber": "6195527",
|
||||||
|
"id": 4243712234858467,
|
||||||
|
"metamaskNetworkId": "4",
|
||||||
|
"status": "confirmed",
|
||||||
|
"time": 1585088013000,
|
||||||
|
"txParams": {
|
||||||
|
"from": "0xee014609ef9e09776ac5fe00bdbfef57bcdefebb",
|
||||||
|
"gas": "0x5208",
|
||||||
|
"gasPrice": "0x77359400",
|
||||||
|
"nonce": "0x3",
|
||||||
|
"to": "0xabca64466f257793eaa52fcfff5066894b76a149",
|
||||||
|
"value": "0xde0b6b3a7640000"
|
||||||
|
},
|
||||||
|
"hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a",
|
||||||
|
"transactionCategory": "swap",
|
||||||
|
"destinationTokenSymbol": "ABC",
|
||||||
|
"destinationTokenAddress": "0xabca64466f257793eaa52fcfff5066894b76a149",
|
||||||
|
"sourceTokenSymbol": "ETH"
|
||||||
|
},
|
||||||
|
"hasRetried": false,
|
||||||
|
"hasCancelled": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -118,6 +118,12 @@ describe('MetaMask', function () {
|
|||||||
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('closes the swaps intro popup', async function () {
|
||||||
|
await driver.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver.delay(regularDelayMs)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Import seed phrase', function () {
|
describe('Import seed phrase', function () {
|
||||||
@ -161,7 +167,7 @@ describe('MetaMask', function () {
|
|||||||
|
|
||||||
describe('Adds an entry to the address book and sends eth to that address', function () {
|
describe('Adds an entry to the address book and sends eth to that address', function () {
|
||||||
it('starts a send transaction', async function () {
|
it('starts a send transaction', async function () {
|
||||||
await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`))
|
await driver.clickElement(By.css('[data-testid="eth-overview-send"]'))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
||||||
@ -210,7 +216,7 @@ describe('MetaMask', function () {
|
|||||||
|
|
||||||
describe('Sends to an address book entry', function () {
|
describe('Sends to an address book entry', function () {
|
||||||
it('starts a send transaction by clicking address book entry', async function () {
|
it('starts a send transaction by clicking address book entry', async function () {
|
||||||
await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`))
|
await driver.clickElement(By.css('[data-testid="eth-overview-send"]'))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
const recipientRowTitle = await driver.findElement(By.css('.send__select-recipient-wrapper__group-item__title'))
|
const recipientRowTitle = await driver.findElement(By.css('.send__select-recipient-wrapper__group-item__title'))
|
||||||
|
@ -84,6 +84,10 @@ describe('MetaMask', function () {
|
|||||||
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
|
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
|
await driver.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
await driver.clickElement(By.css('[data-testid="account-options-menu-button"]'))
|
await driver.clickElement(By.css('[data-testid="account-options-menu-button"]'))
|
||||||
await driver.clickElement(By.css('[data-testid="account-options-menu__account-details"]'))
|
await driver.clickElement(By.css('[data-testid="account-options-menu__account-details"]'))
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"AppStateController": {
|
"AppStateController": {
|
||||||
"mkrMigrationReminderTimestamp": null
|
"mkrMigrationReminderTimestamp": null,
|
||||||
|
"swapsWelcomeMessageHasBeenShown": true
|
||||||
},
|
},
|
||||||
"CachedBalancesController": {
|
"CachedBalancesController": {
|
||||||
"cachedBalances": {
|
"cachedBalances": {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"AppStateController": {
|
"AppStateController": {
|
||||||
"mkrMigrationReminderTimestamp": null
|
"mkrMigrationReminderTimestamp": null,
|
||||||
|
"swapsWelcomeMessageHasBeenShown": true
|
||||||
},
|
},
|
||||||
"CachedBalancesController": {
|
"CachedBalancesController": {
|
||||||
"cachedBalances": {
|
"cachedBalances": {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"AppStateController": {
|
"AppStateController": {
|
||||||
"connectedStatusPopoverHasBeenShown": false
|
"connectedStatusPopoverHasBeenShown": false,
|
||||||
|
"swapsWelcomeMessageHasBeenShown": true
|
||||||
},
|
},
|
||||||
"CachedBalancesController": {
|
"CachedBalancesController": {
|
||||||
"cachedBalances": {
|
"cachedBalances": {
|
||||||
|
@ -93,6 +93,12 @@ describe('Using MetaMask with an existing account', function () {
|
|||||||
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('closes the swaps intro popup', async function () {
|
||||||
|
await driver.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver.delay(regularDelayMs)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Show account information', function () {
|
describe('Show account information', function () {
|
||||||
@ -186,7 +192,7 @@ describe('Using MetaMask with an existing account', function () {
|
|||||||
|
|
||||||
describe('Send ETH from inside MetaMask', function () {
|
describe('Send ETH from inside MetaMask', function () {
|
||||||
it('starts a send transaction', async function () {
|
it('starts a send transaction', async function () {
|
||||||
await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`))
|
await driver.clickElement(By.css('[data-testid="eth-overview-send"]'))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
||||||
|
@ -90,6 +90,10 @@ describe('MetaMask', function () {
|
|||||||
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
|
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
|
await driver.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
await driver.clickElement(By.css('[data-testid="account-options-menu-button"]'))
|
await driver.clickElement(By.css('[data-testid="account-options-menu-button"]'))
|
||||||
await driver.clickElement(By.css('[data-testid="account-options-menu__account-details"]'))
|
await driver.clickElement(By.css('[data-testid="account-options-menu__account-details"]'))
|
||||||
})
|
})
|
||||||
|
@ -113,6 +113,12 @@ describe('MetaMask', function () {
|
|||||||
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('closes the swaps intro popup', async function () {
|
||||||
|
await driver.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver.delay(regularDelayMs)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Show account information', function () {
|
describe('Show account information', function () {
|
||||||
@ -176,7 +182,7 @@ describe('MetaMask', function () {
|
|||||||
|
|
||||||
describe('Send ETH from inside MetaMask', function () {
|
describe('Send ETH from inside MetaMask', function () {
|
||||||
it('starts to send a transaction', async function () {
|
it('starts to send a transaction', async function () {
|
||||||
await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`))
|
await driver.clickElement(By.css('[data-testid="eth-overview-send"]'))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
||||||
|
@ -115,6 +115,12 @@ describe('MetaMask', function () {
|
|||||||
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('closes the swaps intro popup', async function () {
|
||||||
|
await driver.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver.delay(regularDelayMs)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Show account information', function () {
|
describe('Show account information', function () {
|
||||||
@ -217,7 +223,7 @@ describe('MetaMask', function () {
|
|||||||
|
|
||||||
describe('Send ETH from inside MetaMask using default gas', function () {
|
describe('Send ETH from inside MetaMask using default gas', function () {
|
||||||
it('starts a send transaction', async function () {
|
it('starts a send transaction', async function () {
|
||||||
await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`))
|
await driver.clickElement(By.css('[data-testid="eth-overview-send"]'))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
||||||
@ -281,7 +287,7 @@ describe('MetaMask', function () {
|
|||||||
|
|
||||||
describe('Send ETH from inside MetaMask using fast gas option', function () {
|
describe('Send ETH from inside MetaMask using fast gas option', function () {
|
||||||
it('starts a send transaction', async function () {
|
it('starts a send transaction', async function () {
|
||||||
await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`))
|
await driver.clickElement(By.css('[data-testid="eth-overview-send"]'))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
||||||
@ -320,7 +326,7 @@ describe('MetaMask', function () {
|
|||||||
|
|
||||||
describe('Send ETH from inside MetaMask using advanced gas modal', function () {
|
describe('Send ETH from inside MetaMask using advanced gas modal', function () {
|
||||||
it('starts a send transaction', async function () {
|
it('starts a send transaction', async function () {
|
||||||
await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`))
|
await driver.clickElement(By.css('[data-testid="eth-overview-send"]'))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
||||||
@ -842,7 +848,7 @@ describe('MetaMask', function () {
|
|||||||
describe('Send token from inside MetaMask', function () {
|
describe('Send token from inside MetaMask', function () {
|
||||||
let gasModal
|
let gasModal
|
||||||
it('starts to send a transaction', async function () {
|
it('starts to send a transaction', async function () {
|
||||||
await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`))
|
await driver.clickElement(By.css('[data-testid="eth-overview-send"]'))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
||||||
|
@ -85,6 +85,10 @@ describe('MetaMask', function () {
|
|||||||
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
|
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
|
await driver.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
await driver.clickElement(By.css('[data-testid="account-options-menu-button"]'))
|
await driver.clickElement(By.css('[data-testid="account-options-menu-button"]'))
|
||||||
await driver.clickElement(By.css('[data-testid="account-options-menu__account-details"]'))
|
await driver.clickElement(By.css('[data-testid="account-options-menu__account-details"]'))
|
||||||
})
|
})
|
||||||
|
@ -91,11 +91,17 @@ describe('Using MetaMask with an existing account', function () {
|
|||||||
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
await driver.clickElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('closes the swaps intro popup', async function () {
|
||||||
|
await driver.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver.delay(regularDelayMs)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Send ETH from inside MetaMask', function () {
|
describe('Send ETH from inside MetaMask', function () {
|
||||||
it('starts a send transaction', async function () {
|
it('starts a send transaction', async function () {
|
||||||
await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`))
|
await driver.clickElement(By.css('[data-testid="eth-overview-send"]'))
|
||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
|
|
||||||
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
const inputAddress = await driver.findElement(By.css('input[placeholder="Search, public address (0x), or ENS"]'))
|
||||||
|
@ -95,6 +95,12 @@ describe('MetaMask', function () {
|
|||||||
await driver.delay(regularDelayMs)
|
await driver.delay(regularDelayMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('closes the swaps intro popup', async function () {
|
||||||
|
await driver.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver.delay(regularDelayMs)
|
||||||
|
})
|
||||||
|
|
||||||
it('balance renders', async function () {
|
it('balance renders', async function () {
|
||||||
const balance = await driver.findElement(By.css('[data-testid="wallet-balance"] .list-item__heading'))
|
const balance = await driver.findElement(By.css('[data-testid="wallet-balance"] .list-item__heading'))
|
||||||
await driver.wait(until.elementTextMatches(balance, /25\s*ETH/u))
|
await driver.wait(until.elementTextMatches(balance, /25\s*ETH/u))
|
||||||
@ -201,6 +207,12 @@ describe('MetaMask', function () {
|
|||||||
await driver2.delay(regularDelayMs)
|
await driver2.delay(regularDelayMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('closes the swaps intro popup', async function () {
|
||||||
|
await driver2.findElement(By.css(`.popover-header__subtitle`))
|
||||||
|
await driver2.clickElement(By.css('.popover-header__button'))
|
||||||
|
await driver2.delay(regularDelayMs)
|
||||||
|
})
|
||||||
|
|
||||||
it('balance renders', async function () {
|
it('balance renders', async function () {
|
||||||
const balance = await driver2.findElement(By.css('[data-testid="wallet-balance"] .list-item__heading'))
|
const balance = await driver2.findElement(By.css('[data-testid="wallet-balance"] .list-item__heading'))
|
||||||
await driver2.wait(until.elementTextMatches(balance, /25\s*ETH/u))
|
await driver2.wait(until.elementTextMatches(balance, /25\s*ETH/u))
|
||||||
|
@ -47,6 +47,10 @@ async function setupFetchMocking (driver) {
|
|||||||
return { json: async () => clone(mockResponses.ethGasPredictTable) }
|
return { json: async () => clone(mockResponses.ethGasPredictTable) }
|
||||||
} else if (url.match(/chromeextensionmm/u)) {
|
} else if (url.match(/chromeextensionmm/u)) {
|
||||||
return { json: async () => clone(mockResponses.metametrics) }
|
return { json: async () => clone(mockResponses.metametrics) }
|
||||||
|
} else if (url.match(/^https:\/\/(api\.metaswap|.*airswap-dev)/u)) {
|
||||||
|
if (url.match(/featureFlag$/u)) {
|
||||||
|
return { json: async () => clone(mockResponses.swaps.featureFlag) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return window.origFetch(...args)
|
return window.origFetch(...args)
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@ class ThreeBoxControllerMock {
|
|||||||
const ExtensionizerMock = {
|
const ExtensionizerMock = {
|
||||||
runtime: {
|
runtime: {
|
||||||
id: 'fake-extension-id',
|
id: 'fake-extension-id',
|
||||||
|
onInstalled: {
|
||||||
|
addListener: () => undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +63,6 @@ const createLoggerMiddlewareMock = () => (req, res, next) => {
|
|||||||
|
|
||||||
const MetaMaskController = proxyquire('../../../../app/scripts/metamask-controller', {
|
const MetaMaskController = proxyquire('../../../../app/scripts/metamask-controller', {
|
||||||
'./controllers/threebox': { default: ThreeBoxControllerMock },
|
'./controllers/threebox': { default: ThreeBoxControllerMock },
|
||||||
'extensionizer': ExtensionizerMock,
|
|
||||||
'./lib/createLoggerMiddleware': { default: createLoggerMiddlewareMock },
|
'./lib/createLoggerMiddleware': { default: createLoggerMiddlewareMock },
|
||||||
}).default
|
}).default
|
||||||
|
|
||||||
@ -101,6 +103,7 @@ describe('MetaMaskController', function () {
|
|||||||
},
|
},
|
||||||
initState: cloneDeep(firstTimeState),
|
initState: cloneDeep(firstTimeState),
|
||||||
platform: { showTransactionNotification: () => undefined, getVersion: () => 'foo' },
|
platform: { showTransactionNotification: () => undefined, getVersion: () => 'foo' },
|
||||||
|
extension: ExtensionizerMock,
|
||||||
infuraProjectId: 'foo',
|
infuraProjectId: 'foo',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
745
test/unit/app/controllers/swaps-test.js
Normal file
@ -0,0 +1,745 @@
|
|||||||
|
import assert from 'assert'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
|
||||||
|
import { ethers } from 'ethers'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import ObservableStore from 'obs-store'
|
||||||
|
import { createTestProviderTools } from '../../../stub/provider'
|
||||||
|
import { DEFAULT_ERC20_APPROVE_GAS } from '../../../../ui/app/helpers/constants/swaps'
|
||||||
|
import SwapsController from '../../../../app/scripts/controllers/swaps'
|
||||||
|
|
||||||
|
const MOCK_FETCH_PARAMS = {
|
||||||
|
slippage: 3,
|
||||||
|
sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||||
|
sourceDecimals: 18,
|
||||||
|
destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||||
|
value: '1000000000000000000',
|
||||||
|
fromAddress: '0x7F18BB4Dd92CF2404C54CBa1A9BE4A1153bdb078',
|
||||||
|
exchangeList: 'zeroExV1',
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEST_AGG_ID = 'zeroExV1'
|
||||||
|
const MOCK_QUOTES = {
|
||||||
|
[TEST_AGG_ID]: {
|
||||||
|
trade: {
|
||||||
|
data: '0x00',
|
||||||
|
from: '0x7F18BB4Dd92CF2404C54CBa1A9BE4A1153bdb078',
|
||||||
|
value: '0x17647444f166000',
|
||||||
|
gas: '0xe09c0',
|
||||||
|
gasPrice: undefined,
|
||||||
|
to: '0x016B4bf68d421147c06f1b8680602c5bf0Df91A8',
|
||||||
|
},
|
||||||
|
sourceAmount: '1000000000000000000000000000000000000',
|
||||||
|
destinationAmount: '396493201125465',
|
||||||
|
error: null,
|
||||||
|
sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||||
|
destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||||
|
approvalNeeded: null,
|
||||||
|
maxGas: 920000,
|
||||||
|
averageGas: 312510,
|
||||||
|
estimatedRefund: 343090,
|
||||||
|
fetchTime: 559,
|
||||||
|
aggregator: TEST_AGG_ID,
|
||||||
|
aggType: 'AGG',
|
||||||
|
slippage: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const MOCK_FETCH_METADATA = {
|
||||||
|
destinationTokenInfo: {
|
||||||
|
symbol: 'FOO',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const MOCK_TOKEN_RATES_STORE = new ObservableStore({
|
||||||
|
contractExchangeRates: { '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2 },
|
||||||
|
})
|
||||||
|
|
||||||
|
const MOCK_GET_PROVIDER_CONFIG = () => ({ type: 'FAKE_NETWORK' })
|
||||||
|
|
||||||
|
const MOCK_GET_BUFFERED_GAS_LIMIT = async () => ({
|
||||||
|
gasLimit: 2000000,
|
||||||
|
simulationFails: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const EMPTY_INIT_STATE = {
|
||||||
|
swapsState: {
|
||||||
|
quotes: {},
|
||||||
|
fetchParams: null,
|
||||||
|
tokens: null,
|
||||||
|
tradeTxId: null,
|
||||||
|
approveTxId: null,
|
||||||
|
maxMode: false,
|
||||||
|
quotesLastFetched: null,
|
||||||
|
customMaxGas: '',
|
||||||
|
customGasPrice: null,
|
||||||
|
selectedAggId: null,
|
||||||
|
customApproveTxData: '',
|
||||||
|
errorKey: '',
|
||||||
|
topAggId: null,
|
||||||
|
routeState: '',
|
||||||
|
swapsFeatureIsLive: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const sandbox = sinon.createSandbox()
|
||||||
|
const fetchTradesInfoStub = sandbox.stub()
|
||||||
|
const fetchSwapsFeatureLivenessStub = sandbox.stub()
|
||||||
|
|
||||||
|
describe('SwapsController', function () {
|
||||||
|
let provider
|
||||||
|
|
||||||
|
const getSwapsController = () => {
|
||||||
|
return new SwapsController({
|
||||||
|
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
|
||||||
|
provider,
|
||||||
|
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||||
|
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
||||||
|
fetchTradesInfo: fetchTradesInfoStub,
|
||||||
|
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
const providerResultStub = {
|
||||||
|
// 1 gwei
|
||||||
|
eth_gasPrice: '0x0de0b6b3a7640000',
|
||||||
|
// by default, all accounts are external accounts (not contracts)
|
||||||
|
eth_getCode: '0x',
|
||||||
|
}
|
||||||
|
provider = createTestProviderTools({ scaffold: providerResultStub })
|
||||||
|
.provider
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', function () {
|
||||||
|
it('should setup correctly', function () {
|
||||||
|
const swapsController = getSwapsController()
|
||||||
|
assert.deepStrictEqual(swapsController.store.getState(), EMPTY_INIT_STATE)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
swapsController.getBufferedGasLimit,
|
||||||
|
MOCK_GET_BUFFERED_GAS_LIMIT,
|
||||||
|
)
|
||||||
|
assert.strictEqual(swapsController.pollCount, 0)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
swapsController.getProviderConfig,
|
||||||
|
MOCK_GET_PROVIDER_CONFIG,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('API', function () {
|
||||||
|
let swapsController
|
||||||
|
beforeEach(function () {
|
||||||
|
swapsController = getSwapsController()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setters', function () {
|
||||||
|
it('should set selected quote agg id', function () {
|
||||||
|
const selectedAggId = 'test'
|
||||||
|
swapsController.setSelectedQuoteAggId(selectedAggId)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
swapsController.store.getState().swapsState.selectedAggId,
|
||||||
|
selectedAggId,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set swaps tokens', function () {
|
||||||
|
const tokens = []
|
||||||
|
swapsController.setSwapsTokens(tokens)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
swapsController.store.getState().swapsState.tokens,
|
||||||
|
tokens,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set trade tx id', function () {
|
||||||
|
const tradeTxId = 'test'
|
||||||
|
swapsController.setTradeTxId(tradeTxId)
|
||||||
|
assert.strictEqual(
|
||||||
|
swapsController.store.getState().swapsState.tradeTxId,
|
||||||
|
tradeTxId,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set max mode', function () {
|
||||||
|
const maxMode = true
|
||||||
|
swapsController.setMaxMode(maxMode)
|
||||||
|
assert.strictEqual(
|
||||||
|
swapsController.store.getState().swapsState.maxMode,
|
||||||
|
maxMode,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set swaps tx gas price', function () {
|
||||||
|
const gasPrice = 1
|
||||||
|
swapsController.setSwapsTxGasPrice(gasPrice)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
swapsController.store.getState().swapsState.customGasPrice,
|
||||||
|
gasPrice,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set swaps tx gas limit', function () {
|
||||||
|
const gasLimit = '1'
|
||||||
|
swapsController.setSwapsTxGasLimit(gasLimit)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
swapsController.store.getState().swapsState.customMaxGas,
|
||||||
|
gasLimit,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set background swap route state', function () {
|
||||||
|
const routeState = 'test'
|
||||||
|
swapsController.setBackgroundSwapRouteState(routeState)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
swapsController.store.getState().swapsState.routeState,
|
||||||
|
routeState,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set swaps error key', function () {
|
||||||
|
const errorKey = 'test'
|
||||||
|
swapsController.setSwapsErrorKey(errorKey)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
swapsController.store.getState().swapsState.errorKey,
|
||||||
|
errorKey,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set initial gas estimate', async function () {
|
||||||
|
const initialAggId = TEST_AGG_ID
|
||||||
|
const baseGasEstimate = 10
|
||||||
|
const { maxGas, estimatedRefund } = MOCK_QUOTES[TEST_AGG_ID]
|
||||||
|
|
||||||
|
const { swapsState } = swapsController.store.getState()
|
||||||
|
// Set mock quotes in order to have data for the test agg
|
||||||
|
swapsController.store.updateState({
|
||||||
|
swapsState: { ...swapsState, quotes: MOCK_QUOTES },
|
||||||
|
})
|
||||||
|
|
||||||
|
await swapsController.setInitialGasEstimate(
|
||||||
|
initialAggId,
|
||||||
|
baseGasEstimate,
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
gasLimit: bufferedGasLimit,
|
||||||
|
} = await swapsController.getBufferedGasLimit()
|
||||||
|
const {
|
||||||
|
gasEstimate,
|
||||||
|
gasEstimateWithRefund,
|
||||||
|
} = swapsController.store.getState().swapsState.quotes[initialAggId]
|
||||||
|
assert.strictEqual(gasEstimate, bufferedGasLimit)
|
||||||
|
assert.strictEqual(
|
||||||
|
gasEstimateWithRefund,
|
||||||
|
new BigNumber(maxGas, 10).minus(estimatedRefund, 10).toString(16),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set custom approve tx data', function () {
|
||||||
|
const data = 'test'
|
||||||
|
swapsController.setCustomApproveTxData(data)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
swapsController.store.getState().swapsState.customApproveTxData,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fetchAndSetQuotes', function () {
|
||||||
|
it('returns null if fetchParams is not provided', async function () {
|
||||||
|
const quotes = await swapsController.fetchAndSetQuotes(undefined)
|
||||||
|
assert.strictEqual(quotes, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () {
|
||||||
|
fetchTradesInfoStub.resolves(MOCK_QUOTES)
|
||||||
|
|
||||||
|
// Make it so approval is not required
|
||||||
|
sandbox
|
||||||
|
.stub(swapsController, '_getERC20Allowance')
|
||||||
|
.resolves(ethers.BigNumber.from(1))
|
||||||
|
|
||||||
|
const [newQuotes] = await swapsController.fetchAndSetQuotes(
|
||||||
|
MOCK_FETCH_PARAMS,
|
||||||
|
MOCK_FETCH_METADATA,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepStrictEqual(newQuotes[TEST_AGG_ID], {
|
||||||
|
...MOCK_QUOTES[TEST_AGG_ID],
|
||||||
|
sourceTokenInfo: undefined,
|
||||||
|
destinationTokenInfo: {
|
||||||
|
symbol: 'FOO',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
isBestQuote: true,
|
||||||
|
// TODO: find a way to calculate these values dynamically
|
||||||
|
gasEstimate: 2000000,
|
||||||
|
gasEstimateWithRefund: '8cd8e',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('performs the allowance check', async function () {
|
||||||
|
fetchTradesInfoStub.resolves(MOCK_QUOTES)
|
||||||
|
|
||||||
|
// Make it so approval is not required
|
||||||
|
const allowanceStub = sandbox
|
||||||
|
.stub(swapsController, '_getERC20Allowance')
|
||||||
|
.resolves(ethers.BigNumber.from(1))
|
||||||
|
|
||||||
|
await swapsController.fetchAndSetQuotes(
|
||||||
|
MOCK_FETCH_PARAMS,
|
||||||
|
MOCK_FETCH_METADATA,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
allowanceStub.calledOnceWithExactly(
|
||||||
|
MOCK_FETCH_PARAMS.sourceToken,
|
||||||
|
MOCK_FETCH_PARAMS.fromAddress,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('gets the gas limit if approval is required', async function () {
|
||||||
|
fetchTradesInfoStub.resolves(MOCK_QUOTES)
|
||||||
|
|
||||||
|
// Ensure approval is required
|
||||||
|
sandbox
|
||||||
|
.stub(swapsController, '_getERC20Allowance')
|
||||||
|
.resolves(ethers.BigNumber.from(0))
|
||||||
|
|
||||||
|
const timedoutGasReturnResult = { gasLimit: 1000000 }
|
||||||
|
const timedoutGasReturnStub = sandbox
|
||||||
|
.stub(swapsController, 'timedoutGasReturn')
|
||||||
|
.resolves(timedoutGasReturnResult)
|
||||||
|
|
||||||
|
await swapsController.fetchAndSetQuotes(
|
||||||
|
MOCK_FETCH_PARAMS,
|
||||||
|
MOCK_FETCH_METADATA,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mocked quotes approvalNeeded is null, so it will only be called with the gas
|
||||||
|
assert.strictEqual(
|
||||||
|
timedoutGasReturnStub.calledOnceWithExactly({
|
||||||
|
gas: DEFAULT_ERC20_APPROVE_GAS,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks the best quote', async function () {
|
||||||
|
fetchTradesInfoStub.resolves(MOCK_QUOTES)
|
||||||
|
|
||||||
|
// Make it so approval is not required
|
||||||
|
sandbox
|
||||||
|
.stub(swapsController, '_getERC20Allowance')
|
||||||
|
.resolves(ethers.BigNumber.from(1))
|
||||||
|
|
||||||
|
const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes(
|
||||||
|
MOCK_FETCH_PARAMS,
|
||||||
|
MOCK_FETCH_METADATA,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.strictEqual(topAggId, TEST_AGG_ID)
|
||||||
|
assert.strictEqual(newQuotes[topAggId].isBestQuote, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selects the best quote', async function () {
|
||||||
|
const bestAggId = 'bestAggId'
|
||||||
|
|
||||||
|
// Clone the existing mock quote and increase destination amount
|
||||||
|
const bestQuote = {
|
||||||
|
...MOCK_QUOTES[TEST_AGG_ID],
|
||||||
|
aggregator: bestAggId,
|
||||||
|
destinationAmount: ethers.BigNumber.from(
|
||||||
|
MOCK_QUOTES[TEST_AGG_ID].destinationAmount,
|
||||||
|
)
|
||||||
|
.add(1)
|
||||||
|
.toString(),
|
||||||
|
}
|
||||||
|
const quotes = { ...MOCK_QUOTES, [bestAggId]: bestQuote }
|
||||||
|
fetchTradesInfoStub.resolves(quotes)
|
||||||
|
|
||||||
|
// Make it so approval is not required
|
||||||
|
sandbox
|
||||||
|
.stub(swapsController, '_getERC20Allowance')
|
||||||
|
.resolves(ethers.BigNumber.from(1))
|
||||||
|
|
||||||
|
const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes(
|
||||||
|
MOCK_FETCH_PARAMS,
|
||||||
|
MOCK_FETCH_METADATA,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.strictEqual(topAggId, bestAggId)
|
||||||
|
assert.strictEqual(newQuotes[topAggId].isBestQuote, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not set isBestQuote if no conversion rate exists for destination token', async function () {
|
||||||
|
fetchTradesInfoStub.resolves(MOCK_QUOTES)
|
||||||
|
|
||||||
|
// Make it so approval is not required
|
||||||
|
sandbox
|
||||||
|
.stub(swapsController, '_getERC20Allowance')
|
||||||
|
.resolves(ethers.BigNumber.from(1))
|
||||||
|
|
||||||
|
swapsController.tokenRatesStore.updateState({
|
||||||
|
contractExchangeRates: {},
|
||||||
|
})
|
||||||
|
const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes(
|
||||||
|
MOCK_FETCH_PARAMS,
|
||||||
|
MOCK_FETCH_METADATA,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.strictEqual(newQuotes[topAggId].isBestQuote, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resetSwapsState', function () {
|
||||||
|
it('resets the swaps state correctly', function () {
|
||||||
|
const { swapsState: old } = swapsController.store.getState()
|
||||||
|
swapsController.resetSwapsState()
|
||||||
|
const { swapsState } = swapsController.store.getState()
|
||||||
|
assert.deepStrictEqual(swapsState, {
|
||||||
|
...EMPTY_INIT_STATE.swapsState,
|
||||||
|
tokens: old.tokens,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears polling timeout', function () {
|
||||||
|
swapsController.pollingTimeout = setTimeout(
|
||||||
|
() => assert.fail(),
|
||||||
|
1000000,
|
||||||
|
)
|
||||||
|
swapsController.resetSwapsState()
|
||||||
|
assert.strictEqual(swapsController.pollingTimeout._idleTimeout, -1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('stopPollingForQuotes', function () {
|
||||||
|
it('clears polling timeout', function () {
|
||||||
|
swapsController.pollingTimeout = setTimeout(
|
||||||
|
() => assert.fail(),
|
||||||
|
1000000,
|
||||||
|
)
|
||||||
|
swapsController.stopPollingForQuotes()
|
||||||
|
assert.strictEqual(swapsController.pollingTimeout._idleTimeout, -1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resets quotes state correctly', function () {
|
||||||
|
swapsController.stopPollingForQuotes()
|
||||||
|
const { swapsState } = swapsController.store.getState()
|
||||||
|
assert.deepStrictEqual(swapsState.quotes, {})
|
||||||
|
assert.strictEqual(swapsState.quotesLastFetched, null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resetPostFetchState', function () {
|
||||||
|
it('clears polling timeout', function () {
|
||||||
|
swapsController.pollingTimeout = setTimeout(
|
||||||
|
() => assert.fail(),
|
||||||
|
1000000,
|
||||||
|
)
|
||||||
|
swapsController.resetPostFetchState()
|
||||||
|
assert.strictEqual(swapsController.pollingTimeout._idleTimeout, -1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates state correctly', function () {
|
||||||
|
const tokens = 'test'
|
||||||
|
const fetchParams = 'test'
|
||||||
|
const swapsFeatureIsLive = false
|
||||||
|
swapsController.store.updateState({
|
||||||
|
swapsState: { tokens, fetchParams, swapsFeatureIsLive },
|
||||||
|
})
|
||||||
|
|
||||||
|
swapsController.resetPostFetchState()
|
||||||
|
|
||||||
|
const { swapsState } = swapsController.store.getState()
|
||||||
|
assert.deepStrictEqual(swapsState, {
|
||||||
|
...EMPTY_INIT_STATE.swapsState,
|
||||||
|
tokens,
|
||||||
|
fetchParams,
|
||||||
|
swapsFeatureIsLive,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_setupSwapsLivenessFetching ', function () {
|
||||||
|
|
||||||
|
let clock
|
||||||
|
const EXPECTED_TIME = 600000
|
||||||
|
|
||||||
|
const getLivenessState = () => {
|
||||||
|
return swapsController.store.getState().swapsState.swapsFeatureIsLive
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to do this to overwrite window.navigator.onLine
|
||||||
|
const stubWindow = () => {
|
||||||
|
sandbox.replace(global, 'window', {
|
||||||
|
addEventListener: window.addEventListener,
|
||||||
|
navigator: { onLine: true },
|
||||||
|
dispatchEvent: window.dispatchEvent,
|
||||||
|
Event: window.Event,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
stubWindow()
|
||||||
|
clock = sandbox.useFakeTimers()
|
||||||
|
sandbox.spy(clock, 'setInterval')
|
||||||
|
|
||||||
|
sandbox.stub(
|
||||||
|
SwapsController.prototype,
|
||||||
|
'_fetchAndSetSwapsLiveness',
|
||||||
|
).resolves(undefined)
|
||||||
|
|
||||||
|
sandbox.spy(
|
||||||
|
SwapsController.prototype,
|
||||||
|
'_setupSwapsLivenessFetching',
|
||||||
|
)
|
||||||
|
|
||||||
|
sandbox.spy(window, 'addEventListener')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls _setupSwapsLivenessFetching in constructor', function () {
|
||||||
|
swapsController = getSwapsController()
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
swapsController._setupSwapsLivenessFetching.calledOnce,
|
||||||
|
'should have called _setupSwapsLivenessFetching once',
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
window.addEventListener.calledWith('online'),
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
window.addEventListener.calledWith('offline'),
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
clock.setInterval.calledOnceWithExactly(
|
||||||
|
sinon.match.func,
|
||||||
|
EXPECTED_TIME,
|
||||||
|
),
|
||||||
|
'should have set an interval',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles browser being offline on boot, then coming online', async function () {
|
||||||
|
window.navigator.onLine = false
|
||||||
|
|
||||||
|
swapsController = getSwapsController()
|
||||||
|
assert.ok(
|
||||||
|
swapsController._setupSwapsLivenessFetching.calledOnce,
|
||||||
|
'should have called _setupSwapsLivenessFetching once',
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
swapsController._fetchAndSetSwapsLiveness.notCalled,
|
||||||
|
'should not have called _fetchAndSetSwapsLiveness',
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
clock.setInterval.notCalled,
|
||||||
|
'should not have set an interval',
|
||||||
|
)
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), false,
|
||||||
|
'swaps feature should be disabled',
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchPromise = new Promise((resolve) => {
|
||||||
|
const originalFunction = swapsController._fetchAndSetSwapsLiveness
|
||||||
|
swapsController._fetchAndSetSwapsLiveness = () => {
|
||||||
|
originalFunction()
|
||||||
|
resolve()
|
||||||
|
swapsController._fetchAndSetSwapsLiveness = originalFunction
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// browser comes online
|
||||||
|
window.navigator.onLine = true
|
||||||
|
window.dispatchEvent(new window.Event('online'))
|
||||||
|
await fetchPromise
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
swapsController._fetchAndSetSwapsLiveness.calledOnce,
|
||||||
|
'should have called _fetchAndSetSwapsLiveness once',
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
clock.setInterval.calledOnceWithExactly(
|
||||||
|
sinon.match.func,
|
||||||
|
EXPECTED_TIME,
|
||||||
|
),
|
||||||
|
'should have set an interval',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears interval if browser goes offline', async function () {
|
||||||
|
swapsController = getSwapsController()
|
||||||
|
|
||||||
|
// set feature to live
|
||||||
|
const { swapsState } = swapsController.store.getState()
|
||||||
|
swapsController.store.updateState({
|
||||||
|
swapsState: { ...swapsState, swapsFeatureIsLive: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
sandbox.spy(swapsController.store, 'updateState')
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
clock.setInterval.calledOnceWithExactly(
|
||||||
|
sinon.match.func,
|
||||||
|
EXPECTED_TIME,
|
||||||
|
),
|
||||||
|
'should have set an interval',
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearIntervalPromise = new Promise((resolve) => {
|
||||||
|
const originalFunction = clock.clearInterval
|
||||||
|
clock.clearInterval = (intervalId) => {
|
||||||
|
originalFunction(intervalId)
|
||||||
|
clock.clearInterval = originalFunction
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// browser goes offline
|
||||||
|
window.navigator.onLine = false
|
||||||
|
window.dispatchEvent(new window.Event('offline'))
|
||||||
|
|
||||||
|
// if this resolves, clearInterval was called
|
||||||
|
await clearIntervalPromise
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
swapsController._fetchAndSetSwapsLiveness.calledOnce,
|
||||||
|
'should have called _fetchAndSetSwapsLiveness once',
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
swapsController.store.updateState.calledOnce,
|
||||||
|
'should have called updateState once',
|
||||||
|
)
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), false,
|
||||||
|
'swaps feature should be disabled',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_fetchAndSetSwapsLiveness', function () {
|
||||||
|
|
||||||
|
const getLivenessState = () => {
|
||||||
|
return swapsController.store.getState().swapsState.swapsFeatureIsLive
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
fetchSwapsFeatureLivenessStub.reset()
|
||||||
|
sandbox.stub(
|
||||||
|
SwapsController.prototype,
|
||||||
|
'_setupSwapsLivenessFetching',
|
||||||
|
)
|
||||||
|
swapsController = getSwapsController()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches feature liveness as expected when API is live', async function () {
|
||||||
|
fetchSwapsFeatureLivenessStub.resolves(true)
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), false, 'liveness should be false on boot',
|
||||||
|
)
|
||||||
|
|
||||||
|
await swapsController._fetchAndSetSwapsLiveness()
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
fetchSwapsFeatureLivenessStub.calledOnce,
|
||||||
|
'should have called fetch function once',
|
||||||
|
)
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), true, 'liveness should be true after call',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not update state if fetched value is same as state value', async function () {
|
||||||
|
fetchSwapsFeatureLivenessStub.resolves(false)
|
||||||
|
sandbox.spy(swapsController.store, 'updateState')
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), false, 'liveness should be false on boot',
|
||||||
|
)
|
||||||
|
|
||||||
|
await swapsController._fetchAndSetSwapsLiveness()
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
fetchSwapsFeatureLivenessStub.calledOnce,
|
||||||
|
'should have called fetch function once',
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
swapsController.store.updateState.notCalled,
|
||||||
|
'should not have called store.updateState',
|
||||||
|
)
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), false, 'liveness should remain false after call',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tries three times before giving up if fetching fails', async function () {
|
||||||
|
const clock = sandbox.useFakeTimers()
|
||||||
|
fetchSwapsFeatureLivenessStub.rejects(new Error('foo'))
|
||||||
|
sandbox.spy(swapsController.store, 'updateState')
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), false, 'liveness should be false on boot',
|
||||||
|
)
|
||||||
|
|
||||||
|
swapsController._fetchAndSetSwapsLiveness()
|
||||||
|
await clock.runAllAsync()
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
fetchSwapsFeatureLivenessStub.calledThrice,
|
||||||
|
'should have called fetch function three times',
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
swapsController.store.updateState.notCalled,
|
||||||
|
'should not have called store.updateState',
|
||||||
|
)
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), false, 'liveness should remain false after call',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets state after fetching on successful retry', async function () {
|
||||||
|
const clock = sandbox.useFakeTimers()
|
||||||
|
fetchSwapsFeatureLivenessStub.onCall(0).rejects(new Error('foo'))
|
||||||
|
fetchSwapsFeatureLivenessStub.onCall(1).rejects(new Error('foo'))
|
||||||
|
fetchSwapsFeatureLivenessStub.onCall(2).resolves(true)
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), false, 'liveness should be false on boot',
|
||||||
|
)
|
||||||
|
|
||||||
|
swapsController._fetchAndSetSwapsLiveness()
|
||||||
|
await clock.runAllAsync()
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
fetchSwapsFeatureLivenessStub.callCount, 3,
|
||||||
|
'should have called fetch function three times',
|
||||||
|
)
|
||||||
|
assert.strictEqual(
|
||||||
|
getLivenessState(), true, 'liveness should be true after call',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -16,6 +16,13 @@ const { provider } = createTestProviderTools({ scaffold: {} })
|
|||||||
const middleware = [thunk]
|
const middleware = [thunk]
|
||||||
const defaultState = { metamask: {} }
|
const defaultState = { metamask: {} }
|
||||||
const mockStore = (state = defaultState) => configureStore(middleware)(state)
|
const mockStore = (state = defaultState) => configureStore(middleware)(state)
|
||||||
|
const extensionMock = {
|
||||||
|
runtime: {
|
||||||
|
onInstalled: {
|
||||||
|
addListener: () => undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
describe('Actions', function () {
|
describe('Actions', function () {
|
||||||
|
|
||||||
@ -32,6 +39,7 @@ describe('Actions', function () {
|
|||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
|
|
||||||
metamaskController = new MetaMaskController({
|
metamaskController = new MetaMaskController({
|
||||||
|
extension: extensionMock,
|
||||||
platform: { getVersion: () => 'foo' },
|
platform: { getVersion: () => 'foo' },
|
||||||
provider,
|
provider,
|
||||||
keyringController: new KeyringController({}),
|
keyringController: new KeyringController({}),
|
||||||
|
@ -19,7 +19,9 @@ export default class AppHeader extends PureComponent {
|
|||||||
isUnlocked: PropTypes.bool,
|
isUnlocked: PropTypes.bool,
|
||||||
hideNetworkIndicator: PropTypes.bool,
|
hideNetworkIndicator: PropTypes.bool,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
disableNetworkIndicator: PropTypes.bool,
|
||||||
isAccountMenuOpen: PropTypes.bool,
|
isAccountMenuOpen: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -84,7 +86,9 @@ export default class AppHeader extends PureComponent {
|
|||||||
provider,
|
provider,
|
||||||
isUnlocked,
|
isUnlocked,
|
||||||
hideNetworkIndicator,
|
hideNetworkIndicator,
|
||||||
|
disableNetworkIndicator,
|
||||||
disabled,
|
disabled,
|
||||||
|
onClick,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -94,7 +98,12 @@ export default class AppHeader extends PureComponent {
|
|||||||
<div className="app-header__contents">
|
<div className="app-header__contents">
|
||||||
<MetaFoxLogo
|
<MetaFoxLogo
|
||||||
unsetIconHeight
|
unsetIconHeight
|
||||||
onClick={() => history.push(DEFAULT_ROUTE)}
|
onClick={async () => {
|
||||||
|
if (onClick) {
|
||||||
|
await onClick()
|
||||||
|
}
|
||||||
|
history.push(DEFAULT_ROUTE)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="app-header__account-menu-container">
|
<div className="app-header__account-menu-container">
|
||||||
{
|
{
|
||||||
@ -104,7 +113,7 @@ export default class AppHeader extends PureComponent {
|
|||||||
network={network}
|
network={network}
|
||||||
provider={provider}
|
provider={provider}
|
||||||
onClick={(event) => this.handleNetworkIndicatorClick(event)}
|
onClick={(event) => this.handleNetworkIndicatorClick(event)}
|
||||||
disabled={disabled}
|
disabled={disabled || disableNetworkIndicator}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -17,6 +17,7 @@ export default class AdvancedGasInputs extends Component {
|
|||||||
insufficientBalance: PropTypes.bool,
|
insufficientBalance: PropTypes.bool,
|
||||||
customPriceIsSafe: PropTypes.bool,
|
customPriceIsSafe: PropTypes.bool,
|
||||||
isSpeedUp: PropTypes.bool,
|
isSpeedUp: PropTypes.bool,
|
||||||
|
customGasLimitMessage: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@ -101,7 +102,7 @@ export default class AdvancedGasInputs extends Component {
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGasInput ({ value, onChange, errorComponent, errorType, label, tooltipTitle }) {
|
renderGasInput ({ value, onChange, errorComponent, errorType, label, customMessageComponent, tooltipTitle }) {
|
||||||
return (
|
return (
|
||||||
<div className="advanced-gas-inputs__gas-edit-row">
|
<div className="advanced-gas-inputs__gas-edit-row">
|
||||||
<div className="advanced-gas-inputs__gas-edit-row__label">
|
<div className="advanced-gas-inputs__gas-edit-row__label">
|
||||||
@ -140,7 +141,7 @@ export default class AdvancedGasInputs extends Component {
|
|||||||
<i className="fa fa-sm fa-angle-down" />
|
<i className="fa fa-sm fa-angle-down" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ errorComponent }
|
{ errorComponent || customMessageComponent }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -151,6 +152,7 @@ export default class AdvancedGasInputs extends Component {
|
|||||||
insufficientBalance,
|
insufficientBalance,
|
||||||
customPriceIsSafe,
|
customPriceIsSafe,
|
||||||
isSpeedUp,
|
isSpeedUp,
|
||||||
|
customGasLimitMessage,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {
|
const {
|
||||||
gasPrice,
|
gasPrice,
|
||||||
@ -177,6 +179,14 @@ export default class AdvancedGasInputs extends Component {
|
|||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
|
|
||||||
|
const gasLimitCustomMessageComponent = customGasLimitMessage
|
||||||
|
? (
|
||||||
|
<div className="advanced-gas-inputs__gas-edit-row__custom-text">
|
||||||
|
{ customGasLimitMessage }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="advanced-gas-inputs__gas-edit-rows">
|
<div className="advanced-gas-inputs__gas-edit-rows">
|
||||||
{ this.renderGasInput({
|
{ this.renderGasInput({
|
||||||
@ -193,6 +203,7 @@ export default class AdvancedGasInputs extends Component {
|
|||||||
value: this.state.gasLimit,
|
value: this.state.gasLimit,
|
||||||
onChange: this.onChangeGasLimit,
|
onChange: this.onChangeGasLimit,
|
||||||
errorComponent: gasLimitErrorComponent,
|
errorComponent: gasLimitErrorComponent,
|
||||||
|
customMessageComponent: gasLimitCustomMessageComponent,
|
||||||
errorType: gasLimitErrorType,
|
errorType: gasLimitErrorType,
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
|
@ -132,5 +132,9 @@
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
color: $dusty-gray;
|
color: $dusty-gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__custom-text {
|
||||||
|
@include H7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ export default class AdvancedTabContent extends Component {
|
|||||||
customPriceIsSafe: PropTypes.bool,
|
customPriceIsSafe: PropTypes.bool,
|
||||||
isSpeedUp: PropTypes.bool,
|
isSpeedUp: PropTypes.bool,
|
||||||
isEthereumNetwork: PropTypes.bool,
|
isEthereumNetwork: PropTypes.bool,
|
||||||
|
customGasLimitMessage: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDataSummary (transactionFee, timeRemaining) {
|
renderDataSummary (transactionFee, timeRemaining) {
|
||||||
@ -65,6 +66,7 @@ export default class AdvancedTabContent extends Component {
|
|||||||
isSpeedUp,
|
isSpeedUp,
|
||||||
transactionFee,
|
transactionFee,
|
||||||
isEthereumNetwork,
|
isEthereumNetwork,
|
||||||
|
customGasLimitMessage,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -80,6 +82,7 @@ export default class AdvancedTabContent extends Component {
|
|||||||
insufficientBalance={insufficientBalance}
|
insufficientBalance={insufficientBalance}
|
||||||
customPriceIsSafe={customPriceIsSafe}
|
customPriceIsSafe={customPriceIsSafe}
|
||||||
isSpeedUp={isSpeedUp}
|
isSpeedUp={isSpeedUp}
|
||||||
|
customGasLimitMessage={customGasLimitMessage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{ isEthereumNetwork
|
{ isEthereumNetwork
|
||||||
|
@ -2,6 +2,10 @@ import React, { Component } from 'react'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import PageContainer from '../../../ui/page-container'
|
import PageContainer from '../../../ui/page-container'
|
||||||
import { Tabs, Tab } from '../../../ui/tabs'
|
import { Tabs, Tab } from '../../../ui/tabs'
|
||||||
|
import { calcGasTotal } from '../../../../pages/send/send.utils'
|
||||||
|
import {
|
||||||
|
sumHexWEIsToRenderableFiat,
|
||||||
|
} from '../../../../helpers/utils/conversions.util'
|
||||||
import AdvancedTabContent from './advanced-tab-content'
|
import AdvancedTabContent from './advanced-tab-content'
|
||||||
import BasicTabContent from './basic-tab-content'
|
import BasicTabContent from './basic-tab-content'
|
||||||
|
|
||||||
@ -9,6 +13,7 @@ export default class GasModalPageContainer extends Component {
|
|||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
metricsEvent: PropTypes.func,
|
metricsEvent: PropTypes.func,
|
||||||
|
trackEvent: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -29,6 +34,7 @@ export default class GasModalPageContainer extends Component {
|
|||||||
newTotalEth: PropTypes.string,
|
newTotalEth: PropTypes.string,
|
||||||
sendAmount: PropTypes.string,
|
sendAmount: PropTypes.string,
|
||||||
transactionFee: PropTypes.string,
|
transactionFee: PropTypes.string,
|
||||||
|
extraInfoRow: PropTypes.shape({ label: PropTypes.string, value: PropTypes.string }),
|
||||||
}),
|
}),
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
customModalGasPriceInHex: PropTypes.string,
|
customModalGasPriceInHex: PropTypes.string,
|
||||||
@ -43,9 +49,16 @@ export default class GasModalPageContainer extends Component {
|
|||||||
isRetry: PropTypes.bool,
|
isRetry: PropTypes.bool,
|
||||||
disableSave: PropTypes.bool,
|
disableSave: PropTypes.bool,
|
||||||
isEthereumNetwork: PropTypes.bool,
|
isEthereumNetwork: PropTypes.bool,
|
||||||
|
customGasLimitMessage: PropTypes.string,
|
||||||
|
customTotalSupplement: PropTypes.string,
|
||||||
|
isSwap: PropTypes.boolean,
|
||||||
|
value: PropTypes.string,
|
||||||
|
conversionRate: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {}
|
state = {
|
||||||
|
selectedTab: 'Basic',
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const promise = this.props.hideBasic
|
const promise = this.props.hideBasic
|
||||||
@ -84,6 +97,7 @@ export default class GasModalPageContainer extends Component {
|
|||||||
transactionFee,
|
transactionFee,
|
||||||
},
|
},
|
||||||
isEthereumNetwork,
|
isEthereumNetwork,
|
||||||
|
customGasLimitMessage,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -92,6 +106,7 @@ export default class GasModalPageContainer extends Component {
|
|||||||
updateCustomGasLimit={updateCustomGasLimit}
|
updateCustomGasLimit={updateCustomGasLimit}
|
||||||
customModalGasPriceInHex={customModalGasPriceInHex}
|
customModalGasPriceInHex={customModalGasPriceInHex}
|
||||||
customModalGasLimitInHex={customModalGasLimitInHex}
|
customModalGasLimitInHex={customModalGasLimitInHex}
|
||||||
|
customGasLimitMessage={customGasLimitMessage}
|
||||||
timeRemaining={currentTimeEstimate}
|
timeRemaining={currentTimeEstimate}
|
||||||
transactionFee={transactionFee}
|
transactionFee={transactionFee}
|
||||||
gasChartProps={gasChartProps}
|
gasChartProps={gasChartProps}
|
||||||
@ -105,7 +120,7 @@ export default class GasModalPageContainer extends Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee) {
|
renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee, extraInfoRow) {
|
||||||
return (
|
return (
|
||||||
<div className="gas-modal-content__info-row-wrapper">
|
<div className="gas-modal-content__info-row-wrapper">
|
||||||
<div className="gas-modal-content__info-row">
|
<div className="gas-modal-content__info-row">
|
||||||
@ -117,6 +132,12 @@ export default class GasModalPageContainer extends Component {
|
|||||||
<span className="gas-modal-content__info-row__transaction-info__label">{this.context.t('transactionFee')}</span>
|
<span className="gas-modal-content__info-row__transaction-info__label">{this.context.t('transactionFee')}</span>
|
||||||
<span className="gas-modal-content__info-row__transaction-info__value">{transactionFee}</span>
|
<span className="gas-modal-content__info-row__transaction-info__value">{transactionFee}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{extraInfoRow && (
|
||||||
|
<div className="gas-modal-content__info-row__transaction-info">
|
||||||
|
<span className="gas-modal-content__info-row__transaction-info__label">{extraInfoRow.label}</span>
|
||||||
|
<span className="gas-modal-content__info-row__transaction-info__value">{extraInfoRow.value}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="gas-modal-content__info-row__total-info">
|
<div className="gas-modal-content__info-row__total-info">
|
||||||
<span className="gas-modal-content__info-row__total-info__label">{this.context.t('newTotal')}</span>
|
<span className="gas-modal-content__info-row__total-info__label">{this.context.t('newTotal')}</span>
|
||||||
<span className="gas-modal-content__info-row__total-info__value">{newTotalEth}</span>
|
<span className="gas-modal-content__info-row__total-info__value">{newTotalEth}</span>
|
||||||
@ -138,6 +159,7 @@ export default class GasModalPageContainer extends Component {
|
|||||||
newTotalEth,
|
newTotalEth,
|
||||||
sendAmount,
|
sendAmount,
|
||||||
transactionFee,
|
transactionFee,
|
||||||
|
extraInfoRow,
|
||||||
},
|
},
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
@ -157,12 +179,12 @@ export default class GasModalPageContainer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs>
|
<Tabs onTabClick={(tabName) => this.setState({ selectedTab: tabName })}>
|
||||||
{tabsToRender.map(({ name, content }, i) => (
|
{tabsToRender.map(({ name, content }, i) => (
|
||||||
<Tab name={name} key={`gas-modal-tab-${i}`}>
|
<Tab name={name} key={`gas-modal-tab-${i}`}>
|
||||||
<div className="gas-modal-content">
|
<div className="gas-modal-content">
|
||||||
{ content }
|
{ content }
|
||||||
{ this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) }
|
{ this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee, extraInfoRow) }
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
@ -199,7 +221,26 @@ export default class GasModalPageContainer extends Component {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onSubmit(customModalGasLimitInHex, customModalGasPriceInHex)
|
if (this.props.isSwap) {
|
||||||
|
const newSwapGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
|
||||||
|
let speedSet = ''
|
||||||
|
if (this.state.selectedTab === 'Basic') {
|
||||||
|
const { gasButtonInfo } = this.props.gasPriceButtonGroupProps
|
||||||
|
const selectedGasButtonInfo = gasButtonInfo.find(({ priceInHexWei }) => priceInHexWei === customModalGasPriceInHex)
|
||||||
|
speedSet = selectedGasButtonInfo?.gasEstimateType || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.trackEvent({
|
||||||
|
event: 'Gas Fees Changed',
|
||||||
|
category: 'swaps',
|
||||||
|
properties: {
|
||||||
|
speed_set: speedSet,
|
||||||
|
gas_mode: this.state.selectedTab,
|
||||||
|
gas_fees: sumHexWEIsToRenderableFiat([this.props.value, newSwapGasTotal, this.props.customTotalSupplement], 'usd', this.props.conversionRate)?.slice(1),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onSubmit(customModalGasLimitInHex, customModalGasPriceInHex, this.state.selectedTab, this.context.mixPanelTrack)
|
||||||
}}
|
}}
|
||||||
submitText={this.context.t('save')}
|
submitText={this.context.t('save')}
|
||||||
headerCloseText={this.context.t('close')}
|
headerCloseText={this.context.t('close')}
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
updateSendAmount,
|
updateSendAmount,
|
||||||
setGasTotal,
|
setGasTotal,
|
||||||
updateTransaction,
|
updateTransaction,
|
||||||
|
setSwapsTxGasParams,
|
||||||
} from '../../../../store/actions'
|
} from '../../../../store/actions'
|
||||||
import {
|
import {
|
||||||
setCustomGasPrice,
|
setCustomGasPrice,
|
||||||
@ -47,13 +48,11 @@ import {
|
|||||||
} from '../../../../selectors'
|
} from '../../../../selectors'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
formatCurrency,
|
addHexes,
|
||||||
} from '../../../../helpers/utils/confirm-tx.util'
|
|
||||||
import {
|
|
||||||
addHexWEIsToDec,
|
|
||||||
subtractHexWEIsToDec,
|
subtractHexWEIsToDec,
|
||||||
decEthToConvertedCurrency as ethTotalToConvertedCurrency,
|
|
||||||
hexWEIToDecGWEI,
|
hexWEIToDecGWEI,
|
||||||
|
getValueFromWeiHex,
|
||||||
|
sumHexWEIsToRenderableFiat,
|
||||||
} from '../../../../helpers/utils/conversions.util'
|
} from '../../../../helpers/utils/conversions.util'
|
||||||
import { getRenderableTimeEstimate } from '../../../../helpers/utils/gas-time-estimates.util'
|
import { getRenderableTimeEstimate } from '../../../../helpers/utils/gas-time-estimates.util'
|
||||||
import {
|
import {
|
||||||
@ -69,10 +68,17 @@ import GasModalPageContainer from './gas-modal-page-container.component'
|
|||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
const { currentNetworkTxList, send } = state.metamask
|
const { currentNetworkTxList, send } = state.metamask
|
||||||
const { modalState: { props: modalProps } = {} } = state.appState.modal || {}
|
const { modalState: { props: modalProps } = {} } = state.appState.modal || {}
|
||||||
const { txData = {} } = modalProps || {}
|
const {
|
||||||
|
txData = {},
|
||||||
|
isSwap = false,
|
||||||
|
customGasLimitMessage = '',
|
||||||
|
customTotalSupplement = '',
|
||||||
|
extraInfoRow = null,
|
||||||
|
} = modalProps || {}
|
||||||
const { transaction = {} } = ownProps
|
const { transaction = {} } = ownProps
|
||||||
const selectedTransaction = currentNetworkTxList.find(({ id }) => id === (transaction.id || txData.id))
|
const selectedTransaction = isSwap
|
||||||
|
? txData
|
||||||
|
: currentNetworkTxList.find(({ id }) => id === (transaction.id || txData.id))
|
||||||
const buttonDataLoading = getBasicGasEstimateLoadingStatus(state)
|
const buttonDataLoading = getBasicGasEstimateLoadingStatus(state)
|
||||||
const gasEstimatesLoading = getGasEstimatesLoadingStatus(state)
|
const gasEstimatesLoading = getGasEstimatesLoadingStatus(state)
|
||||||
const sendToken = getSendToken(state)
|
const sendToken = getSendToken(state)
|
||||||
@ -95,8 +101,11 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
|
|
||||||
const currentCurrency = getCurrentCurrency(state)
|
const currentCurrency = getCurrentCurrency(state)
|
||||||
const conversionRate = getConversionRate(state)
|
const conversionRate = getConversionRate(state)
|
||||||
|
const newTotalFiat = sumHexWEIsToRenderableFiat(
|
||||||
const newTotalFiat = addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate)
|
[value, customGasTotal, customTotalSupplement],
|
||||||
|
currentCurrency,
|
||||||
|
conversionRate,
|
||||||
|
)
|
||||||
|
|
||||||
const { hideBasic } = state.appState.modal.modalState.props
|
const { hideBasic } = state.appState.modal.modalState.props
|
||||||
|
|
||||||
@ -114,9 +123,13 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
|
|
||||||
const isSendTokenSet = Boolean(sendToken)
|
const isSendTokenSet = Boolean(sendToken)
|
||||||
|
|
||||||
const newTotalEth = maxModeOn && !isSendTokenSet ? addHexWEIsToRenderableEth(balance, '0x0') : addHexWEIsToRenderableEth(value, customGasTotal)
|
const newTotalEth = maxModeOn && !isSendTokenSet
|
||||||
|
? sumHexWEIsToRenderableEth([balance, '0x0'])
|
||||||
|
: sumHexWEIsToRenderableEth([value, customGasTotal, customTotalSupplement])
|
||||||
|
|
||||||
const sendAmount = maxModeOn && !isSendTokenSet ? subtractHexWEIsFromRenderableEth(balance, customGasTotal) : addHexWEIsToRenderableEth(value, '0x0')
|
const sendAmount = maxModeOn && !isSendTokenSet
|
||||||
|
? subtractHexWEIsFromRenderableEth(balance, customGasTotal)
|
||||||
|
: sumHexWEIsToRenderableEth([value, '0x0'])
|
||||||
|
|
||||||
const insufficientBalance = maxModeOn ? false : !isBalanceSufficient({
|
const insufficientBalance = maxModeOn ? false : !isBalanceSufficient({
|
||||||
amount: value,
|
amount: value,
|
||||||
@ -135,6 +148,7 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
return {
|
return {
|
||||||
hideBasic,
|
hideBasic,
|
||||||
isConfirm: isConfirm(state),
|
isConfirm: isConfirm(state),
|
||||||
|
isSwap,
|
||||||
customModalGasPriceInHex,
|
customModalGasPriceInHex,
|
||||||
customModalGasLimitInHex,
|
customModalGasLimitInHex,
|
||||||
customGasPrice,
|
customGasPrice,
|
||||||
@ -158,12 +172,19 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
estimatedTimesMax: estimatedTimes[0],
|
estimatedTimesMax: estimatedTimes[0],
|
||||||
},
|
},
|
||||||
infoRowProps: {
|
infoRowProps: {
|
||||||
originalTotalFiat: addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate),
|
originalTotalFiat: sumHexWEIsToRenderableFiat(
|
||||||
originalTotalEth: addHexWEIsToRenderableEth(value, customGasTotal),
|
[value, customGasTotal, customTotalSupplement],
|
||||||
|
currentCurrency,
|
||||||
|
conversionRate,
|
||||||
|
),
|
||||||
|
originalTotalEth: sumHexWEIsToRenderableEth(
|
||||||
|
[value, customGasTotal, customTotalSupplement],
|
||||||
|
),
|
||||||
newTotalFiat: showFiat ? newTotalFiat : '',
|
newTotalFiat: showFiat ? newTotalFiat : '',
|
||||||
newTotalEth,
|
newTotalEth,
|
||||||
transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal),
|
transactionFee: sumHexWEIsToRenderableEth(['0x0', customGasTotal]),
|
||||||
sendAmount,
|
sendAmount,
|
||||||
|
extraInfoRow,
|
||||||
},
|
},
|
||||||
transaction: txData || transaction,
|
transaction: txData || transaction,
|
||||||
isSpeedUp: transaction.status === 'submitted',
|
isSpeedUp: transaction.status === 'submitted',
|
||||||
@ -176,6 +197,10 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
sendToken,
|
sendToken,
|
||||||
balance,
|
balance,
|
||||||
tokenBalance: getTokenBalance(state),
|
tokenBalance: getTokenBalance(state),
|
||||||
|
customGasLimitMessage,
|
||||||
|
conversionRate,
|
||||||
|
value,
|
||||||
|
customTotalSupplement,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,6 +239,9 @@ const mapDispatchToProps = (dispatch) => {
|
|||||||
dispatch(updateSendErrors({ amount: null }))
|
dispatch(updateSendErrors({ amount: null }))
|
||||||
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
|
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
|
||||||
},
|
},
|
||||||
|
updateSwapTxGas: (gasLimit, gasPrice) => {
|
||||||
|
dispatch(setSwapsTxGasParams(gasLimit, gasPrice))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +250,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
|||||||
gasPriceButtonGroupProps,
|
gasPriceButtonGroupProps,
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
isConfirm,
|
isConfirm,
|
||||||
|
isSwap,
|
||||||
txId,
|
txId,
|
||||||
isSpeedUp,
|
isSpeedUp,
|
||||||
isRetry,
|
isRetry,
|
||||||
@ -245,6 +274,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
|||||||
cancelAndClose: dispatchCancelAndClose,
|
cancelAndClose: dispatchCancelAndClose,
|
||||||
hideModal: dispatchHideModal,
|
hideModal: dispatchHideModal,
|
||||||
setAmountToMax: dispatchSetAmountToMax,
|
setAmountToMax: dispatchSetAmountToMax,
|
||||||
|
updateSwapTxGas: dispatchUpdateSwapTxGas,
|
||||||
...otherDispatchProps
|
...otherDispatchProps
|
||||||
} = dispatchProps
|
} = dispatchProps
|
||||||
|
|
||||||
@ -253,7 +283,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
|||||||
...otherDispatchProps,
|
...otherDispatchProps,
|
||||||
...ownProps,
|
...ownProps,
|
||||||
onSubmit: (gasLimit, gasPrice) => {
|
onSubmit: (gasLimit, gasPrice) => {
|
||||||
if (isConfirm) {
|
if (isSwap) {
|
||||||
|
dispatchUpdateSwapTxGas(gasLimit, gasPrice)
|
||||||
|
dispatchHideModal()
|
||||||
|
} else if (isConfirm) {
|
||||||
const updatedTx = {
|
const updatedTx = {
|
||||||
...transaction,
|
...transaction,
|
||||||
txParams: {
|
txParams: {
|
||||||
@ -296,7 +329,11 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
|||||||
dispatchHideSidebar()
|
dispatchHideSidebar()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0) || customGasLimit < 21000,
|
disableSave: (
|
||||||
|
insufficientBalance ||
|
||||||
|
(isSpeedUp && customGasPrice === 0) ||
|
||||||
|
customGasLimit < 21000
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,19 +351,15 @@ function calcCustomGasLimit (customGasLimitInHex) {
|
|||||||
return parseInt(customGasLimitInHex, 16)
|
return parseInt(customGasLimitInHex, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) {
|
function sumHexWEIsToRenderableEth (hexWEIs) {
|
||||||
return formatETHFee(addHexWEIsToDec(aHexWEI, bHexWEI))
|
const hexWEIsSum = hexWEIs.filter((n) => n).reduce(addHexes)
|
||||||
|
return formatETHFee(getValueFromWeiHex({
|
||||||
|
value: hexWEIsSum,
|
||||||
|
toCurrency: 'ETH',
|
||||||
|
numberOfDecimals: 6,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function subtractHexWEIsFromRenderableEth (aHexWEI, bHexWEI) {
|
function subtractHexWEIsFromRenderableEth (aHexWEI, bHexWEI) {
|
||||||
return formatETHFee(subtractHexWEIsToDec(aHexWEI, bHexWEI))
|
return formatETHFee(subtractHexWEIsToDec(aHexWEI, bHexWEI))
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conversionRate) {
|
|
||||||
const ethTotal = ethTotalToConvertedCurrency(
|
|
||||||
addHexWEIsToDec(aHexWEI, bHexWEI),
|
|
||||||
convertedCurrency,
|
|
||||||
conversionRate,
|
|
||||||
)
|
|
||||||
return formatCurrency(ethTotal, convertedCurrency)
|
|
||||||
}
|
|
||||||
|
@ -56,6 +56,7 @@ const mockInfoRowProps = {
|
|||||||
newTotalEth: 'mockNewTotalEth',
|
newTotalEth: 'mockNewTotalEth',
|
||||||
sendAmount: 'mockSendAmount',
|
sendAmount: 'mockSendAmount',
|
||||||
transactionFee: 'mockTransactionFee',
|
transactionFee: 'mockTransactionFee',
|
||||||
|
extraInfoRow: { label: 'mockLabel', value: 'mockValue' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const GP = GasModalPageContainer.prototype
|
const GP = GasModalPageContainer.prototype
|
||||||
@ -183,8 +184,8 @@ describe('GasModalPageContainer Component', function () {
|
|||||||
|
|
||||||
assert.equal(GP.renderInfoRows.callCount, 2)
|
assert.equal(GP.renderInfoRows.callCount, 2)
|
||||||
|
|
||||||
assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee'])
|
assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee', { label: 'mockLabel', value: 'mockValue' }])
|
||||||
assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee'])
|
assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee', { label: 'mockLabel', value: 'mockValue' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not render the basic tab if hideBasic is true', function () {
|
it('should not render the basic tab if hideBasic is true', function () {
|
||||||
|
@ -62,6 +62,7 @@ describe('gas-modal-page-container container', function () {
|
|||||||
txData: {
|
txData: {
|
||||||
id: 34,
|
id: 34,
|
||||||
},
|
},
|
||||||
|
extraInfoRow: { label: 'mockLabel', value: 'mockValue' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -125,6 +126,9 @@ describe('gas-modal-page-container container', function () {
|
|||||||
currentTimeEstimate: '~1 min 11 sec',
|
currentTimeEstimate: '~1 min 11 sec',
|
||||||
newTotalFiat: '637.41',
|
newTotalFiat: '637.41',
|
||||||
blockTime: 12,
|
blockTime: 12,
|
||||||
|
conversionRate: 50,
|
||||||
|
customGasLimitMessage: '',
|
||||||
|
customTotalSupplement: '',
|
||||||
customModalGasLimitInHex: 'aaaaaaaa',
|
customModalGasLimitInHex: 'aaaaaaaa',
|
||||||
customModalGasPriceInHex: 'ffffffff',
|
customModalGasPriceInHex: 'ffffffff',
|
||||||
customGasTotal: 'aaaaaaa955555556',
|
customGasTotal: 'aaaaaaa955555556',
|
||||||
@ -144,6 +148,7 @@ describe('gas-modal-page-container container', function () {
|
|||||||
gasEstimatesLoading: false,
|
gasEstimatesLoading: false,
|
||||||
hideBasic: true,
|
hideBasic: true,
|
||||||
infoRowProps: {
|
infoRowProps: {
|
||||||
|
extraInfoRow: { label: 'mockLabel', value: 'mockValue' },
|
||||||
originalTotalFiat: '637.41',
|
originalTotalFiat: '637.41',
|
||||||
originalTotalEth: '12.748189 ETH',
|
originalTotalEth: '12.748189 ETH',
|
||||||
newTotalFiat: '637.41',
|
newTotalFiat: '637.41',
|
||||||
@ -154,6 +159,7 @@ describe('gas-modal-page-container container', function () {
|
|||||||
insufficientBalance: true,
|
insufficientBalance: true,
|
||||||
isSpeedUp: false,
|
isSpeedUp: false,
|
||||||
isRetry: false,
|
isRetry: false,
|
||||||
|
isSwap: false,
|
||||||
txId: 34,
|
txId: 34,
|
||||||
isEthereumNetwork: true,
|
isEthereumNetwork: true,
|
||||||
isMainnet: true,
|
isMainnet: true,
|
||||||
@ -163,6 +169,7 @@ describe('gas-modal-page-container container', function () {
|
|||||||
transaction: {
|
transaction: {
|
||||||
id: 34,
|
id: 34,
|
||||||
},
|
},
|
||||||
|
value: '0x640000000000000',
|
||||||
}
|
}
|
||||||
const baseMockOwnProps = { transaction: { id: 34 } }
|
const baseMockOwnProps = { transaction: { id: 34 } }
|
||||||
const tests = [
|
const tests = [
|
||||||
|
@ -21,6 +21,7 @@ export default class EditApprovalPermission extends PureComponent {
|
|||||||
tokenBalance: PropTypes.string,
|
tokenBalance: PropTypes.string,
|
||||||
setCustomAmount: PropTypes.func,
|
setCustomAmount: PropTypes.func,
|
||||||
origin: PropTypes.string.isRequired,
|
origin: PropTypes.string.isRequired,
|
||||||
|
requiredMinimum: PropTypes.instanceOf(BigNumber),
|
||||||
}
|
}
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -28,7 +29,8 @@ export default class EditApprovalPermission extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
customSpendLimit: this.props.customTokenAmount,
|
// This is used as a TextField value, which should be a string.
|
||||||
|
customSpendLimit: this.props.customTokenAmount || '',
|
||||||
selectedOptionIsUnlimited: !this.props.customTokenAmount,
|
selectedOptionIsUnlimited: !this.props.customTokenAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,8 +65,10 @@ export default class EditApprovalPermission extends PureComponent {
|
|||||||
address={address}
|
address={address}
|
||||||
diameter={32}
|
diameter={32}
|
||||||
/>
|
/>
|
||||||
<div className="edit-approval-permission__account-info__name">{ name }</div>
|
<div className="edit-approval-permission__name-and-balance-container">
|
||||||
<div>{ t('balance') }</div>
|
<div className="edit-approval-permission__account-info__name">{ name }</div>
|
||||||
|
<div>{ t('balance') }</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="edit-approval-permission__account-info__balance">
|
<div className="edit-approval-permission__account-info__balance">
|
||||||
{`${Number(tokenBalance).toPrecision(9)} ${tokenSymbol}`}
|
{`${Number(tokenBalance).toPrecision(9)} ${tokenSymbol}`}
|
||||||
@ -163,7 +167,7 @@ export default class EditApprovalPermission extends PureComponent {
|
|||||||
|
|
||||||
validateSpendLimit () {
|
validateSpendLimit () {
|
||||||
const { t } = this.context
|
const { t } = this.context
|
||||||
const { decimals } = this.props
|
const { decimals, requiredMinimum } = this.props
|
||||||
const { selectedOptionIsUnlimited, customSpendLimit } = this.state
|
const { selectedOptionIsUnlimited, customSpendLimit } = this.state
|
||||||
|
|
||||||
if (selectedOptionIsUnlimited || !customSpendLimit) {
|
if (selectedOptionIsUnlimited || !customSpendLimit) {
|
||||||
@ -187,6 +191,13 @@ export default class EditApprovalPermission extends PureComponent {
|
|||||||
return t('spendLimitTooLarge')
|
return t('spendLimitTooLarge')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
requiredMinimum !== undefined &&
|
||||||
|
customSpendLimitNumber.lessThan(requiredMinimum)
|
||||||
|
) {
|
||||||
|
return t('spendLimitInsufficient')
|
||||||
|
}
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,12 +46,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
margin-left: 8px;
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
min-width: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__balance {
|
&__balance {
|
||||||
color: #6a737d;
|
color: #6a737d;
|
||||||
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +156,15 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__name-and-balance-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
flex-flow: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-approval-permission-modal-content {
|
.edit-approval-permission-modal-content {
|
||||||
|
@ -5,12 +5,14 @@ import Interaction from '../../ui/icon/interaction-icon.component'
|
|||||||
import Receive from '../../ui/icon/receive-icon.component'
|
import Receive from '../../ui/icon/receive-icon.component'
|
||||||
import Send from '../../ui/icon/send-icon.component'
|
import Send from '../../ui/icon/send-icon.component'
|
||||||
import Sign from '../../ui/icon/sign-icon.component'
|
import Sign from '../../ui/icon/sign-icon.component'
|
||||||
|
import Swap from '../../ui/icon/swap-icon-for-list.component'
|
||||||
import {
|
import {
|
||||||
TRANSACTION_CATEGORY_APPROVAL,
|
TRANSACTION_CATEGORY_APPROVAL,
|
||||||
TRANSACTION_CATEGORY_SIGNATURE_REQUEST,
|
TRANSACTION_CATEGORY_SIGNATURE_REQUEST,
|
||||||
TRANSACTION_CATEGORY_INTERACTION,
|
TRANSACTION_CATEGORY_INTERACTION,
|
||||||
TRANSACTION_CATEGORY_SEND,
|
TRANSACTION_CATEGORY_SEND,
|
||||||
TRANSACTION_CATEGORY_RECEIVE,
|
TRANSACTION_CATEGORY_RECEIVE,
|
||||||
|
TRANSACTION_CATEGORY_SWAP,
|
||||||
UNAPPROVED_STATUS,
|
UNAPPROVED_STATUS,
|
||||||
FAILED_STATUS,
|
FAILED_STATUS,
|
||||||
REJECTED_STATUS,
|
REJECTED_STATUS,
|
||||||
@ -26,6 +28,7 @@ const ICON_MAP = {
|
|||||||
[TRANSACTION_CATEGORY_SEND]: Send,
|
[TRANSACTION_CATEGORY_SEND]: Send,
|
||||||
[TRANSACTION_CATEGORY_SIGNATURE_REQUEST]: Sign,
|
[TRANSACTION_CATEGORY_SIGNATURE_REQUEST]: Sign,
|
||||||
[TRANSACTION_CATEGORY_RECEIVE]: Receive,
|
[TRANSACTION_CATEGORY_RECEIVE]: Receive,
|
||||||
|
[TRANSACTION_CATEGORY_SWAP]: Swap,
|
||||||
}
|
}
|
||||||
|
|
||||||
const FAIL_COLOR = '#D73A49'
|
const FAIL_COLOR = '#D73A49'
|
||||||
|
@ -12,19 +12,36 @@ import * as actions from '../../../ducks/gas/gas.duck'
|
|||||||
import { useI18nContext } from '../../../hooks/useI18nContext'
|
import { useI18nContext } from '../../../hooks/useI18nContext'
|
||||||
import TransactionListItem from '../transaction-list-item'
|
import TransactionListItem from '../transaction-list-item'
|
||||||
import Button from '../../ui/button'
|
import Button from '../../ui/button'
|
||||||
import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions'
|
import { TOKEN_CATEGORY_HASH, TRANSACTION_CATEGORY_SWAP } from '../../../helpers/constants/transactions'
|
||||||
|
import { SWAPS_CONTRACT_ADDRESS } from '../../../helpers/constants/swaps'
|
||||||
|
|
||||||
const PAGE_INCREMENT = 10
|
const PAGE_INCREMENT = 10
|
||||||
|
|
||||||
const getTransactionGroupRecipientAddressFilter = (recipientAddress) => {
|
const getTransactionGroupRecipientAddressFilter = (recipientAddress) => {
|
||||||
return ({ initialTransaction: { txParams } }) => txParams && txParams.to === recipientAddress
|
return ({ initialTransaction: { txParams } }) => {
|
||||||
|
return txParams?.to === recipientAddress || (
|
||||||
|
txParams?.to === SWAPS_CONTRACT_ADDRESS &&
|
||||||
|
txParams.data.match(recipientAddress.slice(2))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenTransactionFilter = ({
|
const tokenTransactionFilter = ({
|
||||||
initialTransaction: {
|
initialTransaction: {
|
||||||
transactionCategory,
|
transactionCategory,
|
||||||
},
|
},
|
||||||
}) => !TOKEN_CATEGORY_HASH[transactionCategory]
|
primaryTransaction: {
|
||||||
|
destinationTokenSymbol,
|
||||||
|
sourceTokenSymbol,
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
if (TOKEN_CATEGORY_HASH[transactionCategory]) {
|
||||||
|
return false
|
||||||
|
} else if (transactionCategory === TRANSACTION_CATEGORY_SWAP) {
|
||||||
|
return destinationTokenSymbol === 'ETH' || sourceTokenSymbol === 'ETH'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const getFilteredTransactionGroups = (transactionGroups, hideTokenTransactions, tokenAddress) => {
|
const getFilteredTransactionGroups = (transactionGroups, hideTokenTransactions, tokenAddress) => {
|
||||||
if (hideTokenTransactions) {
|
if (hideTokenTransactions) {
|
||||||
@ -88,7 +105,11 @@ export default function TransactionList ({ hideTokenTransactions, tokenAddress }
|
|||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
pendingTransactions.map((transactionGroup, index) => (
|
pendingTransactions.map((transactionGroup, index) => (
|
||||||
<TransactionListItem isEarliestNonce={index === 0} transactionGroup={transactionGroup} key={`${transactionGroup.nonce}:${index}`} />
|
<TransactionListItem
|
||||||
|
isEarliestNonce={index === 0}
|
||||||
|
transactionGroup={transactionGroup}
|
||||||
|
key={`${transactionGroup.nonce}:${index}`}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -107,7 +128,10 @@ export default function TransactionList ({ hideTokenTransactions, tokenAddress }
|
|||||||
{
|
{
|
||||||
completedTransactions.length > 0
|
completedTransactions.length > 0
|
||||||
? completedTransactions.slice(0, limit).map((transactionGroup, index) => (
|
? completedTransactions.slice(0, limit).map((transactionGroup, index) => (
|
||||||
<TransactionListItem transactionGroup={transactionGroup} key={`${transactionGroup.nonce}:${limit + index - 10}`} />
|
<TransactionListItem
|
||||||
|
transactionGroup={transactionGroup}
|
||||||
|
key={`${transactionGroup.nonce}:${limit + index - 10}`}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
: (
|
: (
|
||||||
<div className="transaction-list__empty">
|
<div className="transaction-list__empty">
|
||||||
|
@ -4,17 +4,22 @@ import { useDispatch, useSelector } from 'react-redux'
|
|||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
import Button from '../../ui/button'
|
|
||||||
import Identicon from '../../ui/identicon'
|
import Identicon from '../../ui/identicon'
|
||||||
import { I18nContext } from '../../../contexts/i18n'
|
import { I18nContext } from '../../../contexts/i18n'
|
||||||
import { SEND_ROUTE } from '../../../helpers/constants/routes'
|
import { SEND_ROUTE, BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes'
|
||||||
import { useMetricEvent } from '../../../hooks/useMetricEvent'
|
import { useMetricEvent, useNewMetricEvent } from '../../../hooks/useMetricEvent'
|
||||||
import Tooltip from '../../ui/tooltip'
|
import Tooltip from '../../ui/tooltip'
|
||||||
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
|
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
|
||||||
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
|
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
|
||||||
import { showModal } from '../../../store/actions'
|
import { showModal } from '../../../store/actions'
|
||||||
import { isBalanceCached, getSelectedAccount, getShouldShowFiat } from '../../../selectors/selectors'
|
import { isBalanceCached, getSelectedAccount, getShouldShowFiat, getCurrentNetworkId, getCurrentKeyring } from '../../../selectors/selectors'
|
||||||
import PaperAirplane from '../../ui/icon/paper-airplane-icon'
|
import { getValueFromWeiHex } from '../../../helpers/utils/conversions.util'
|
||||||
|
import SwapIcon from '../../ui/icon/swap-icon.component'
|
||||||
|
import BuyIcon from '../../ui/icon/overview-buy-icon.component'
|
||||||
|
import SendIcon from '../../ui/icon/overview-send-icon.component'
|
||||||
|
import { getSwapsFeatureLiveness, setSwapsFromToken } from '../../../ducks/swaps/swaps'
|
||||||
|
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../helpers/constants/swaps'
|
||||||
|
import IconButton from '../../ui/icon-button'
|
||||||
import WalletOverview from './wallet-overview'
|
import WalletOverview from './wallet-overview'
|
||||||
|
|
||||||
const EthOverview = ({ className }) => {
|
const EthOverview = ({ className }) => {
|
||||||
@ -35,10 +40,15 @@ const EthOverview = ({ className }) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
const keyring = useSelector(getCurrentKeyring)
|
||||||
|
const usingHardwareWallet = keyring.type.search('Hardware') !== -1
|
||||||
const balanceIsCached = useSelector(isBalanceCached)
|
const balanceIsCached = useSelector(isBalanceCached)
|
||||||
const showFiat = useSelector(getShouldShowFiat)
|
const showFiat = useSelector(getShouldShowFiat)
|
||||||
const selectedAccount = useSelector(getSelectedAccount)
|
const selectedAccount = useSelector(getSelectedAccount)
|
||||||
const { balance } = selectedAccount
|
const { balance } = selectedAccount
|
||||||
|
const networkId = useSelector(getCurrentNetworkId)
|
||||||
|
const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', properties: { source: 'Main View', active_currency: 'ETH' }, category: 'swaps' })
|
||||||
|
const swapsEnabled = useSelector(getSwapsFeatureLiveness)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WalletOverview
|
<WalletOverview
|
||||||
@ -80,30 +90,53 @@ const EthOverview = ({ className }) => {
|
|||||||
)}
|
)}
|
||||||
buttons={(
|
buttons={(
|
||||||
<>
|
<>
|
||||||
<Button
|
<IconButton
|
||||||
type="primary"
|
|
||||||
className="eth-overview__button"
|
className="eth-overview__button"
|
||||||
rounded
|
Icon={BuyIcon}
|
||||||
|
label={t('buy')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
depositEvent()
|
depositEvent()
|
||||||
dispatch(showModal({ name: 'DEPOSIT_ETHER' }))
|
dispatch(showModal({ name: 'DEPOSIT_ETHER' }))
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{ t('buy') }
|
<IconButton
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="secondary"
|
|
||||||
className="eth-overview__button"
|
className="eth-overview__button"
|
||||||
rounded
|
data-testid="eth-overview-send"
|
||||||
icon={<PaperAirplane color="#037DD6" size={20} />}
|
Icon={SendIcon}
|
||||||
|
label={t('send')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sendEvent()
|
sendEvent()
|
||||||
history.push(SEND_ROUTE)
|
history.push(SEND_ROUTE)
|
||||||
}}
|
}}
|
||||||
data-testid="eth-overview-send"
|
/>
|
||||||
>
|
{swapsEnabled ? (
|
||||||
{ t('send') }
|
<IconButton
|
||||||
</Button>
|
className="eth-overview__button"
|
||||||
|
disabled={networkId !== '1'}
|
||||||
|
Icon={SwapIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (networkId === '1') {
|
||||||
|
enteredSwapsEvent()
|
||||||
|
dispatch(setSwapsFromToken({
|
||||||
|
...ETH_SWAPS_TOKEN_OBJECT,
|
||||||
|
balance,
|
||||||
|
string: getValueFromWeiHex({ value: balance, numberOfDecimals: 4, toDenomination: 'ETH' }),
|
||||||
|
}))
|
||||||
|
if (usingHardwareWallet) {
|
||||||
|
global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE)
|
||||||
|
} else {
|
||||||
|
history.push(BUILD_QUOTE_ROUTE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label={ t('swap') }
|
||||||
|
tooltipRender={(contents) => (
|
||||||
|
<Tooltip title={t('onlyAvailableOnMainnet')} position="bottom" disabled={networkId === '1'}>
|
||||||
|
{contents}
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
className={className}
|
className={className}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 209px;
|
min-height: 209px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -20,7 +20,7 @@
|
|||||||
&__buttons {
|
&__buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
height: 44px;
|
height: 68px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,8 +83,23 @@
|
|||||||
color: $Grey-400;
|
color: $Grey-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-overview &__button {
|
&__button {
|
||||||
@extend %asset-buttons;
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button:last-of-type {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__circle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
background: #037dd6;
|
||||||
|
border-radius: 18px;
|
||||||
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +130,11 @@
|
|||||||
color: $Grey-400;
|
color: $Grey-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-overview &__button {
|
&__button {
|
||||||
@extend %asset-buttons;
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button:last-of-type {
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,22 @@ import PropTypes from 'prop-types'
|
|||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
import Button from '../../ui/button'
|
|
||||||
import Identicon from '../../ui/identicon'
|
import Identicon from '../../ui/identicon'
|
||||||
|
import Tooltip from '../../ui/tooltip'
|
||||||
import CurrencyDisplay from '../../ui/currency-display'
|
import CurrencyDisplay from '../../ui/currency-display'
|
||||||
import { I18nContext } from '../../../contexts/i18n'
|
import { I18nContext } from '../../../contexts/i18n'
|
||||||
import { SEND_ROUTE } from '../../../helpers/constants/routes'
|
import { SEND_ROUTE, BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes'
|
||||||
import { useMetricEvent } from '../../../hooks/useMetricEvent'
|
import { useMetricEvent, useNewMetricEvent } from '../../../hooks/useMetricEvent'
|
||||||
import { useTokenTracker } from '../../../hooks/useTokenTracker'
|
import { useTokenTracker } from '../../../hooks/useTokenTracker'
|
||||||
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'
|
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'
|
||||||
import { getAssetImages } from '../../../selectors/selectors'
|
|
||||||
import { updateSendToken } from '../../../store/actions'
|
import { updateSendToken } from '../../../store/actions'
|
||||||
import PaperAirplane from '../../ui/icon/paper-airplane-icon'
|
import { getSwapsFeatureLiveness, setSwapsFromToken } from '../../../ducks/swaps/swaps'
|
||||||
|
import { getAssetImages, getCurrentKeyring, getCurrentNetworkId } from '../../../selectors/selectors'
|
||||||
|
|
||||||
|
import SwapIcon from '../../ui/icon/swap-icon.component'
|
||||||
|
import SendIcon from '../../ui/icon/overview-send-icon.component'
|
||||||
|
|
||||||
|
import IconButton from '../../ui/icon-button'
|
||||||
import WalletOverview from './wallet-overview'
|
import WalletOverview from './wallet-overview'
|
||||||
|
|
||||||
const TokenOverview = ({ className, token }) => {
|
const TokenOverview = ({ className, token }) => {
|
||||||
@ -28,9 +33,15 @@ const TokenOverview = ({ className, token }) => {
|
|||||||
})
|
})
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const assetImages = useSelector(getAssetImages)
|
const assetImages = useSelector(getAssetImages)
|
||||||
|
|
||||||
|
const keyring = useSelector(getCurrentKeyring)
|
||||||
|
const usingHardwareWallet = keyring.type.search('Hardware') !== -1
|
||||||
const { tokensWithBalances } = useTokenTracker([token])
|
const { tokensWithBalances } = useTokenTracker([token])
|
||||||
const balance = tokensWithBalances[0]?.string
|
const balance = tokensWithBalances[0]?.string
|
||||||
const formattedFiatBalance = useTokenFiatAmount(token.address, balance, token.symbol)
|
const formattedFiatBalance = useTokenFiatAmount(token.address, balance, token.symbol)
|
||||||
|
const networkId = useSelector(getCurrentNetworkId)
|
||||||
|
const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', properties: { source: 'Token View', active_currency: token.symbol }, category: 'swaps' })
|
||||||
|
const swapsEnabled = useSelector(getSwapsFeatureLiveness)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WalletOverview
|
<WalletOverview
|
||||||
@ -55,19 +66,43 @@ const TokenOverview = ({ className, token }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
buttons={(
|
buttons={(
|
||||||
<Button
|
<>
|
||||||
type="secondary"
|
<IconButton
|
||||||
className="token-overview__button"
|
className="token-overview__button"
|
||||||
rounded
|
onClick={() => {
|
||||||
icon={<PaperAirplane color="#037DD6" size={20} />}
|
sendTokenEvent()
|
||||||
onClick={() => {
|
dispatch(updateSendToken(token))
|
||||||
sendTokenEvent()
|
history.push(SEND_ROUTE)
|
||||||
dispatch(updateSendToken(token))
|
}}
|
||||||
history.push(SEND_ROUTE)
|
Icon={SendIcon}
|
||||||
}}
|
label={t('send')}
|
||||||
>
|
data-testid="eth-overview-send"
|
||||||
{ t('send') }
|
/>
|
||||||
</Button>
|
{swapsEnabled ? (
|
||||||
|
<IconButton
|
||||||
|
className="token-overview__button"
|
||||||
|
disabled={networkId !== '1'}
|
||||||
|
Icon={SwapIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (networkId === '1') {
|
||||||
|
enteredSwapsEvent()
|
||||||
|
dispatch(setSwapsFromToken({ ...token, iconUrl: assetImages[token.address] }))
|
||||||
|
if (usingHardwareWallet) {
|
||||||
|
global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE)
|
||||||
|
} else {
|
||||||
|
history.push(BUILD_QUOTE_ROUTE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label={ t('swap') }
|
||||||
|
tooltipRender={(contents) => (
|
||||||
|
<Tooltip title={t('onlyAvailableOnMainnet')} position="bottom" disabled={networkId === '1'}>
|
||||||
|
{contents}
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
className={className}
|
className={className}
|
||||||
icon={(
|
icon={(
|
||||||
|
@ -50,9 +50,9 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
font-weight: bold;
|
background: $Blue-500;
|
||||||
background: $Blue-300;
|
|
||||||
color: white;
|
color: white;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--danger {
|
&--danger {
|
||||||
@ -61,7 +61,6 @@
|
|||||||
background: $white;
|
background: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 0 14px rgba(0, 0, 0, 0.18);
|
box-shadow: 0 0 14px rgba(0, 0, 0, 0.18);
|
||||||
};
|
};
|
||||||
|
37
ui/app/components/ui/icon-button/icon-button.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
const defaultRender = (inner) => inner
|
||||||
|
|
||||||
|
export default function IconButton ({ onClick, Icon, disabled, label, tooltipRender, className, ...props }) {
|
||||||
|
const renderWrapper = tooltipRender ?? defaultRender
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames('icon-button', className, { 'icon-button--disabled': disabled })}
|
||||||
|
data-testid={props['data-testid'] ?? undefined}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{renderWrapper(
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="icon-button__circle"
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
</div>
|
||||||
|
<span>{label}</span>
|
||||||
|
</>,
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
Icon: PropTypes.element.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
tooltipRender: PropTypes.func,
|
||||||
|
className: PropTypes.string,
|
||||||
|
'data-testid': PropTypes.string,
|
||||||
|
}
|
30
ui/app/components/ui/icon-button/icon-button.scss
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.icon-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background-color: unset;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@include H7;
|
||||||
|
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $Blue-500;
|
||||||
|
|
||||||
|
&__circle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
background: #037dd6;
|
||||||
|
border-radius: 18px;
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
}
|
1
ui/app/components/ui/icon-button/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './icon-button'
|
@ -1,11 +1,14 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React, { PureComponent } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
export default class IconWithFallback extends PureComponent {
|
export default class IconWithFallback extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number,
|
||||||
|
className: PropTypes.string,
|
||||||
|
fallbackClassName: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -18,8 +21,8 @@ export default class IconWithFallback extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { icon, name, size } = this.props
|
const { icon, name, size, className, fallbackClassName } = this.props
|
||||||
const style = { height: `${size}px`, width: `${size}px` }
|
const style = size ? { height: `${size}px`, width: `${size}px` } : {}
|
||||||
|
|
||||||
return !this.state.iconError && icon
|
return !this.state.iconError && icon
|
||||||
? (
|
? (
|
||||||
@ -27,10 +30,11 @@ export default class IconWithFallback extends PureComponent {
|
|||||||
onError={() => this.setState({ iconError: true })}
|
onError={() => this.setState({ iconError: true })}
|
||||||
src={icon}
|
src={icon}
|
||||||
style={style}
|
style={style}
|
||||||
|
className={className}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<i className="icon-with-fallback__fallback">
|
<i className={classnames('icon-with-fallback__fallback', fallbackClassName)}>
|
||||||
{ name.length ? name.charAt(0).toUpperCase() : '' }
|
{ name.length ? name.charAt(0).toUpperCase() : '' }
|
||||||
</i>
|
</i>
|
||||||
)
|
)
|
||||||
|
25
ui/app/components/ui/icon/overview-buy-icon.component.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function BuyIcon ({
|
||||||
|
width = '17',
|
||||||
|
height = '21',
|
||||||
|
fill = 'white',
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M8.62829 14.3216C8.65369 14.2947 8.67756 14.2664 8.69978 14.2368L12.8311 10.1286C13.0886 9.87975 13.1913 9.51233 13.1 9.16703C13.0087 8.82174 12.7375 8.55207 12.3903 8.46129C12.0431 8.37051 11.6736 8.47268 11.4233 8.72869L8.89913 11.2387L8.89913 1.3293C8.90647 0.970874 8.71837 0.636511 8.40739 0.455161C8.0964 0.273811 7.71112 0.27381 7.40014 0.45516C7.08915 0.636511 6.90105 0.970873 6.90839 1.3293L6.90839 11.2387L4.38422 8.72869C4.13396 8.47268 3.76446 8.37051 3.41722 8.46129C3.06998 8.55207 2.79879 8.82174 2.7075 9.16703C2.61621 9.51233 2.71896 9.87975 2.97641 10.1286L7.11049 14.2395C7.28724 14.4717 7.55784 14.6148 7.85026 14.6306C8.14268 14.6464 8.42727 14.5333 8.62829 14.3216Z"
|
||||||
|
fill={fill}
|
||||||
|
/>
|
||||||
|
<rect x="0.260986" y="15.75" width="15.8387" height="2.25" rx="1" fill="white" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BuyIcon.propTypes = {
|
||||||
|
width: PropTypes.string,
|
||||||
|
height: PropTypes.string,
|
||||||
|
fill: PropTypes.string,
|
||||||
|
}
|
23
ui/app/components/ui/icon/overview-send-icon.component.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function SwapIcon ({
|
||||||
|
width = '15',
|
||||||
|
height = '15',
|
||||||
|
fill = 'white',
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M13.6827 0.889329C13.6458 0.890495 13.609 0.893722 13.5725 0.898996H7.76263C7.40564 0.893947 7.07358 1.08151 6.89361 1.38986C6.71364 1.69821 6.71364 2.07958 6.89361 2.38793C7.07358 2.69628 7.40564 2.88384 7.76263 2.87879H11.3124L1.12335 13.0678C0.864749 13.3161 0.760577 13.6848 0.851011 14.0315C0.941446 14.3786 1.21235 14.6495 1.55926 14.7399C1.90616 14.8303 2.27485 14.7262 2.52313 14.4676L12.7121 4.27857V7.82829C12.7071 8.18528 12.8946 8.51734 13.203 8.69731C13.5113 8.87728 13.8927 8.87728 14.2011 8.69731C14.5094 8.51734 14.697 8.18528 14.6919 7.82829V2.01457C14.7318 1.7261 14.6427 1.43469 14.4483 1.2179C14.2538 1.00111 13.9738 0.880924 13.6827 0.889329Z"
|
||||||
|
fill={fill}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SwapIcon.propTypes = {
|
||||||
|
width: PropTypes.string,
|
||||||
|
height: PropTypes.string,
|
||||||
|
fill: PropTypes.string,
|
||||||
|
}
|
23
ui/app/components/ui/icon/sun-check-icon.component.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function SunCheck ({ reverseColors }) {
|
||||||
|
const sunColor = reverseColors ? '#037DD6' : 'white'
|
||||||
|
const checkColor = reverseColors ? 'white' : '#037DD6'
|
||||||
|
return (
|
||||||
|
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M13.2148 9.05384C13.432 8.40203 14.8878 7.92403 14.8878 7.20703C14.8878 6.49003 13.432 6.01204 13.2148 5.36022C12.9975 4.68668 13.8883 3.44823 13.4755 2.88332C13.0627 2.31842 11.607 2.77469 11.0421 2.3836C10.4771 1.97078 10.4771 0.449879 9.80361 0.232608C9.15179 0.0153358 8.26098 1.25378 7.54398 1.25378C6.82698 1.25378 5.91444 0.0153358 5.28435 0.232608C4.61081 0.449879 4.61081 1.99251 4.04591 2.3836C3.481 2.79641 2.02528 2.31842 1.61246 2.88332C1.19965 3.44823 2.09046 4.68668 1.87319 5.36022C1.65592 6.01204 0.200195 6.49003 0.200195 7.20703C0.200195 7.92403 1.65592 8.40203 1.87319 9.05384C2.09046 9.72738 1.19965 10.9658 1.61246 11.5307C2.02528 12.0956 3.481 11.6394 4.04591 12.0305C4.61081 12.4433 4.61081 13.9642 5.28435 14.1815C5.93617 14.3987 6.82698 13.1603 7.54398 13.1603C8.26098 13.1603 9.17352 14.3987 9.80361 14.1815C10.4771 13.9642 10.4771 12.4216 11.0421 12.0305C11.607 11.6176 13.0627 12.0956 13.4755 11.5307C13.8883 10.9658 12.9975 9.70566 13.2148 9.05384Z"
|
||||||
|
fill={sunColor}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M6.42285 10.084L4.13965 7.81445C4.07585 7.75065 4.04395 7.66862 4.04395 7.56836C4.04395 7.4681 4.07585 7.38607 4.13965 7.32227L4.64551 6.83008C4.70931 6.75716 4.78678 6.7207 4.87793 6.7207C4.97819 6.7207 5.06478 6.75716 5.1377 6.83008L6.66895 8.36133L9.9502 5.08008C10.0231 5.00716 10.1051 4.9707 10.1963 4.9707C10.2965 4.9707 10.3786 5.00716 10.4424 5.08008L10.9482 5.57227C11.012 5.63607 11.0439 5.7181 11.0439 5.81836C11.0439 5.91862 11.012 6.00065 10.9482 6.06445L6.91504 10.084C6.85124 10.1569 6.76921 10.1934 6.66895 10.1934C6.56868 10.1934 6.48665 10.1569 6.42285 10.084Z"
|
||||||
|
fill={checkColor}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SunCheck.propTypes = {
|
||||||
|
reverseColors: PropTypes.bool,
|
||||||
|
}
|
25
ui/app/components/ui/icon/swap-icon-for-list.component.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
const Swap = ({
|
||||||
|
className,
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
}) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M17 33C25.8366 33 33 25.8366 33 17C33 8.16344 25.8366 1 17 1C8.16344 1 1 8.16344 1 17C1 25.8366 8.16344 33 17 33Z" stroke={color} />
|
||||||
|
<path d="M21.4444 21.2214C21.4444 21.4147 21.2877 21.5714 21.0944 21.5714H12.9056C12.7123 21.5714 12.5556 21.4147 12.5556 21.2214V19.6907C12.5556 19.3765 12.1736 19.2214 11.9546 19.4467L9.2372 22.2417C9.10513 22.3776 9.10513 22.5938 9.2372 22.7297L11.9546 25.5247C12.1736 25.75 12.5556 25.595 12.5556 25.2808V23.75C12.5556 23.5567 12.7123 23.4 12.9056 23.4H22.8722C23.0655 23.4 23.2222 23.2433 23.2222 23.05V18.2643C23.2222 18.071 23.0655 17.9143 22.8722 17.9143H21.7944C21.6011 17.9143 21.4444 18.071 21.4444 18.2643V21.2214ZM12.5556 13.9214C12.5556 13.7281 12.7123 13.5714 12.9056 13.5714H21.0944C21.2877 13.5714 21.4444 13.7281 21.4444 13.9214V15.4522C21.4444 15.7664 21.8264 15.9214 22.0454 15.6962L24.7628 12.9011C24.8949 12.7653 24.8949 12.549 24.7628 12.4132L22.0454 9.61812C21.8264 9.39284 21.4444 9.5479 21.4444 9.8621V11.3929C21.4444 11.5862 21.2877 11.7429 21.0944 11.7429H11.1278C10.9345 11.7429 10.7778 11.8996 10.7778 12.0929V16.8786C10.7778 17.0719 10.9345 17.2286 11.1278 17.2286H12.2056C12.3989 17.2286 12.5556 17.0719 12.5556 16.8786V13.9214Z" fill={color} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
Swap.defaultProps = {
|
||||||
|
className: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
Swap.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
color: PropTypes.string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Swap
|
25
ui/app/components/ui/icon/swap-icon.component.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function SwapIcon ({
|
||||||
|
width = '17',
|
||||||
|
height = '17',
|
||||||
|
color = 'white',
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M13.1714 9.66035V12.3786H4.68253C4.51685 12.3786 4.38253 12.2443 4.38253 12.0786V10.5478C4.38253 10.1888 3.94605 10.0116 3.69574 10.269L0.978328 13.0641C0.827392 13.2193 0.827392 13.4665 0.978328 13.6217L3.69573 16.4168C3.94604 16.6742 4.38253 16.497 4.38253 16.1379V14.6072C4.38253 14.4415 4.51685 14.3072 4.68253 14.3072H14.9992H15.0492V14.2572V9.66035C15.0492 9.14182 14.6288 8.72146 14.1103 8.72146C13.5918 8.72146 13.1714 9.14182 13.1714 9.66035ZM2.55476 2.55003H2.50476V2.60003V7.19686C2.50476 7.71539 2.92511 8.13575 3.44364 8.13575C3.96218 8.13575 4.38253 7.71539 4.38253 7.19686V4.70619C4.38253 4.5805 4.48443 4.47861 4.61012 4.47861H12.8714C13.0371 4.47861 13.1714 4.61292 13.1714 4.77861V6.30937C13.1714 6.66845 13.6079 6.84566 13.8582 6.5882L16.5756 3.79315C16.7266 3.6379 16.7266 3.39074 16.5756 3.23549L13.8582 0.440443C13.6079 0.182981 13.1714 0.360188 13.1714 0.719273V2.25004C13.1714 2.41572 13.0371 2.55003 12.8714 2.55003H2.55476Z"
|
||||||
|
fill={color}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="0.1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SwapIcon.propTypes = {
|
||||||
|
width: PropTypes.string,
|
||||||
|
height: PropTypes.string,
|
||||||
|
color: PropTypes.string,
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
.info-tooltip {
|
.info-tooltip {
|
||||||
img {
|
img {
|
||||||
height: 10px;
|
height: 12px;
|
||||||
width: 10px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +32,7 @@
|
|||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
|
|
||||||
.tippy-tooltip-content {
|
.tippy-tooltip-content {
|
||||||
@include H8;
|
@include H7;
|
||||||
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: $Grey-500;
|
color: $Grey-500;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import classnames from 'classnames'
|
||||||
import Button from '../../button'
|
import Button from '../../button'
|
||||||
|
|
||||||
export default class PageContainerFooter extends Component {
|
export default class PageContainerFooter extends Component {
|
||||||
@ -15,6 +16,8 @@ export default class PageContainerFooter extends Component {
|
|||||||
submitButtonType: PropTypes.string,
|
submitButtonType: PropTypes.string,
|
||||||
hideCancel: PropTypes.bool,
|
hideCancel: PropTypes.bool,
|
||||||
buttonSizeLarge: PropTypes.bool,
|
buttonSizeLarge: PropTypes.bool,
|
||||||
|
footerClassName: PropTypes.string,
|
||||||
|
footerButtonClassName: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -33,17 +36,19 @@ export default class PageContainerFooter extends Component {
|
|||||||
hideCancel,
|
hideCancel,
|
||||||
cancelButtonType,
|
cancelButtonType,
|
||||||
buttonSizeLarge = false,
|
buttonSizeLarge = false,
|
||||||
|
footerClassName,
|
||||||
|
footerButtonClassName,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container__footer">
|
<div className={classnames('page-container__footer', footerClassName)}>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{!hideCancel && (
|
{!hideCancel && (
|
||||||
<Button
|
<Button
|
||||||
type={cancelButtonType || 'default'}
|
type={cancelButtonType || 'default'}
|
||||||
large={buttonSizeLarge}
|
large={buttonSizeLarge}
|
||||||
className="page-container__footer-button"
|
className={classnames('page-container__footer-button', footerButtonClassName)}
|
||||||
onClick={(e) => onCancel(e)}
|
onClick={(e) => onCancel(e)}
|
||||||
data-testid="page-container-footer-cancel"
|
data-testid="page-container-footer-cancel"
|
||||||
>
|
>
|
||||||
@ -54,7 +59,7 @@ export default class PageContainerFooter extends Component {
|
|||||||
<Button
|
<Button
|
||||||
type={submitButtonType || 'secondary'}
|
type={submitButtonType || 'secondary'}
|
||||||
large={buttonSizeLarge}
|
large={buttonSizeLarge}
|
||||||
className="page-container__footer-button"
|
className={classnames('page-container__footer-button', footerButtonClassName)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={(e) => onSubmit(e)}
|
onClick={(e) => onSubmit(e)}
|
||||||
data-testid="page-container-footer-next"
|
data-testid="page-container-footer-next"
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
&__loading-dot-two,
|
&__loading-dot-two,
|
||||||
&__loading-dot-three {
|
&__loading-dot-three {
|
||||||
background: $Blue-500;
|
background: $Blue-500;
|
||||||
width: 4px;
|
width: 9px;
|
||||||
height: 4px;
|
height: 9px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
|
30
ui/app/components/ui/tooltip/index.scss
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.tippy-tooltip.white-theme {
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
box-shadow: 0 0 14px rgba(0, 0, 0, 0.18);
|
||||||
|
padding: 12px 16px;
|
||||||
|
padding-bottom: 11px;
|
||||||
|
|
||||||
|
.tippy-tooltip-content {
|
||||||
|
@include H7;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
color: $Grey-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-popper[x-placement^=top] .white-theme [x-arrow] {
|
||||||
|
border-top-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-popper[x-placement^=right] .white-theme [x-arrow] {
|
||||||
|
border-right-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-popper[x-placement^=left] .white-theme [x-arrow] {
|
||||||
|
border-left-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-popper[x-placement^=bottom] .white-theme [x-arrow] {
|
||||||
|
border-bottom-color: $white;
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
@import 'error-message/index';
|
@import 'error-message/index';
|
||||||
@import 'export-text-container/index';
|
@import 'export-text-container/index';
|
||||||
@import 'icon-border/icon-border';
|
@import 'icon-border/icon-border';
|
||||||
|
@import 'icon-button/icon-button';
|
||||||
@import 'icon-with-fallback/icon-with-fallback';
|
@import 'icon-with-fallback/icon-with-fallback';
|
||||||
@import 'icon-with-label/index';
|
@import 'icon-with-label/index';
|
||||||
@import 'icon/index';
|
@import 'icon/index';
|
||||||
@ -35,4 +36,6 @@
|
|||||||
@import 'tabs/index';
|
@import 'tabs/index';
|
||||||
@import 'toggle-button/index';
|
@import 'toggle-button/index';
|
||||||
@import 'token-balance/index';
|
@import 'token-balance/index';
|
||||||
|
@import 'tooltip/index';
|
||||||
@import 'unit-input/index';
|
@import 'unit-input/index';
|
||||||
|
@import 'url-icon/index';
|
||||||
|
1
ui/app/components/ui/url-icon/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './url-icon'
|
38
ui/app/components/ui/url-icon/index.scss
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.url-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $white;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba($black, 0.24);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
-moz-animation: fadein 1s;
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
-webkit-animation: fadein 1s;
|
||||||
|
|
||||||
|
/* Safari and Chrome */
|
||||||
|
-o-animation: fadein 1s;
|
||||||
|
|
||||||
|
&__fallback {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #bbc0c5;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
25
ui/app/components/ui/url-icon/url-icon.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import IconWithFallback from '../icon-with-fallback'
|
||||||
|
|
||||||
|
export default function UrlIcon ({
|
||||||
|
url,
|
||||||
|
className,
|
||||||
|
name,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<IconWithFallback
|
||||||
|
className={classnames('url-icon', className)}
|
||||||
|
icon={url}
|
||||||
|
name={name}
|
||||||
|
fallbackClassName="url-icon__fallback"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlIcon.propTypes = {
|
||||||
|
url: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
}
|
@ -7,6 +7,7 @@ import appStateReducer from './app/app'
|
|||||||
import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck'
|
import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck'
|
||||||
import gasReducer from './gas/gas.duck'
|
import gasReducer from './gas/gas.duck'
|
||||||
import { invalidCustomNetwork, unconnectedAccount } from './alerts'
|
import { invalidCustomNetwork, unconnectedAccount } from './alerts'
|
||||||
|
import swapsReducer from './swaps/swaps'
|
||||||
import historyReducer from './history/history'
|
import historyReducer from './history/history'
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
@ -18,6 +19,7 @@ export default combineReducers({
|
|||||||
history: historyReducer,
|
history: historyReducer,
|
||||||
send: sendReducer,
|
send: sendReducer,
|
||||||
confirmTransaction: confirmTransactionReducer,
|
confirmTransaction: confirmTransactionReducer,
|
||||||
|
swaps: swapsReducer,
|
||||||
gas: gasReducer,
|
gas: gasReducer,
|
||||||
localeMessages: localeMessagesReducer,
|
localeMessages: localeMessagesReducer,
|
||||||
})
|
})
|
||||||
|
570
ui/app/ducks/swaps/swaps.js
Normal file
@ -0,0 +1,570 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import log from 'loglevel'
|
||||||
|
|
||||||
|
import {
|
||||||
|
addToken,
|
||||||
|
addUnapprovedTransaction,
|
||||||
|
fetchAndSetQuotes,
|
||||||
|
forceUpdateMetamaskState,
|
||||||
|
resetSwapsPostFetchState,
|
||||||
|
setBackgroundSwapRouteState,
|
||||||
|
setInitialGasEstimate,
|
||||||
|
setSwapsErrorKey,
|
||||||
|
setSwapsTxGasPrice,
|
||||||
|
setApproveTxId,
|
||||||
|
setTradeTxId,
|
||||||
|
stopPollingForQuotes,
|
||||||
|
updateAndApproveTx,
|
||||||
|
updateTransaction,
|
||||||
|
resetBackgroundSwapsState,
|
||||||
|
setSwapsLiveness,
|
||||||
|
} from '../../store/actions'
|
||||||
|
import { AWAITING_SWAP_ROUTE, BUILD_QUOTE_ROUTE, LOADING_QUOTES_ROUTE, SWAPS_ERROR_ROUTE, SWAPS_MAINTENANCE_ROUTE } from '../../helpers/constants/routes'
|
||||||
|
import { fetchTradesInfo, fetchSwapsFeatureLiveness } from '../../pages/swaps/swaps.util'
|
||||||
|
import { calcGasTotal } from '../../pages/send/send.utils'
|
||||||
|
import { decimalToHex, getValueFromWeiHex, hexMax, decGWEIToHexWEI, hexWEIToDecETH } from '../../helpers/utils/conversions.util'
|
||||||
|
import { constructTxParams } from '../../helpers/utils/util'
|
||||||
|
import { calcTokenAmount } from '../../helpers/utils/token-util'
|
||||||
|
import {
|
||||||
|
getFastPriceEstimateInHexWEI,
|
||||||
|
getSelectedAccount,
|
||||||
|
getTokenExchangeRates,
|
||||||
|
conversionRateSelector as getConversionRate,
|
||||||
|
} from '../../selectors'
|
||||||
|
import {
|
||||||
|
ERROR_FETCHING_QUOTES,
|
||||||
|
QUOTES_NOT_AVAILABLE_ERROR,
|
||||||
|
ETH_SWAPS_TOKEN_OBJECT,
|
||||||
|
SWAP_FAILED_ERROR,
|
||||||
|
} from '../../helpers/constants/swaps'
|
||||||
|
import { SWAP, SWAP_APPROVAL } from '../../helpers/constants/transactions'
|
||||||
|
import { fetchBasicGasAndTimeEstimates, fetchGasEstimates, resetCustomData } from '../gas/gas.duck'
|
||||||
|
import { formatCurrency } from '../../helpers/utils/confirm-tx.util'
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
aggregatorMetadata: null,
|
||||||
|
approveTxId: null,
|
||||||
|
balanceError: false,
|
||||||
|
fetchingQuotes: false,
|
||||||
|
fromToken: null,
|
||||||
|
quotesFetchStartTime: null,
|
||||||
|
topAssets: {},
|
||||||
|
toToken: null,
|
||||||
|
metamaskFeeAmount: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = createSlice({
|
||||||
|
name: 'swaps',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
clearSwapsState: () => initialState,
|
||||||
|
navigatedBackToBuildQuote: (state) => {
|
||||||
|
state.approveTxId = null
|
||||||
|
state.balanceError = false
|
||||||
|
state.fetchingQuotes = false
|
||||||
|
},
|
||||||
|
retriedGetQuotes: (state) => {
|
||||||
|
state.approveTxId = null
|
||||||
|
state.balanceError = false
|
||||||
|
state.fetchingQuotes = false
|
||||||
|
},
|
||||||
|
setAggregatorMetadata: (state, action) => {
|
||||||
|
state.aggregatorMetadata = action.payload
|
||||||
|
},
|
||||||
|
setBalanceError: (state, action) => {
|
||||||
|
state.balanceError = action.payload
|
||||||
|
},
|
||||||
|
setFetchingQuotes: (state, action) => {
|
||||||
|
state.fetchingQuotes = action.payload
|
||||||
|
},
|
||||||
|
setFromToken: (state, action) => {
|
||||||
|
state.fromToken = action.payload
|
||||||
|
},
|
||||||
|
setQuotesFetchStartTime: (state, action) => {
|
||||||
|
state.quotesFetchStartTime = action.payload
|
||||||
|
},
|
||||||
|
setTopAssets: (state, action) => {
|
||||||
|
state.topAssets = action.payload
|
||||||
|
},
|
||||||
|
setToToken: (state, action) => {
|
||||||
|
state.toToken = action.payload
|
||||||
|
},
|
||||||
|
setMetamaskFeeAmount: (state, action) => {
|
||||||
|
state.metamaskFeeAmount = action.payload
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { actions, reducer } = slice
|
||||||
|
|
||||||
|
export default reducer
|
||||||
|
|
||||||
|
// Selectors
|
||||||
|
|
||||||
|
export const getAggregatorMetadata = (state) => state.swaps.aggregatorMetadata
|
||||||
|
|
||||||
|
export const getBalanceError = (state) => state.swaps.balanceError
|
||||||
|
|
||||||
|
export const getFromToken = (state) => state.swaps.fromToken
|
||||||
|
|
||||||
|
export const getTopAssets = (state) => state.swaps.topAssets
|
||||||
|
|
||||||
|
export const getToToken = (state) => state.swaps.toToken
|
||||||
|
|
||||||
|
export const getMetaMaskFeeAmount = (state) => state.swaps.metamaskFeeAmount
|
||||||
|
|
||||||
|
export const getFetchingQuotes = (state) => state.swaps.fetchingQuotes
|
||||||
|
|
||||||
|
export const getQuotesFetchStartTime = (state) => state.swaps.quotesFetchStartTime
|
||||||
|
|
||||||
|
// Background selectors
|
||||||
|
|
||||||
|
const getSwapsState = (state) => state.metamask.swapsState
|
||||||
|
|
||||||
|
export const getSwapsFeatureLiveness = (state) => state.metamask.swapsState.swapsFeatureIsLive
|
||||||
|
|
||||||
|
export const getBackgroundSwapRouteState = (state) => state.metamask.swapsState.routeState
|
||||||
|
|
||||||
|
export const getCustomSwapsGas = (state) => state.metamask.swapsState.customMaxGas
|
||||||
|
|
||||||
|
export const getCustomSwapsGasPrice = (state) => state.metamask.swapsState.customGasPrice
|
||||||
|
|
||||||
|
export const getFetchParams = (state) => state.metamask.swapsState.fetchParams
|
||||||
|
|
||||||
|
export const getMaxMode = (state) => state.metamask.swapsState.maxMode
|
||||||
|
|
||||||
|
export const getQuotes = (state) => state.metamask.swapsState.quotes
|
||||||
|
|
||||||
|
export const getQuotesLastFetched = (state) => state.metamask.swapsState.quotesLastFetched
|
||||||
|
|
||||||
|
export const getSelectedQuote = (state) => {
|
||||||
|
const { selectedAggId, quotes } = getSwapsState(state)
|
||||||
|
return quotes[selectedAggId]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSwapsErrorKey = (state) => getSwapsState(state)?.errorKey
|
||||||
|
|
||||||
|
export const getShowQuoteLoadingScreen = (state) => state.swaps.showQuoteLoadingScreen
|
||||||
|
|
||||||
|
export const getSwapsTokens = (state) => state.metamask.swapsState.tokens
|
||||||
|
|
||||||
|
export const getSwapsWelcomeMessageSeenStatus = (state) => state.metamask.swapsWelcomeMessageHasBeenShown
|
||||||
|
|
||||||
|
export const getTopQuote = (state) => {
|
||||||
|
const { topAggId, quotes } = getSwapsState(state)
|
||||||
|
return quotes[topAggId]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getApproveTxId = (state) => state.metamask.swapsState.approveTxId
|
||||||
|
|
||||||
|
export const getTradeTxId = (state) => state.metamask.swapsState.tradeTxId
|
||||||
|
|
||||||
|
export const getUsedQuote = (state) => getSelectedQuote(state) || getTopQuote(state)
|
||||||
|
|
||||||
|
// Compound selectors
|
||||||
|
|
||||||
|
export const getDestinationTokenInfo = (state) => getFetchParams(state)?.metaData?.destinationTokenInfo
|
||||||
|
|
||||||
|
export const getSwapsTradeTxParams = (state) => {
|
||||||
|
const { selectedAggId, topAggId, quotes } = getSwapsState(state)
|
||||||
|
const usedQuote = selectedAggId ? quotes[selectedAggId] : quotes[topAggId]
|
||||||
|
if (!usedQuote) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const { trade } = usedQuote
|
||||||
|
const gas = getCustomSwapsGas(state) || trade.gas
|
||||||
|
const gasPrice = getCustomSwapsGasPrice(state) || trade.gasPrice
|
||||||
|
return { ...trade, gas, gasPrice }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getApproveTxParams = (state) => {
|
||||||
|
const { approvalNeeded } = getSelectedQuote(state) || getTopQuote(state) || {}
|
||||||
|
|
||||||
|
if (!approvalNeeded) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const data = getSwapsState(state)?.customApproveTxData || approvalNeeded.data
|
||||||
|
|
||||||
|
const gasPrice = getCustomSwapsGasPrice(state) || approvalNeeded.gasPrice
|
||||||
|
return { ...approvalNeeded, gasPrice, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions / action-creators
|
||||||
|
|
||||||
|
const {
|
||||||
|
clearSwapsState,
|
||||||
|
navigatedBackToBuildQuote,
|
||||||
|
retriedGetQuotes,
|
||||||
|
setAggregatorMetadata,
|
||||||
|
setBalanceError,
|
||||||
|
setFetchingQuotes,
|
||||||
|
setFromToken,
|
||||||
|
setQuotesFetchStartTime,
|
||||||
|
setTopAssets,
|
||||||
|
setToToken,
|
||||||
|
setMetamaskFeeAmount,
|
||||||
|
} = actions
|
||||||
|
|
||||||
|
export {
|
||||||
|
clearSwapsState,
|
||||||
|
setAggregatorMetadata,
|
||||||
|
setBalanceError,
|
||||||
|
setFetchingQuotes,
|
||||||
|
setFromToken as setSwapsFromToken,
|
||||||
|
setQuotesFetchStartTime as setSwapQuotesFetchStartTime,
|
||||||
|
setTopAssets,
|
||||||
|
setToToken as setSwapToToken,
|
||||||
|
setMetamaskFeeAmount,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const navigateBackToBuildQuote = (history) => {
|
||||||
|
return async (dispatch) => {
|
||||||
|
// TODO: Ensure any fetch in progress is cancelled
|
||||||
|
await dispatch(resetSwapsPostFetchState())
|
||||||
|
dispatch(navigatedBackToBuildQuote())
|
||||||
|
|
||||||
|
history.push(BUILD_QUOTE_ROUTE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prepareForRetryGetQuotes = () => {
|
||||||
|
return async (dispatch) => {
|
||||||
|
// TODO: Ensure any fetch in progress is cancelled
|
||||||
|
await dispatch(resetSwapsPostFetchState())
|
||||||
|
dispatch(retriedGetQuotes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prepareToLeaveSwaps = () => {
|
||||||
|
return async (dispatch) => {
|
||||||
|
dispatch(resetCustomData())
|
||||||
|
dispatch(clearSwapsState())
|
||||||
|
await dispatch(resetBackgroundSwapsState())
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchAndSetSwapsGasPriceInfo = () => {
|
||||||
|
return async (dispatch) => {
|
||||||
|
const basicEstimates = await dispatch(fetchBasicGasAndTimeEstimates())
|
||||||
|
dispatch(setSwapsTxGasPrice(decGWEIToHexWEI(basicEstimates.fast)))
|
||||||
|
await dispatch(fetchGasEstimates(basicEstimates.blockTime))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchQuotesAndSetQuoteState = (history, inputValue, maxSlippage, metaMetricsEvent) => {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
let swapsFeatureIsLive = false
|
||||||
|
try {
|
||||||
|
swapsFeatureIsLive = await fetchSwapsFeatureLiveness()
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to fetch Swaps liveness, defaulting to false.', error)
|
||||||
|
}
|
||||||
|
await dispatch(setSwapsLiveness(swapsFeatureIsLive))
|
||||||
|
|
||||||
|
if (!swapsFeatureIsLive) {
|
||||||
|
await history.push(SWAPS_MAINTENANCE_ROUTE)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getState()
|
||||||
|
const fetchParams = getFetchParams(state)
|
||||||
|
const selectedAccount = getSelectedAccount(state)
|
||||||
|
const balanceError = getBalanceError(state)
|
||||||
|
const fetchParamsFromToken = fetchParams?.metaData?.sourceTokenInfo?.symbol === 'ETH' ?
|
||||||
|
{
|
||||||
|
...ETH_SWAPS_TOKEN_OBJECT,
|
||||||
|
string: getValueFromWeiHex({ value: selectedAccount.balance, numberOfDecimals: 4, toDenomination: 'ETH' }),
|
||||||
|
balance: selectedAccount.balance,
|
||||||
|
} :
|
||||||
|
fetchParams?.metaData?.sourceTokenInfo
|
||||||
|
const selectedFromToken = getFromToken(state) || fetchParamsFromToken || {}
|
||||||
|
const selectedToToken = getToToken(state) || fetchParams?.metaData?.destinationTokenInfo || {}
|
||||||
|
const {
|
||||||
|
address: fromTokenAddress,
|
||||||
|
symbol: fromTokenSymbol,
|
||||||
|
decimals: fromTokenDecimals,
|
||||||
|
iconUrl: fromTokenIconUrl,
|
||||||
|
balance: fromTokenBalance,
|
||||||
|
} = selectedFromToken
|
||||||
|
const {
|
||||||
|
address: toTokenAddress,
|
||||||
|
symbol: toTokenSymbol,
|
||||||
|
decimals: toTokenDecimals,
|
||||||
|
iconUrl: toTokenIconUrl,
|
||||||
|
} = selectedToToken
|
||||||
|
await dispatch(setBackgroundSwapRouteState('loading'))
|
||||||
|
history.push(LOADING_QUOTES_ROUTE)
|
||||||
|
dispatch(setFetchingQuotes(true))
|
||||||
|
|
||||||
|
const contractExchangeRates = getTokenExchangeRates(state)
|
||||||
|
|
||||||
|
let destinationTokenAddedForSwap = false
|
||||||
|
if (toTokenSymbol !== 'ETH' && !contractExchangeRates[toTokenAddress]) {
|
||||||
|
destinationTokenAddedForSwap = true
|
||||||
|
await dispatch(addToken(toTokenAddress, toTokenSymbol, toTokenDecimals, toTokenIconUrl, true))
|
||||||
|
}
|
||||||
|
if (fromTokenSymbol !== 'ETH' && !contractExchangeRates[fromTokenAddress] && fromTokenBalance && (new BigNumber(fromTokenBalance, 16)).gt(0)) {
|
||||||
|
dispatch(addToken(fromTokenAddress, fromTokenSymbol, fromTokenDecimals, fromTokenIconUrl, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
const swapsTokens = getSwapsTokens(state)
|
||||||
|
|
||||||
|
const sourceTokenInfo = swapsTokens?.find(({ address }) => address === fromTokenAddress) || selectedFromToken
|
||||||
|
const destinationTokenInfo = swapsTokens?.find(({ address }) => address === toTokenAddress) || selectedToToken
|
||||||
|
|
||||||
|
dispatch(setFromToken(selectedFromToken))
|
||||||
|
|
||||||
|
const maxMode = getMaxMode(state)
|
||||||
|
|
||||||
|
metaMetricsEvent({
|
||||||
|
event: 'Quotes Requested',
|
||||||
|
category: 'swaps',
|
||||||
|
})
|
||||||
|
metaMetricsEvent({
|
||||||
|
event: 'Quotes Requested',
|
||||||
|
category: 'swaps',
|
||||||
|
excludeMetaMetricsId: true,
|
||||||
|
properties: {
|
||||||
|
token_from: fromTokenSymbol,
|
||||||
|
token_from_amount: String(inputValue),
|
||||||
|
token_to: toTokenSymbol,
|
||||||
|
request_type: balanceError ? 'Quote' : 'Order',
|
||||||
|
slippage: maxSlippage,
|
||||||
|
custom_slippage: maxSlippage !== 2,
|
||||||
|
anonymizedData: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetchStartTime = Date.now()
|
||||||
|
dispatch(setQuotesFetchStartTime(fetchStartTime))
|
||||||
|
|
||||||
|
const fetchAndSetQuotesPromise = dispatch(fetchAndSetQuotes(
|
||||||
|
{
|
||||||
|
slippage: maxSlippage,
|
||||||
|
sourceToken: fromTokenAddress,
|
||||||
|
destinationToken: toTokenAddress,
|
||||||
|
value: inputValue,
|
||||||
|
fromAddress: selectedAccount.address,
|
||||||
|
destinationTokenAddedForSwap,
|
||||||
|
balanceError,
|
||||||
|
sourceDecimals: fromTokenDecimals,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxMode,
|
||||||
|
sourceTokenInfo,
|
||||||
|
destinationTokenInfo,
|
||||||
|
accountBalance: selectedAccount.balance,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
|
const gasPriceFetchPromise = dispatch(fetchAndSetSwapsGasPriceInfo())
|
||||||
|
|
||||||
|
const [[fetchedQuotes, selectedAggId]] = await Promise.all([fetchAndSetQuotesPromise, gasPriceFetchPromise])
|
||||||
|
|
||||||
|
if (Object.values(fetchedQuotes)?.length === 0) {
|
||||||
|
metaMetricsEvent({
|
||||||
|
event: 'No Quotes Available',
|
||||||
|
category: 'swaps',
|
||||||
|
})
|
||||||
|
metaMetricsEvent({
|
||||||
|
event: 'No Quotes Available',
|
||||||
|
category: 'swaps',
|
||||||
|
excludeMetaMetricsId: true,
|
||||||
|
properties: {
|
||||||
|
token_from: fromTokenSymbol,
|
||||||
|
token_from_amount: String(inputValue),
|
||||||
|
token_to: toTokenSymbol,
|
||||||
|
request_type: balanceError ? 'Quote' : 'Order',
|
||||||
|
slippage: maxSlippage,
|
||||||
|
custom_slippage: maxSlippage !== 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
dispatch(setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR))
|
||||||
|
} else {
|
||||||
|
const newSelectedQuote = fetchedQuotes[selectedAggId]
|
||||||
|
|
||||||
|
metaMetricsEvent({
|
||||||
|
event: 'Quotes Received',
|
||||||
|
category: 'swaps',
|
||||||
|
})
|
||||||
|
metaMetricsEvent({
|
||||||
|
event: 'Quotes Received',
|
||||||
|
category: 'swaps',
|
||||||
|
excludeMetaMetricsId: true,
|
||||||
|
properties: {
|
||||||
|
token_from: fromTokenSymbol,
|
||||||
|
token_from_amount: String(inputValue),
|
||||||
|
token_to: toTokenSymbol,
|
||||||
|
token_to_amount: calcTokenAmount(newSelectedQuote.destinationAmount, newSelectedQuote.decimals || 18),
|
||||||
|
request_type: balanceError ? 'Quote' : 'Order',
|
||||||
|
slippage: maxSlippage,
|
||||||
|
custom_slippage: maxSlippage !== 2,
|
||||||
|
response_time: Date.now() - fetchStartTime,
|
||||||
|
best_quote_source: newSelectedQuote.aggregator,
|
||||||
|
available_quotes: Object.values(fetchedQuotes)?.length,
|
||||||
|
anonymizedData: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(setInitialGasEstimate(selectedAggId, newSelectedQuote.maxGas))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(setSwapsErrorKey(ERROR_FETCHING_QUOTES))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setFetchingQuotes(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const signAndSendTransactions = (history, metaMetricsEvent) => {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
let swapsFeatureIsLive = false
|
||||||
|
try {
|
||||||
|
swapsFeatureIsLive = await fetchSwapsFeatureLiveness()
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to fetch Swaps liveness, defaulting to false.', error)
|
||||||
|
}
|
||||||
|
await dispatch(setSwapsLiveness(swapsFeatureIsLive))
|
||||||
|
|
||||||
|
if (!swapsFeatureIsLive) {
|
||||||
|
await history.push(SWAPS_MAINTENANCE_ROUTE)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getState()
|
||||||
|
const customSwapsGas = getCustomSwapsGas(state)
|
||||||
|
const fetchParams = getFetchParams(state)
|
||||||
|
const { metaData, value: swapTokenValue, slippage } = fetchParams
|
||||||
|
const { sourceTokenInfo = {}, destinationTokenInfo = {} } = metaData
|
||||||
|
await dispatch(setBackgroundSwapRouteState('awaiting'))
|
||||||
|
await dispatch(stopPollingForQuotes())
|
||||||
|
history.push(AWAITING_SWAP_ROUTE)
|
||||||
|
|
||||||
|
const usedQuote = getUsedQuote(state)
|
||||||
|
let usedTradeTxParams = usedQuote.trade
|
||||||
|
|
||||||
|
const estimatedGasLimit = new BigNumber(usedQuote?.gasEstimate || decimalToHex(usedQuote?.averageGas || 0), 16)
|
||||||
|
const estimatedGasLimitWithMultiplier = estimatedGasLimit.times(1.4, 10).round(0).toString(16)
|
||||||
|
const maxGasLimit = customSwapsGas || hexMax((`0x${decimalToHex(usedQuote?.maxGas || 0)}`), estimatedGasLimitWithMultiplier)
|
||||||
|
usedTradeTxParams.gas = maxGasLimit
|
||||||
|
|
||||||
|
const customConvertGasPrice = getCustomSwapsGasPrice(state)
|
||||||
|
const tradeTxParams = getSwapsTradeTxParams(state)
|
||||||
|
const fastGasEstimate = getFastPriceEstimateInHexWEI(state)
|
||||||
|
const usedGasPrice = customConvertGasPrice || tradeTxParams?.gasPrice || fastGasEstimate
|
||||||
|
usedTradeTxParams.gasPrice = usedGasPrice
|
||||||
|
|
||||||
|
const totalGasLimitForCalculation = (new BigNumber(usedTradeTxParams.gas, 16)).plus(usedQuote.approvalNeeded?.gas || '0x0', 16).toString(16)
|
||||||
|
const gasTotalInWeiHex = calcGasTotal(totalGasLimitForCalculation, usedGasPrice)
|
||||||
|
|
||||||
|
const maxMode = getMaxMode(state)
|
||||||
|
const selectedAccount = getSelectedAccount(state)
|
||||||
|
if (maxMode && sourceTokenInfo.symbol === 'ETH') {
|
||||||
|
const ethBalance = selectedAccount.balance
|
||||||
|
|
||||||
|
if ((new BigNumber(ethBalance, 16)).lte(gasTotalInWeiHex, 16)) {
|
||||||
|
// If somehow signAndSendTransactions was called when the users eth balance is less than the gas cost of a swap, an error has occured.
|
||||||
|
// The swap transaction should not be created.
|
||||||
|
dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR))
|
||||||
|
history.push(SWAPS_ERROR_ROUTE)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const revisedTradeValueInHexWei = (new BigNumber(ethBalance, 16)).minus(gasTotalInWeiHex, 16).toString(16)
|
||||||
|
const revisedTradeValueInEth = hexWEIToDecETH(revisedTradeValueInHexWei)
|
||||||
|
const revisedQuotes = await fetchTradesInfo({
|
||||||
|
sourceToken: sourceTokenInfo.address,
|
||||||
|
destinationToken: destinationTokenInfo.address,
|
||||||
|
slippage,
|
||||||
|
value: revisedTradeValueInEth,
|
||||||
|
exchangeList: usedQuote.aggregator,
|
||||||
|
fromAddress: selectedAccount.address,
|
||||||
|
timeout: 10000,
|
||||||
|
sourceDecimals: 18,
|
||||||
|
})
|
||||||
|
const revisedQuote = Object.values(revisedQuotes)[0]
|
||||||
|
usedTradeTxParams = constructTxParams({
|
||||||
|
...revisedQuote.trade,
|
||||||
|
gas: decimalToHex(usedTradeTxParams.gas),
|
||||||
|
amount: decimalToHex(revisedQuote.trade.value),
|
||||||
|
gasPrice: tradeTxParams.gasPrice,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversionRate = getConversionRate(state)
|
||||||
|
const destinationValue = calcTokenAmount(usedQuote.destinationAmount, destinationTokenInfo.decimals || 18).toPrecision(8)
|
||||||
|
const usedGasLimitEstimate = usedQuote?.gasEstimateWithRefund || (`0x${decimalToHex(usedQuote?.averageGas || 0)}`)
|
||||||
|
const totalGasLimitEstimate = (new BigNumber(usedGasLimitEstimate, 16)).plus(usedQuote.approvalNeeded?.gas || '0x0', 16).toString(16)
|
||||||
|
const gasEstimateTotalInEth = getValueFromWeiHex({
|
||||||
|
value: calcGasTotal(totalGasLimitEstimate, usedGasPrice),
|
||||||
|
toCurrency: 'usd',
|
||||||
|
conversionRate,
|
||||||
|
numberOfDecimals: 6,
|
||||||
|
})
|
||||||
|
const swapMetaData = {
|
||||||
|
token_from: sourceTokenInfo.symbol,
|
||||||
|
token_from_amount: String(swapTokenValue),
|
||||||
|
token_to: destinationTokenInfo.symbol,
|
||||||
|
token_to_amount: destinationValue,
|
||||||
|
slippage,
|
||||||
|
custom_slippage: slippage !== 2,
|
||||||
|
best_quote_source: getTopQuote(state)?.aggregator,
|
||||||
|
available_quotes: getQuotes(state)?.length,
|
||||||
|
other_quote_selected: usedQuote.aggregator !== getTopQuote(state)?.aggregator,
|
||||||
|
other_quote_selected_source: usedQuote.aggregator === getTopQuote(state)?.aggregator ? '' : usedQuote.aggregator,
|
||||||
|
gas_fees: formatCurrency(gasEstimateTotalInEth, 'usd')?.slice(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaMetricsConfig = {
|
||||||
|
event: 'Swap Started',
|
||||||
|
category: 'swaps',
|
||||||
|
}
|
||||||
|
|
||||||
|
metaMetricsEvent({ ...metaMetricsConfig })
|
||||||
|
metaMetricsEvent({ ...metaMetricsConfig, excludeMetaMetricsId: true, properties: swapMetaData })
|
||||||
|
|
||||||
|
const approveTxParams = getApproveTxParams(state)
|
||||||
|
if (approveTxParams) {
|
||||||
|
const approveTxMeta = await dispatch(addUnapprovedTransaction({ ...approveTxParams, amount: '0x0' }, 'metamask'))
|
||||||
|
await dispatch(setApproveTxId(approveTxMeta.id))
|
||||||
|
const finalApproveTxMeta = await (dispatch(updateTransaction({
|
||||||
|
...approveTxMeta,
|
||||||
|
transactionCategory: SWAP_APPROVAL,
|
||||||
|
sourceTokenSymbol: sourceTokenInfo.symbol,
|
||||||
|
}, true)))
|
||||||
|
try {
|
||||||
|
await dispatch(updateAndApproveTx(finalApproveTxMeta, true))
|
||||||
|
} catch (e) {
|
||||||
|
await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR))
|
||||||
|
history.push(SWAPS_ERROR_ROUTE)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tradeTxMeta = await dispatch(addUnapprovedTransaction(usedTradeTxParams, 'metamask'))
|
||||||
|
dispatch(setTradeTxId(tradeTxMeta.id))
|
||||||
|
const finalTradeTxMeta = await (dispatch(updateTransaction({
|
||||||
|
...tradeTxMeta,
|
||||||
|
sourceTokenSymbol: sourceTokenInfo.symbol,
|
||||||
|
destinationTokenSymbol: destinationTokenInfo.symbol,
|
||||||
|
transactionCategory: SWAP,
|
||||||
|
destinationTokenDecimals: destinationTokenInfo.decimals,
|
||||||
|
destinationTokenAddress: destinationTokenInfo.address,
|
||||||
|
swapMetaData,
|
||||||
|
swapTokenValue,
|
||||||
|
}, true)))
|
||||||
|
try {
|
||||||
|
await dispatch(updateAndApproveTx(finalTradeTxMeta, true))
|
||||||
|
} catch (e) {
|
||||||
|
await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR))
|
||||||
|
history.push(SWAPS_ERROR_ROUTE)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await forceUpdateMetamaskState(dispatch)
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,13 @@ const CONNECT_ROUTE = '/connect'
|
|||||||
const CONNECT_CONFIRM_PERMISSIONS_ROUTE = '/confirm-permissions'
|
const CONNECT_CONFIRM_PERMISSIONS_ROUTE = '/confirm-permissions'
|
||||||
const CONNECTED_ROUTE = '/connected'
|
const CONNECTED_ROUTE = '/connected'
|
||||||
const CONNECTED_ACCOUNTS_ROUTE = '/connected/accounts'
|
const CONNECTED_ACCOUNTS_ROUTE = '/connected/accounts'
|
||||||
|
const SWAPS_ROUTE = '/swaps'
|
||||||
|
const BUILD_QUOTE_ROUTE = '/swaps/build-quote'
|
||||||
|
const VIEW_QUOTE_ROUTE = '/swaps/view-quote'
|
||||||
|
const LOADING_QUOTES_ROUTE = '/swaps/loading-quotes'
|
||||||
|
const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap'
|
||||||
|
const SWAPS_ERROR_ROUTE = '/swaps/swaps-error'
|
||||||
|
const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance'
|
||||||
|
|
||||||
const INITIALIZE_ROUTE = '/initialize'
|
const INITIALIZE_ROUTE = '/initialize'
|
||||||
const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome'
|
const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome'
|
||||||
@ -109,6 +116,11 @@ const PATH_NAME_MAP = {
|
|||||||
[INITIALIZE_END_OF_FLOW_ROUTE]: 'End of Initialization Page',
|
[INITIALIZE_END_OF_FLOW_ROUTE]: 'End of Initialization Page',
|
||||||
[INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE]: 'Initialization Confirm Seed Phrase Page',
|
[INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE]: 'Initialization Confirm Seed Phrase Page',
|
||||||
[INITIALIZE_METAMETRICS_OPT_IN_ROUTE]: 'MetaMetrics Opt In Page',
|
[INITIALIZE_METAMETRICS_OPT_IN_ROUTE]: 'MetaMetrics Opt In Page',
|
||||||
|
[BUILD_QUOTE_ROUTE]: 'Swaps Build Quote Page',
|
||||||
|
[VIEW_QUOTE_ROUTE]: 'Swaps View Quotes Page',
|
||||||
|
[LOADING_QUOTES_ROUTE]: 'Swaps Loading Quotes Page',
|
||||||
|
[AWAITING_SWAP_ROUTE]: 'Swaps Awaiting Swaps Page',
|
||||||
|
[SWAPS_ERROR_ROUTE]: 'Swaps Error Page',
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -166,4 +178,11 @@ export {
|
|||||||
CONNECTED_ROUTE,
|
CONNECTED_ROUTE,
|
||||||
CONNECTED_ACCOUNTS_ROUTE,
|
CONNECTED_ACCOUNTS_ROUTE,
|
||||||
PATH_NAME_MAP,
|
PATH_NAME_MAP,
|
||||||
|
SWAPS_ROUTE,
|
||||||
|
BUILD_QUOTE_ROUTE,
|
||||||
|
VIEW_QUOTE_ROUTE,
|
||||||
|
LOADING_QUOTES_ROUTE,
|
||||||
|
AWAITING_SWAP_ROUTE,
|
||||||
|
SWAPS_ERROR_ROUTE,
|
||||||
|
SWAPS_MAINTENANCE_ROUTE,
|
||||||
}
|
}
|
||||||
|
21
ui/app/helpers/constants/swaps.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// An address that the metaswap-api recognizes as ETH, in place of the token address that ERC-20 tokens have
|
||||||
|
export const ETH_SWAPS_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||||
|
|
||||||
|
export const ETH_SWAPS_TOKEN_OBJECT = {
|
||||||
|
symbol: 'ETH',
|
||||||
|
name: 'Ether',
|
||||||
|
address: ETH_SWAPS_TOKEN_ADDRESS,
|
||||||
|
decimals: 18,
|
||||||
|
iconUrl: 'images/black-eth-logo.svg',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QUOTES_EXPIRED_ERROR = 'quotes-expired'
|
||||||
|
export const SWAP_FAILED_ERROR = 'swap-failed-error'
|
||||||
|
export const ERROR_FETCHING_QUOTES = 'error-fetching-quotes'
|
||||||
|
export const QUOTES_NOT_AVAILABLE_ERROR = 'quotes-not-avilable'
|
||||||
|
export const OFFLINE_FOR_MAINTENANCE = 'offline-for-maintenance'
|
||||||
|
|
||||||
|
// A gas value for ERC20 approve calls that should be sufficient for all ERC20 approve implementations
|
||||||
|
export const DEFAULT_ERC20_APPROVE_GAS = '0x1d4c0'
|
||||||
|
|
||||||
|
export const SWAPS_CONTRACT_ADDRESS = '0x016b4bf68d421147c06f1b8680602c5bf0df91a8'
|
@ -29,6 +29,9 @@ export const TOKEN_CATEGORY_HASH = {
|
|||||||
[TOKEN_METHOD_TRANSFER_FROM]: true,
|
[TOKEN_METHOD_TRANSFER_FROM]: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SWAP = 'swap'
|
||||||
|
export const SWAP_APPROVAL = 'swapApproval'
|
||||||
|
|
||||||
export const INCOMING_TRANSACTION = 'incoming'
|
export const INCOMING_TRANSACTION = 'incoming'
|
||||||
|
|
||||||
export const SEND_ETHER_ACTION_KEY = 'sentEther'
|
export const SEND_ETHER_ACTION_KEY = 'sentEther'
|
||||||
@ -50,3 +53,4 @@ export const TRANSACTION_CATEGORY_RECEIVE = 'receive'
|
|||||||
export const TRANSACTION_CATEGORY_INTERACTION = 'interaction'
|
export const TRANSACTION_CATEGORY_INTERACTION = 'interaction'
|
||||||
export const TRANSACTION_CATEGORY_APPROVAL = 'approval'
|
export const TRANSACTION_CATEGORY_APPROVAL = 'approval'
|
||||||
export const TRANSACTION_CATEGORY_SIGNATURE_REQUEST = 'signature-request'
|
export const TRANSACTION_CATEGORY_SIGNATURE_REQUEST = 'signature-request'
|
||||||
|
export const TRANSACTION_CATEGORY_SWAP = 'swap'
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Redirect, Route } from 'react-router-dom'
|
||||||
|
|
||||||
|
export default function FeatureToggledRoute ({ flag, redirectRoute, ...props }) {
|
||||||
|
|
||||||
|
if (flag) {
|
||||||
|
return <Route {...props} />
|
||||||
|
}
|
||||||
|
return <Redirect to={{ pathname: redirectRoute }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
FeatureToggledRoute.propTypes = {
|
||||||
|
flag: PropTypes.bool.isRequired,
|
||||||
|
redirectRoute: PropTypes.string.isRequired,
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
import ethUtil from 'ethereumjs-util'
|
import ethUtil from 'ethereumjs-util'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
import { ETH, GWEI, WEI } from '../constants/common'
|
import { ETH, GWEI, WEI } from '../constants/common'
|
||||||
import { conversionUtil, addCurrencies, subtractCurrencies } from './conversion-util'
|
import { conversionUtil, addCurrencies, subtractCurrencies } from './conversion-util'
|
||||||
|
import {
|
||||||
|
formatCurrency,
|
||||||
|
} from './confirm-tx.util'
|
||||||
|
|
||||||
export function bnToHex (inputBn) {
|
export function bnToHex (inputBn) {
|
||||||
return ethUtil.addHexPrefix(inputBn.toString(16))
|
return ethUtil.addHexPrefix(inputBn.toString(16))
|
||||||
@ -138,3 +142,43 @@ export function decETHToDecWEI (decEth) {
|
|||||||
toDenomination: 'WEI',
|
toDenomination: 'WEI',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hexWEIToDecETH (hexWEI) {
|
||||||
|
return conversionUtil(hexWEI, {
|
||||||
|
fromNumericBase: 'hex',
|
||||||
|
toNumericBase: 'dec',
|
||||||
|
fromDenomination: 'WEI',
|
||||||
|
toDenomination: 'ETH',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hexMax (...hexNumbers) {
|
||||||
|
let max = hexNumbers[0]
|
||||||
|
hexNumbers.slice(1).forEach((hexNumber) => {
|
||||||
|
if ((new BigNumber(hexNumber, 16)).gt(max, 16)) {
|
||||||
|
max = hexNumber
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addHexes (aHexWEI, bHexWEI) {
|
||||||
|
return addCurrencies(aHexWEI, bHexWEI, {
|
||||||
|
aBase: 16,
|
||||||
|
bBase: 16,
|
||||||
|
toNumericBase: 'hex',
|
||||||
|
numberOfDecimals: 6,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sumHexWEIsToRenderableFiat (hexWEIs, convertedCurrency, conversionRate) {
|
||||||
|
const hexWEIsSum = hexWEIs.filter((n) => n).reduce(addHexes)
|
||||||
|
const ethTotal = decEthToConvertedCurrency(
|
||||||
|
getValueFromWeiHex({
|
||||||
|
value: hexWEIsSum, toCurrency: 'ETH', numberOfDecimals: 4,
|
||||||
|
}),
|
||||||
|
convertedCurrency,
|
||||||
|
conversionRate,
|
||||||
|
)
|
||||||
|
return formatCurrency(ethTotal, convertedCurrency)
|
||||||
|
}
|
||||||
|
@ -163,22 +163,31 @@ export function getTokenValueParam (tokenData = {}) {
|
|||||||
return tokenData?.args?.['_value']?.toString()
|
return tokenData?.args?.['_value']?.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTokenValue (tokenParams = []) {
|
||||||
|
const valueData = tokenParams.find((param) => param.name === '_value')
|
||||||
|
return valueData && valueData.value
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the token balance converted to fiat and formatted for display
|
* Get the token balance converted to fiat and optionally formatted for display
|
||||||
*
|
*
|
||||||
* @param {number} [contractExchangeRate] - The exchange rate between the current token and the native currency
|
* @param {number} [contractExchangeRate] - The exchange rate between the current token and the native currency
|
||||||
* @param {number} conversionRate - The exchange rate between the current fiat currency and the native currency
|
* @param {number} conversionRate - The exchange rate between the current fiat currency and the native currency
|
||||||
* @param {string} currentCurrency - The currency code for the user's chosen fiat currency
|
* @param {string} currentCurrency - The currency code for the user's chosen fiat currency
|
||||||
* @param {string} [tokenAmount] - The current token balance
|
* @param {string} [tokenAmount] - The current token balance
|
||||||
* @param {string} [tokenSymbol] - The token symbol
|
* @param {string} [tokenSymbol] - The token symbol
|
||||||
* @returns {string|undefined} The formatted token amount in the user's chosen fiat currency
|
* @param {boolean} [formatted] - Whether the return value should be formatted or not
|
||||||
|
* @param {boolean} [hideCurrencySymbol] - excludes the currency symbol in the result if true
|
||||||
|
* @returns {string|undefined} The token amount in the user's chosen fiat currency, optionally formatted and localize
|
||||||
*/
|
*/
|
||||||
export function getFormattedTokenFiatAmount (
|
export function getTokenFiatAmount (
|
||||||
contractExchangeRate,
|
contractExchangeRate,
|
||||||
conversionRate,
|
conversionRate,
|
||||||
currentCurrency,
|
currentCurrency,
|
||||||
tokenAmount,
|
tokenAmount,
|
||||||
tokenSymbol,
|
tokenSymbol,
|
||||||
|
formatted = true,
|
||||||
|
hideCurrencySymbol = false,
|
||||||
) {
|
) {
|
||||||
// If the conversionRate is 0 (i.e. unknown) or the contract exchange rate
|
// If the conversionRate is 0 (i.e. unknown) or the contract exchange rate
|
||||||
// is currently unknown, the fiat amount cannot be calculated so it is not
|
// is currently unknown, the fiat amount cannot be calculated so it is not
|
||||||
@ -198,5 +207,13 @@ export function getFormattedTokenFiatAmount (
|
|||||||
numberOfDecimals: 2,
|
numberOfDecimals: 2,
|
||||||
conversionRate: currentTokenToFiatRate,
|
conversionRate: currentTokenToFiatRate,
|
||||||
})
|
})
|
||||||
return `${formatCurrency(currentTokenInFiat, currentCurrency)} ${currentCurrency.toUpperCase()}`
|
let result
|
||||||
|
if (hideCurrencySymbol) {
|
||||||
|
result = formatCurrency(currentTokenInFiat, currentCurrency)
|
||||||
|
} else if (formatted) {
|
||||||
|
result = `${formatCurrency(currentTokenInFiat, currentCurrency)} ${currentCurrency.toUpperCase()}`
|
||||||
|
} else {
|
||||||
|
result = currentTokenInFiat
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
@ -369,3 +369,41 @@ export function toPrecisionWithoutTrailingZeros (n, precision) {
|
|||||||
.toPrecision(precision)
|
.toPrecision(precision)
|
||||||
.replace(/(\.[0-9]*[1-9])0*|(\.0*)/u, '$1')
|
.replace(/(\.[0-9]*[1-9])0*|(\.0*)/u, '$1')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given and object where all values are strings, returns the same object with all values
|
||||||
|
* now prefixed with '0x'
|
||||||
|
*/
|
||||||
|
export function addHexPrefixToObjectValues (obj) {
|
||||||
|
return Object.keys(obj).reduce((newObj, key) => {
|
||||||
|
return { ...newObj, [key]: ethUtil.addHexPrefix(obj[key]) }
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the standard set of information about a transaction, returns a transaction properly formatted for
|
||||||
|
* publishing via JSON RPC and web3
|
||||||
|
*
|
||||||
|
* @param {string|object|boolean} sendToken - Indicates whether or not the transaciton is a token transaction
|
||||||
|
* @param {string} data - A hex string containing the data to include in the transaction
|
||||||
|
* @param {string} to - A hex address of the tx recipient address
|
||||||
|
* @param {string} from - A hex address of the tx sender address
|
||||||
|
* @param {string} gas - A hex representation of the gas value for the transaction
|
||||||
|
* @param {string} gasPrice - A hex representation of the gas price for the transaction
|
||||||
|
* @returns {object} An object ready for submission to the blockchain, with all values appropriately hex prefixed
|
||||||
|
*/
|
||||||
|
export function constructTxParams ({ sendToken, data, to, amount, from, gas, gasPrice }) {
|
||||||
|
const txParams = {
|
||||||
|
data,
|
||||||
|
from,
|
||||||
|
value: '0',
|
||||||
|
gas,
|
||||||
|
gasPrice,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sendToken) {
|
||||||
|
txParams.value = amount
|
||||||
|
txParams.to = to
|
||||||
|
}
|
||||||
|
return addHexPrefixToObjectValues(txParams)
|
||||||
|
}
|
||||||
|
@ -392,4 +392,21 @@ describe('util', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('addHexPrefixToObjectValues()', function () {
|
||||||
|
it('should return a new object with the same properties with a 0x prefix', function () {
|
||||||
|
assert.deepEqual(
|
||||||
|
util.addHexPrefixToObjectValues({
|
||||||
|
prop1: '0x123',
|
||||||
|
prop2: '456',
|
||||||
|
prop3: 'x',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
prop1: '0x123',
|
||||||
|
prop2: '0x456',
|
||||||
|
prop3: '0xx',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
|
import React from 'react'
|
||||||
import * as reactRedux from 'react-redux'
|
import * as reactRedux from 'react-redux'
|
||||||
import { renderHook } from '@testing-library/react-hooks'
|
import { renderHook } from '@testing-library/react-hooks'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
import transactions from '../../../../test/data/transaction-data.json'
|
import transactions from '../../../../test/data/transaction-data.json'
|
||||||
import { useTransactionDisplayData } from '../useTransactionDisplayData'
|
import { useTransactionDisplayData } from '../useTransactionDisplayData'
|
||||||
import * as useTokenFiatAmountHooks from '../useTokenFiatAmount'
|
import * as useTokenFiatAmountHooks from '../useTokenFiatAmount'
|
||||||
@ -10,6 +12,7 @@ import { getTokens } from '../../ducks/metamask/metamask'
|
|||||||
import * as i18nhooks from '../useI18nContext'
|
import * as i18nhooks from '../useI18nContext'
|
||||||
import { getMessage } from '../../helpers/utils/i18n-helper'
|
import { getMessage } from '../../helpers/utils/i18n-helper'
|
||||||
import messages from '../../../../app/_locales/en/messages.json'
|
import messages from '../../../../app/_locales/en/messages.json'
|
||||||
|
import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../helpers/constants/routes'
|
||||||
|
|
||||||
const expectedResults = [
|
const expectedResults = [
|
||||||
{
|
{
|
||||||
@ -90,10 +93,30 @@ const expectedResults = [
|
|||||||
isPending: false,
|
isPending: false,
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Swap ETH to ABC',
|
||||||
|
category: 'swap',
|
||||||
|
subtitle: '',
|
||||||
|
subtitleContainsOrigin: false,
|
||||||
|
date: 'May 12',
|
||||||
|
primaryCurrency: '+1 ABC',
|
||||||
|
senderAddress: '0xee014609ef9e09776ac5fe00bdbfef57bcdefebb',
|
||||||
|
recipientAddress: '0xabca64466f257793eaa52fcfff5066894b76a149',
|
||||||
|
secondaryCurrency: undefined,
|
||||||
|
isPending: false,
|
||||||
|
status: 'confirmed',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let useSelector, useI18nContext, useTokenFiatAmount
|
let useSelector, useI18nContext, useTokenFiatAmount
|
||||||
|
|
||||||
|
const renderHookWithRouter = (cb, tokenAddress) => {
|
||||||
|
const initialEntries = [tokenAddress ? `${ASSET_ROUTE}/${tokenAddress}` : DEFAULT_ROUTE]
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const wrapper = ({ children }) => <MemoryRouter initialEntries={initialEntries}>{children}</MemoryRouter>
|
||||||
|
return renderHook(cb, { wrapper })
|
||||||
|
}
|
||||||
|
|
||||||
describe('useTransactionDisplayData', function () {
|
describe('useTransactionDisplayData', function () {
|
||||||
before(function () {
|
before(function () {
|
||||||
useSelector = sinon.stub(reactRedux, 'useSelector')
|
useSelector = sinon.stub(reactRedux, 'useSelector')
|
||||||
@ -105,7 +128,7 @@ describe('useTransactionDisplayData', function () {
|
|||||||
useI18nContext.returns((key, variables) => getMessage('en', messages, key, variables))
|
useI18nContext.returns((key, variables) => getMessage('en', messages, key, variables))
|
||||||
useSelector.callsFake((selector) => {
|
useSelector.callsFake((selector) => {
|
||||||
if (selector === getTokens) {
|
if (selector === getTokens) {
|
||||||
return []
|
return [{ address: '0xabca64466f257793eaa52fcfff5066894b76a149', symbol: 'ABC', decimals: 18 }]
|
||||||
} else if (selector === getPreferences) {
|
} else if (selector === getPreferences) {
|
||||||
return {
|
return {
|
||||||
useNativeCurrencyAsPrimaryCurrency: true,
|
useNativeCurrencyAsPrimaryCurrency: true,
|
||||||
@ -123,42 +146,43 @@ describe('useTransactionDisplayData', function () {
|
|||||||
transactions.forEach((transactionGroup, idx) => {
|
transactions.forEach((transactionGroup, idx) => {
|
||||||
describe(`when called with group containing primaryTransaction id ${transactionGroup.primaryTransaction.id}`, function () {
|
describe(`when called with group containing primaryTransaction id ${transactionGroup.primaryTransaction.id}`, function () {
|
||||||
const expected = expectedResults[idx]
|
const expected = expectedResults[idx]
|
||||||
|
const tokenAddress = transactionGroup.primaryTransaction?.destinationTokenAddress
|
||||||
it(`should return a title of ${expected.title}`, function () {
|
it(`should return a title of ${expected.title}`, function () {
|
||||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup))
|
const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactionGroup), tokenAddress)
|
||||||
assert.equal(result.current.title, expected.title)
|
assert.equal(result.current.title, expected.title)
|
||||||
})
|
})
|
||||||
it(`should return a subtitle of ${expected.subtitle}`, function () {
|
it(`should return a subtitle of ${expected.subtitle}`, function () {
|
||||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup))
|
const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactionGroup), tokenAddress)
|
||||||
assert.equal(result.current.subtitle, expected.subtitle)
|
assert.equal(result.current.subtitle, expected.subtitle)
|
||||||
})
|
})
|
||||||
it(`should return a category of ${expected.category}`, function () {
|
it(`should return a category of ${expected.category}`, function () {
|
||||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup))
|
const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactionGroup), tokenAddress)
|
||||||
assert.equal(result.current.category, expected.category)
|
assert.equal(result.current.category, expected.category)
|
||||||
})
|
})
|
||||||
it(`should return a primaryCurrency of ${expected.primaryCurrency}`, function () {
|
it(`should return a primaryCurrency of ${expected.primaryCurrency}`, function () {
|
||||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup))
|
const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactionGroup), tokenAddress)
|
||||||
assert.equal(result.current.primaryCurrency, expected.primaryCurrency)
|
assert.equal(result.current.primaryCurrency, expected.primaryCurrency)
|
||||||
})
|
})
|
||||||
it(`should return a secondaryCurrency of ${expected.secondaryCurrency}`, function () {
|
it(`should return a secondaryCurrency of ${expected.secondaryCurrency}`, function () {
|
||||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup))
|
const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactionGroup), tokenAddress)
|
||||||
assert.equal(result.current.secondaryCurrency, expected.secondaryCurrency)
|
assert.equal(result.current.secondaryCurrency, expected.secondaryCurrency)
|
||||||
})
|
})
|
||||||
it(`should return a status of ${expected.status}`, function () {
|
it(`should return a status of ${expected.status}`, function () {
|
||||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup))
|
const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactionGroup), tokenAddress)
|
||||||
assert.equal(result.current.status, expected.status)
|
assert.equal(result.current.status, expected.status)
|
||||||
})
|
})
|
||||||
it(`should return a recipientAddress of ${expected.recipientAddress}`, function () {
|
it(`should return a recipientAddress of ${expected.recipientAddress}`, function () {
|
||||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup))
|
const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactionGroup), tokenAddress)
|
||||||
assert.equal(result.current.recipientAddress, expected.recipientAddress)
|
assert.equal(result.current.recipientAddress, expected.recipientAddress)
|
||||||
})
|
})
|
||||||
it(`should return a senderAddress of ${expected.senderAddress}`, function () {
|
it(`should return a senderAddress of ${expected.senderAddress}`, function () {
|
||||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup))
|
const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactionGroup), tokenAddress)
|
||||||
assert.equal(result.current.senderAddress, expected.senderAddress)
|
assert.equal(result.current.senderAddress, expected.senderAddress)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('should return an appropriate object', function () {
|
it('should return an appropriate object', function () {
|
||||||
const { result } = renderHook(() => useTransactionDisplayData(transactions[0]))
|
const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactions[0]))
|
||||||
assert.deepEqual(result.current, expectedResults[0])
|
assert.deepEqual(result.current, expectedResults[0])
|
||||||
})
|
})
|
||||||
after(function () {
|
after(function () {
|
||||||
|
24
ui/app/hooks/useCurrentAsset.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { useRouteMatch } from 'react-router-dom'
|
||||||
|
import { getTokens } from '../ducks/metamask/metamask'
|
||||||
|
import { ASSET_ROUTE } from '../helpers/constants/routes'
|
||||||
|
import { ETH_SWAPS_TOKEN_OBJECT } from '../helpers/constants/swaps'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a token object for the asset that is currently being viewed.
|
||||||
|
* Will return the ETH_SWAPS_TOKEN_OBJECT when the user is viewing either
|
||||||
|
* the primary, unfiltered, activity list or the ETH asset page.
|
||||||
|
* @returns {import('./useTokenDisplayValue').Token}
|
||||||
|
*/
|
||||||
|
export function useCurrentAsset () {
|
||||||
|
// To determine which primary currency to display for swaps transactions we need to be aware
|
||||||
|
// of which asset, if any, we are viewing at present
|
||||||
|
const match = useRouteMatch({ path: `${ASSET_ROUTE}/:asset`, exact: true, strict: true })
|
||||||
|
const tokenAddress = match?.params?.asset
|
||||||
|
const knownTokens = useSelector(getTokens)
|
||||||
|
const token = tokenAddress && knownTokens.find(
|
||||||
|
({ address }) => address === tokenAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
return token ?? ETH_SWAPS_TOKEN_OBJECT
|
||||||
|
}
|
34
ui/app/hooks/useEthFiatAmount.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { getConversionRate, getCurrentCurrency, getShouldShowFiat } from '../selectors'
|
||||||
|
import { decEthToConvertedCurrency } from '../helpers/utils/conversions.util'
|
||||||
|
import { formatCurrency } from '../helpers/utils/confirm-tx.util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an Eth amount converted to fiat and formatted for display
|
||||||
|
*
|
||||||
|
* @param {string} [tokenAmount] - The eth amount to convert
|
||||||
|
* @param {object} [overrides] - A configuration object that allows the called to explicitly
|
||||||
|
* ensure fiat is shown even if the property is not set in state.
|
||||||
|
* @param {boolean} [overrides.showFiat] - If truthy, ensures the fiat value is shown even if the showFiat value from state is falsey
|
||||||
|
* @param {boolean} hideCurrencySymbol Indicates whether the returned formatted amount should include the trailing currency symbol
|
||||||
|
* @return {string} - The formatted token amount in the user's chosen fiat currency
|
||||||
|
*/
|
||||||
|
export function useEthFiatAmount (ethAmount, overrides = {}, hideCurrencySymbol) {
|
||||||
|
const conversionRate = useSelector(getConversionRate)
|
||||||
|
const currentCurrency = useSelector(getCurrentCurrency)
|
||||||
|
const userPrefersShownFiat = useSelector(getShouldShowFiat)
|
||||||
|
const showFiat = overrides.showFiat ?? userPrefersShownFiat
|
||||||
|
const formattedFiat = useMemo(
|
||||||
|
() => decEthToConvertedCurrency(ethAmount, currentCurrency, conversionRate),
|
||||||
|
[conversionRate, currentCurrency, ethAmount],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!showFiat || currentCurrency.toUpperCase() === 'ETH' || conversionRate <= 0 || ethAmount === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return hideCurrencySymbol
|
||||||
|
? formatCurrency(formattedFiat, currentCurrency)
|
||||||
|
: `${formatCurrency(formattedFiat, currentCurrency)} ${currentCurrency.toUpperCase()}`
|
||||||
|
}
|
9
ui/app/hooks/usePrevious.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export function usePrevious (value) {
|
||||||
|
const ref = useRef()
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value
|
||||||
|
}, [value])
|
||||||
|
return ref.current
|
||||||
|
}
|
56
ui/app/hooks/useSwappedTokenValue.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { ETH_SWAPS_TOKEN_ADDRESS } from '../helpers/constants/swaps'
|
||||||
|
import { SWAP } from '../helpers/constants/transactions'
|
||||||
|
import { getSwapsTokensReceivedFromTxMeta } from '../pages/swaps/swaps.util'
|
||||||
|
import { useTokenFiatAmount } from './useTokenFiatAmount'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} SwappedTokenValue
|
||||||
|
* @property {string} swapTokenValue - a primary currency string formatted for display
|
||||||
|
* @property {string} swapTokenFiatAmount - a secondary currency string formatted for display
|
||||||
|
* @property {boolean} isViewingReceivedTokenFromSwap - true if user is on the asset page for the
|
||||||
|
* destination/received asset in a swap.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SWAP transaction group's primaryTransaction contains details of the swap,
|
||||||
|
* including the source (from) and destination (to) token type (ETH, DAI, etc..)
|
||||||
|
* When viewing a non ETH asset page, we need to determine if that asset is the
|
||||||
|
* token that was received (destination) from the swap. In that circumstance we
|
||||||
|
* would want to show the primaryCurrency in the activity list that is most relevant
|
||||||
|
* for that token (- 1000 DAI, for example, when swapping DAI for ETH).
|
||||||
|
* @param {import('../selectors').transactionGroup} transactionGroup - Group of transactions by nonce
|
||||||
|
* @param {import('./useTokenDisplayValue').Token} currentAsset - The current asset the user is looking at
|
||||||
|
* @returns {SwappedTokenValue}
|
||||||
|
*/
|
||||||
|
export function useSwappedTokenValue (transactionGroup, currentAsset) {
|
||||||
|
const { symbol, decimals, address } = currentAsset
|
||||||
|
const { primaryTransaction, initialTransaction } = transactionGroup
|
||||||
|
const { transactionCategory } = initialTransaction
|
||||||
|
const { from: senderAddress } = initialTransaction.txParams || {}
|
||||||
|
|
||||||
|
const isViewingReceivedTokenFromSwap = (
|
||||||
|
(currentAsset?.symbol === primaryTransaction.destinationTokenSymbol) || (
|
||||||
|
currentAsset.address === ETH_SWAPS_TOKEN_ADDRESS &&
|
||||||
|
primaryTransaction.destinationTokenSymbol === 'ETH'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const swapTokenValue = transactionCategory === SWAP && isViewingReceivedTokenFromSwap
|
||||||
|
? getSwapsTokensReceivedFromTxMeta(
|
||||||
|
primaryTransaction.destinationTokenSymbol,
|
||||||
|
initialTransaction,
|
||||||
|
address,
|
||||||
|
senderAddress,
|
||||||
|
decimals,
|
||||||
|
)
|
||||||
|
: transactionCategory === SWAP && primaryTransaction.swapTokenValue
|
||||||
|
const _swapTokenFiatAmount = useTokenFiatAmount(
|
||||||
|
address,
|
||||||
|
swapTokenValue || '',
|
||||||
|
symbol,
|
||||||
|
)
|
||||||
|
const swapTokenFiatAmount = (
|
||||||
|
swapTokenValue && isViewingReceivedTokenFromSwap && _swapTokenFiatAmount
|
||||||
|
)
|
||||||
|
return { swapTokenValue, swapTokenFiatAmount, isViewingReceivedTokenFromSwap }
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { getTokenExchangeRates, getConversionRate, getCurrentCurrency, getShouldShowFiat } from '../selectors'
|
import { getTokenExchangeRates, getConversionRate, getCurrentCurrency, getShouldShowFiat } from '../selectors'
|
||||||
import { getFormattedTokenFiatAmount } from '../helpers/utils/token-util'
|
import { getTokenFiatAmount } from '../helpers/utils/token-util'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the token balance converted to fiat and formatted for display
|
* Get the token balance converted to fiat and formatted for display
|
||||||
@ -9,25 +9,31 @@ import { getFormattedTokenFiatAmount } from '../helpers/utils/token-util'
|
|||||||
* @param {string} [tokenAddress] - The token address
|
* @param {string} [tokenAddress] - The token address
|
||||||
* @param {string} [tokenAmount] - The token balance
|
* @param {string} [tokenAmount] - The token balance
|
||||||
* @param {string} [tokenSymbol] - The token symbol
|
* @param {string} [tokenSymbol] - The token symbol
|
||||||
|
* @param {object} [overrides] - A configuration object that allows the caller to explicitly pass an exchange rate or
|
||||||
|
* ensure fiat is shown even if the property is not set in state.
|
||||||
|
* @param {number} [overrides.exchangeRate] - An exhchange rate to use instead of the one selected from state
|
||||||
|
* @param {boolean} [overrides.showFiat] - If truthy, ensures the fiat value is shown even if the showFiat value from state is falsey
|
||||||
|
* @param {boolean} hideCurrencySymbol Indicates whether the returned formatted amount should include the trailing currency symbol
|
||||||
* @return {string} - The formatted token amount in the user's chosen fiat currency
|
* @return {string} - The formatted token amount in the user's chosen fiat currency
|
||||||
*/
|
*/
|
||||||
export function useTokenFiatAmount (tokenAddress, tokenAmount, tokenSymbol) {
|
export function useTokenFiatAmount (tokenAddress, tokenAmount, tokenSymbol, overrides = {}, hideCurrencySymbol) {
|
||||||
const contractExchangeRates = useSelector(getTokenExchangeRates)
|
const contractExchangeRates = useSelector(getTokenExchangeRates)
|
||||||
const conversionRate = useSelector(getConversionRate)
|
const conversionRate = useSelector(getConversionRate)
|
||||||
const currentCurrency = useSelector(getCurrentCurrency)
|
const currentCurrency = useSelector(getCurrentCurrency)
|
||||||
const showFiat = useSelector(getShouldShowFiat)
|
const userPrefersShownFiat = useSelector(getShouldShowFiat)
|
||||||
|
const showFiat = overrides.showFiat ?? userPrefersShownFiat
|
||||||
const tokenExchangeRate = contractExchangeRates[tokenAddress]
|
const tokenExchangeRate = overrides.exchangeRate ?? contractExchangeRates[tokenAddress]
|
||||||
|
|
||||||
const formattedFiat = useMemo(
|
const formattedFiat = useMemo(
|
||||||
() => getFormattedTokenFiatAmount(
|
() => getTokenFiatAmount(
|
||||||
tokenExchangeRate,
|
tokenExchangeRate,
|
||||||
conversionRate,
|
conversionRate,
|
||||||
currentCurrency,
|
currentCurrency,
|
||||||
tokenAmount,
|
tokenAmount,
|
||||||
tokenSymbol,
|
tokenSymbol,
|
||||||
|
true,
|
||||||
|
hideCurrencySymbol,
|
||||||
),
|
),
|
||||||
[tokenExchangeRate, conversionRate, currentCurrency, tokenAmount, tokenSymbol],
|
[tokenExchangeRate, conversionRate, currentCurrency, tokenAmount, tokenSymbol, hideCurrencySymbol],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!showFiat || currentCurrency.toUpperCase() === tokenSymbol) {
|
if (!showFiat || currentCurrency.toUpperCase() === tokenSymbol) {
|
||||||
|
115
ui/app/hooks/useTokensToSearch.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import contractMap from 'eth-contract-metadata'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import { isEqual, shuffle } from 'lodash'
|
||||||
|
import { getValueFromWeiHex } from '../helpers/utils/conversions.util'
|
||||||
|
import { checksumAddress } from '../helpers/utils/util'
|
||||||
|
import { getTokenFiatAmount } from '../helpers/utils/token-util'
|
||||||
|
import { getTokenExchangeRates, getConversionRate, getCurrentCurrency } from '../selectors'
|
||||||
|
import { getSwapsTokens } from '../ducks/swaps/swaps'
|
||||||
|
import { ETH_SWAPS_TOKEN_OBJECT } from '../helpers/constants/swaps'
|
||||||
|
import { useEqualityCheck } from './useEqualityCheck'
|
||||||
|
|
||||||
|
const tokenList = shuffle(Object.entries(contractMap)
|
||||||
|
.map(([address, tokenData]) => ({ ...tokenData, address: address.toLowerCase() }))
|
||||||
|
.filter((tokenData) => Boolean(tokenData.erc20)))
|
||||||
|
|
||||||
|
export function getRenderableTokenData (token, contractExchangeRates, conversionRate, currentCurrency) {
|
||||||
|
const { symbol, name, address, iconUrl, string, balance, decimals } = token
|
||||||
|
|
||||||
|
const formattedFiat = getTokenFiatAmount(
|
||||||
|
symbol === 'ETH' ? 1 : contractExchangeRates[address],
|
||||||
|
conversionRate,
|
||||||
|
currentCurrency,
|
||||||
|
string,
|
||||||
|
symbol,
|
||||||
|
true,
|
||||||
|
) || ''
|
||||||
|
const rawFiat = getTokenFiatAmount(
|
||||||
|
symbol === 'ETH' ? 1 : contractExchangeRates[address],
|
||||||
|
conversionRate,
|
||||||
|
currentCurrency,
|
||||||
|
string,
|
||||||
|
symbol,
|
||||||
|
false,
|
||||||
|
) || ''
|
||||||
|
const usedIconUrl = iconUrl || (contractMap[checksumAddress(address)] && `images/contract/${contractMap[checksumAddress(address)].logo}`)
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
primaryLabel: symbol,
|
||||||
|
secondaryLabel: name || contractMap[checksumAddress(address)]?.name,
|
||||||
|
rightPrimaryLabel: string && `${(new BigNumber(string)).round(6).toString()} ${symbol}`,
|
||||||
|
rightSecondaryLabel: formattedFiat,
|
||||||
|
iconUrl: usedIconUrl,
|
||||||
|
identiconAddress: usedIconUrl ? null : address,
|
||||||
|
balance,
|
||||||
|
decimals,
|
||||||
|
name: name || contractMap[checksumAddress(address)]?.name,
|
||||||
|
rawFiat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTokensToSearch ({ providedTokens, rawEthBalance, usersTokens = [], topTokens = {}, onlyEth, singleToken }) {
|
||||||
|
const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual)
|
||||||
|
const conversionRate = useSelector(getConversionRate)
|
||||||
|
const currentCurrency = useSelector(getCurrentCurrency)
|
||||||
|
|
||||||
|
const memoizedTopTokens = useEqualityCheck(topTokens)
|
||||||
|
const memoizedUsersToken = useEqualityCheck(usersTokens)
|
||||||
|
|
||||||
|
const decEthBalance = getValueFromWeiHex({ value: rawEthBalance, numberOfDecimals: 4, toDenomination: 'ETH' })
|
||||||
|
const [ethToken] = useState(() => getRenderableTokenData(
|
||||||
|
{ ...ETH_SWAPS_TOKEN_OBJECT, balance: rawEthBalance, string: decEthBalance },
|
||||||
|
tokenConversionRates,
|
||||||
|
conversionRate,
|
||||||
|
currentCurrency,
|
||||||
|
))
|
||||||
|
|
||||||
|
const swapsTokens = useSelector(getSwapsTokens) || []
|
||||||
|
let tokensToSearch
|
||||||
|
if (onlyEth) {
|
||||||
|
tokensToSearch = [ethToken]
|
||||||
|
} else if (singleToken) {
|
||||||
|
tokensToSearch = providedTokens
|
||||||
|
} else if (providedTokens) {
|
||||||
|
tokensToSearch = [ethToken, ...providedTokens]
|
||||||
|
} else if (swapsTokens.length) {
|
||||||
|
tokensToSearch = [ethToken, ...swapsTokens]
|
||||||
|
} else {
|
||||||
|
tokensToSearch = [ethToken, ...tokenList]
|
||||||
|
}
|
||||||
|
const memoizedTokensToSearch = useEqualityCheck(tokensToSearch)
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
|
||||||
|
const usersTokensAddressMap = memoizedUsersToken.reduce((acc, token) => ({ ...acc, [token.address]: token }), {})
|
||||||
|
|
||||||
|
const tokensToSearchBuckets = {
|
||||||
|
owned: singleToken ? [] : [ethToken],
|
||||||
|
top: [],
|
||||||
|
others: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
memoizedTokensToSearch.forEach((token) => {
|
||||||
|
const renderableDataToken = getRenderableTokenData({ ...usersTokensAddressMap[token.address], ...token }, tokenConversionRates, conversionRate, currentCurrency)
|
||||||
|
if (usersTokensAddressMap[token.address] && ((renderableDataToken.symbol === 'ETH') || Number(renderableDataToken.balance ?? 0) !== 0)) {
|
||||||
|
tokensToSearchBuckets.owned.push(renderableDataToken)
|
||||||
|
} else if (memoizedTopTokens[token.address]) {
|
||||||
|
tokensToSearchBuckets.top[memoizedTopTokens[token.address].index] = renderableDataToken
|
||||||
|
} else {
|
||||||
|
tokensToSearchBuckets.others.push(renderableDataToken)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tokensToSearchBuckets.owned = tokensToSearchBuckets.owned.sort(({ rawFiat }, { rawFiat: secondRawFiat }) => {
|
||||||
|
return ((new BigNumber(rawFiat || 0)).gt(secondRawFiat || 0) ? -1 : 1)
|
||||||
|
})
|
||||||
|
tokensToSearchBuckets.top = tokensToSearchBuckets.top.filter((token) => token)
|
||||||
|
return [
|
||||||
|
...tokensToSearchBuckets.owned,
|
||||||
|
...tokensToSearchBuckets.top,
|
||||||
|
...tokensToSearchBuckets.others,
|
||||||
|
]
|
||||||
|
}, [memoizedTokensToSearch, memoizedUsersToken, tokenConversionRates, conversionRate, currentCurrency, memoizedTopTokens, ethToken, singleToken])
|
||||||
|
}
|
@ -17,9 +17,12 @@ import {
|
|||||||
TRANSACTION_CATEGORY_RECEIVE,
|
TRANSACTION_CATEGORY_RECEIVE,
|
||||||
TRANSACTION_CATEGORY_SEND,
|
TRANSACTION_CATEGORY_SEND,
|
||||||
TRANSACTION_CATEGORY_SIGNATURE_REQUEST,
|
TRANSACTION_CATEGORY_SIGNATURE_REQUEST,
|
||||||
|
TRANSACTION_CATEGORY_SWAP,
|
||||||
TOKEN_METHOD_APPROVE,
|
TOKEN_METHOD_APPROVE,
|
||||||
PENDING_STATUS_HASH,
|
PENDING_STATUS_HASH,
|
||||||
TOKEN_CATEGORY_HASH,
|
TOKEN_CATEGORY_HASH,
|
||||||
|
SWAP,
|
||||||
|
SWAP_APPROVAL,
|
||||||
} from '../helpers/constants/transactions'
|
} from '../helpers/constants/transactions'
|
||||||
import { getTokens } from '../ducks/metamask/metamask'
|
import { getTokens } from '../ducks/metamask/metamask'
|
||||||
import { useI18nContext } from './useI18nContext'
|
import { useI18nContext } from './useI18nContext'
|
||||||
@ -28,6 +31,8 @@ import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'
|
|||||||
import { useCurrencyDisplay } from './useCurrencyDisplay'
|
import { useCurrencyDisplay } from './useCurrencyDisplay'
|
||||||
import { useTokenDisplayValue } from './useTokenDisplayValue'
|
import { useTokenDisplayValue } from './useTokenDisplayValue'
|
||||||
import { useTokenData } from './useTokenData'
|
import { useTokenData } from './useTokenData'
|
||||||
|
import { useSwappedTokenValue } from './useSwappedTokenValue'
|
||||||
|
import { useCurrentAsset } from './useCurrentAsset'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} TransactionDisplayData
|
* @typedef {Object} TransactionDisplayData
|
||||||
@ -53,6 +58,9 @@ import { useTokenData } from './useTokenData'
|
|||||||
* @return {TransactionDisplayData}
|
* @return {TransactionDisplayData}
|
||||||
*/
|
*/
|
||||||
export function useTransactionDisplayData (transactionGroup) {
|
export function useTransactionDisplayData (transactionGroup) {
|
||||||
|
// To determine which primary currency to display for swaps transactions we need to be aware
|
||||||
|
// of which asset, if any, we are viewing at present
|
||||||
|
const currentAsset = useCurrentAsset()
|
||||||
const knownTokens = useSelector(getTokens)
|
const knownTokens = useSelector(getTokens)
|
||||||
const t = useI18nContext()
|
const t = useI18nContext()
|
||||||
const { initialTransaction, primaryTransaction } = transactionGroup
|
const { initialTransaction, primaryTransaction } = transactionGroup
|
||||||
@ -65,6 +73,7 @@ export function useTransactionDisplayData (transactionGroup) {
|
|||||||
const methodData = useSelector((state) => getKnownMethodData(state, initialTransaction?.txParams?.data)) || {}
|
const methodData = useSelector((state) => getKnownMethodData(state, initialTransaction?.txParams?.data)) || {}
|
||||||
|
|
||||||
const status = getStatusKey(primaryTransaction)
|
const status = getStatusKey(primaryTransaction)
|
||||||
|
const isPending = status in PENDING_STATUS_HASH
|
||||||
|
|
||||||
const primaryValue = primaryTransaction.txParams?.value
|
const primaryValue = primaryTransaction.txParams?.value
|
||||||
let prefix = '-'
|
let prefix = '-'
|
||||||
@ -90,19 +99,58 @@ export function useTransactionDisplayData (transactionGroup) {
|
|||||||
|
|
||||||
const origin = stripHttpSchemes(initialTransaction.origin || initialTransaction.msgParams?.origin || '')
|
const origin = stripHttpSchemes(initialTransaction.origin || initialTransaction.msgParams?.origin || '')
|
||||||
|
|
||||||
|
// used to append to the primary display value. initialized to either token.symbol or undefined
|
||||||
|
// but can later be modified if dealing with a swap
|
||||||
|
let primarySuffix = isTokenCategory ? token?.symbol : undefined
|
||||||
|
// used to display the primary value of tx. initialized to either tokenDisplayValue or undefined
|
||||||
|
// but can later be modified if dealing with a swap
|
||||||
|
let primaryDisplayValue = isTokenCategory ? tokenDisplayValue : undefined
|
||||||
|
// used to display fiat amount of tx. initialized to either tokenFiatAmount or undefined
|
||||||
|
// but can later be modified if dealing with a swap
|
||||||
|
let secondaryDisplayValue = isTokenCategory ? tokenFiatAmount : undefined
|
||||||
|
// The transaction group category that will be used for rendering the icon in the activity list
|
||||||
let category
|
let category
|
||||||
|
// The primary title of the Tx that will be displayed in the activity list
|
||||||
let title
|
let title
|
||||||
// There are four types of transaction entries that are currently differentiated in the design
|
|
||||||
// 1. signature request
|
const { swapTokenValue, swapTokenFiatAmount, isViewingReceivedTokenFromSwap } = useSwappedTokenValue(transactionGroup, currentAsset)
|
||||||
|
|
||||||
|
// There are seven types of transaction entries that are currently differentiated in the design
|
||||||
|
// 1. Signature request
|
||||||
// 2. Send (sendEth sendTokens)
|
// 2. Send (sendEth sendTokens)
|
||||||
// 3. Deposit
|
// 3. Deposit
|
||||||
// 4. Site interaction
|
// 4. Site interaction
|
||||||
// 5. Approval
|
// 5. Approval
|
||||||
|
// 6. Swap
|
||||||
|
// 7. Swap Approval
|
||||||
|
|
||||||
if (transactionCategory === null || transactionCategory === undefined) {
|
if (transactionCategory === null || transactionCategory === undefined) {
|
||||||
category = TRANSACTION_CATEGORY_SIGNATURE_REQUEST
|
category = TRANSACTION_CATEGORY_SIGNATURE_REQUEST
|
||||||
title = t('signatureRequest')
|
title = t('signatureRequest')
|
||||||
subtitle = origin
|
subtitle = origin
|
||||||
subtitleContainsOrigin = true
|
subtitleContainsOrigin = true
|
||||||
|
|
||||||
|
} else if (transactionCategory === SWAP) {
|
||||||
|
category = TRANSACTION_CATEGORY_SWAP
|
||||||
|
title = t('swapTokenToToken', [
|
||||||
|
primaryTransaction.sourceTokenSymbol,
|
||||||
|
primaryTransaction.destinationTokenSymbol,
|
||||||
|
])
|
||||||
|
subtitle = origin
|
||||||
|
subtitleContainsOrigin = true
|
||||||
|
primarySuffix = isViewingReceivedTokenFromSwap
|
||||||
|
? currentAsset.symbol
|
||||||
|
: primaryTransaction.sourceTokenSymbol
|
||||||
|
primaryDisplayValue = swapTokenValue
|
||||||
|
secondaryDisplayValue = swapTokenFiatAmount
|
||||||
|
prefix = isViewingReceivedTokenFromSwap ? '+' : '-'
|
||||||
|
|
||||||
|
} else if (transactionCategory === SWAP_APPROVAL) {
|
||||||
|
category = TRANSACTION_CATEGORY_APPROVAL
|
||||||
|
title = t('swapApproval', [primaryTransaction.sourceTokenSymbol])
|
||||||
|
subtitle = origin
|
||||||
|
subtitleContainsOrigin = true
|
||||||
|
primarySuffix = primaryTransaction.sourceTokenSymbol
|
||||||
} else if (transactionCategory === TOKEN_METHOD_APPROVE) {
|
} else if (transactionCategory === TOKEN_METHOD_APPROVE) {
|
||||||
category = TRANSACTION_CATEGORY_APPROVAL
|
category = TRANSACTION_CATEGORY_APPROVAL
|
||||||
title = t('approveSpendLimit', [token?.symbol || t('token')])
|
title = t('approveSpendLimit', [token?.symbol || t('token')])
|
||||||
@ -113,16 +161,19 @@ export function useTransactionDisplayData (transactionGroup) {
|
|||||||
title = (methodData?.name && camelCaseToCapitalize(methodData.name)) || t(transactionCategory)
|
title = (methodData?.name && camelCaseToCapitalize(methodData.name)) || t(transactionCategory)
|
||||||
subtitle = origin
|
subtitle = origin
|
||||||
subtitleContainsOrigin = true
|
subtitleContainsOrigin = true
|
||||||
|
|
||||||
} else if (transactionCategory === INCOMING_TRANSACTION) {
|
} else if (transactionCategory === INCOMING_TRANSACTION) {
|
||||||
category = TRANSACTION_CATEGORY_RECEIVE
|
category = TRANSACTION_CATEGORY_RECEIVE
|
||||||
title = t('receive')
|
title = t('receive')
|
||||||
prefix = ''
|
prefix = ''
|
||||||
subtitle = t('fromAddress', [shortenAddress(senderAddress)])
|
subtitle = t('fromAddress', [shortenAddress(senderAddress)])
|
||||||
|
|
||||||
} else if (transactionCategory === TOKEN_METHOD_TRANSFER_FROM || transactionCategory === TOKEN_METHOD_TRANSFER) {
|
} else if (transactionCategory === TOKEN_METHOD_TRANSFER_FROM || transactionCategory === TOKEN_METHOD_TRANSFER) {
|
||||||
category = TRANSACTION_CATEGORY_SEND
|
category = TRANSACTION_CATEGORY_SEND
|
||||||
title = t('sendSpecifiedTokens', [token?.symbol || t('token')])
|
title = t('sendSpecifiedTokens', [token?.symbol || t('token')])
|
||||||
recipientAddress = getTokenAddressParam(tokenData)
|
recipientAddress = getTokenAddressParam(tokenData.params)
|
||||||
subtitle = t('toAddress', [shortenAddress(recipientAddress)])
|
subtitle = t('toAddress', [shortenAddress(recipientAddress)])
|
||||||
|
|
||||||
} else if (transactionCategory === SEND_ETHER_ACTION_KEY) {
|
} else if (transactionCategory === SEND_ETHER_ACTION_KEY) {
|
||||||
category = TRANSACTION_CATEGORY_SEND
|
category = TRANSACTION_CATEGORY_SEND
|
||||||
title = t('sendETH')
|
title = t('sendETH')
|
||||||
@ -134,15 +185,15 @@ export function useTransactionDisplayData (transactionGroup) {
|
|||||||
|
|
||||||
const [primaryCurrency] = useCurrencyDisplay(primaryValue, {
|
const [primaryCurrency] = useCurrencyDisplay(primaryValue, {
|
||||||
prefix,
|
prefix,
|
||||||
displayValue: isTokenCategory ? tokenDisplayValue : undefined,
|
displayValue: primaryDisplayValue,
|
||||||
suffix: isTokenCategory ? token?.symbol : undefined,
|
suffix: primarySuffix,
|
||||||
...primaryCurrencyPreferences,
|
...primaryCurrencyPreferences,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [secondaryCurrency] = useCurrencyDisplay(primaryValue, {
|
const [secondaryCurrency] = useCurrencyDisplay(primaryValue, {
|
||||||
prefix,
|
prefix,
|
||||||
displayValue: isTokenCategory ? tokenFiatAmount : undefined,
|
displayValue: secondaryDisplayValue,
|
||||||
hideLabel: isTokenCategory ? true : undefined,
|
hideLabel: isTokenCategory || Boolean(swapTokenValue),
|
||||||
...secondaryCurrencyPreferences,
|
...secondaryCurrencyPreferences,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -152,11 +203,14 @@ export function useTransactionDisplayData (transactionGroup) {
|
|||||||
date,
|
date,
|
||||||
subtitle,
|
subtitle,
|
||||||
subtitleContainsOrigin,
|
subtitleContainsOrigin,
|
||||||
primaryCurrency,
|
primaryCurrency: transactionCategory === SWAP && isPending ? '' : primaryCurrency,
|
||||||
senderAddress,
|
senderAddress,
|
||||||
recipientAddress,
|
recipientAddress,
|
||||||
secondaryCurrency: isTokenCategory && !tokenFiatAmount ? undefined : secondaryCurrency,
|
secondaryCurrency: (
|
||||||
|
(isTokenCategory && !tokenFiatAmount) ||
|
||||||
|
(transactionCategory === SWAP && !swapTokenFiatAmount)
|
||||||
|
) ? undefined : secondaryCurrency,
|
||||||
status,
|
status,
|
||||||
isPending: status in PENDING_STATUS_HASH,
|
isPending,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) {
|
|||||||
* @param {bool} isEarliestNonce - is this transaction the earliest nonce in list
|
* @param {bool} isEarliestNonce - is this transaction the earliest nonce in list
|
||||||
* @param {number} submittedTime - the timestamp for when the transaction was submitted
|
* @param {number} submittedTime - the timestamp for when the transaction was submitted
|
||||||
* @param {number} currentGasPrice - gas price to use for calculation of time
|
* @param {number} currentGasPrice - gas price to use for calculation of time
|
||||||
|
* @param {boolean} dontFormat - Whether the result should be be formatted, or just a number of minutes
|
||||||
* @returns {string | undefined} i18n formatted string if applicable
|
* @returns {string | undefined} i18n formatted string if applicable
|
||||||
*/
|
*/
|
||||||
export function useTransactionTimeRemaining (
|
export function useTransactionTimeRemaining (
|
||||||
@ -37,6 +38,8 @@ export function useTransactionTimeRemaining (
|
|||||||
isEarliestNonce,
|
isEarliestNonce,
|
||||||
submittedTime,
|
submittedTime,
|
||||||
currentGasPrice,
|
currentGasPrice,
|
||||||
|
forceAllow,
|
||||||
|
dontFormat,
|
||||||
) {
|
) {
|
||||||
// the following two selectors return the result of mapping over an array, as such they
|
// the following two selectors return the result of mapping over an array, as such they
|
||||||
// will always be new objects and trigger effects. To avoid this, we use isEqual as the
|
// will always be new objects and trigger effects. To avoid this, we use isEqual as the
|
||||||
@ -68,8 +71,8 @@ export function useTransactionTimeRemaining (
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
isMainNet &&
|
(isMainNet &&
|
||||||
transactionTimeFeatureActive &&
|
(transactionTimeFeatureActive || forceAllow)) &&
|
||||||
isPending &&
|
isPending &&
|
||||||
isEarliestNonce &&
|
isEarliestNonce &&
|
||||||
!isNaN(initialTimeEstimate)
|
!isNaN(initialTimeEstimate)
|
||||||
@ -93,6 +96,7 @@ export function useTransactionTimeRemaining (
|
|||||||
isPending,
|
isPending,
|
||||||
submittedTime,
|
submittedTime,
|
||||||
initialTimeEstimate,
|
initialTimeEstimate,
|
||||||
|
forceAllow,
|
||||||
])
|
])
|
||||||
|
|
||||||
// there are numerous checks to determine if time should be displayed.
|
// there are numerous checks to determine if time should be displayed.
|
||||||
@ -100,5 +104,8 @@ export function useTransactionTimeRemaining (
|
|||||||
// User is currently not on the mainnet
|
// User is currently not on the mainnet
|
||||||
// User does not have the transactionTime feature flag enabled
|
// User does not have the transactionTime feature flag enabled
|
||||||
// The transaction is not pending, or isn't the earliest nonce
|
// The transaction is not pending, or isn't the earliest nonce
|
||||||
return timeRemaining ? rtf.format(timeRemaining, 'minute') : undefined
|
const usedFormat = dontFormat
|
||||||
|
? timeRemaining
|
||||||
|
: rtf.format(timeRemaining, 'minute')
|
||||||
|
return timeRemaining ? usedFormat : undefined
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,13 @@ export default class EndOfFlowScreen extends PureComponent {
|
|||||||
location: PropTypes.string,
|
location: PropTypes.string,
|
||||||
tabId: PropTypes.number,
|
tabId: PropTypes.number,
|
||||||
}),
|
}),
|
||||||
|
setSpecialRPC: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
onComplete = async () => {
|
onComplete = async () => {
|
||||||
const { history, completionMetaMetricsName, onboardingInitiator } = this.props
|
const { history, completionMetaMetricsName, onboardingInitiator, setSpecialRPC } = this.props
|
||||||
|
|
||||||
|
setSpecialRPC()
|
||||||
|
|
||||||
this.context.metricsEvent({
|
this.context.metricsEvent({
|
||||||
eventOpts: {
|
eventOpts: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
import { updateAndSetCustomRpc } from '../../../store/actions'
|
||||||
import { getOnboardingInitiator } from '../../../selectors'
|
import { getOnboardingInitiator } from '../../../selectors'
|
||||||
import EndOfFlow from './end-of-flow.component'
|
import EndOfFlow from './end-of-flow.component'
|
||||||
|
|
||||||
@ -16,4 +17,14 @@ const mapStateToProps = (state) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(EndOfFlow)
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
return {
|
||||||
|
setSpecialRPC: () => {
|
||||||
|
if (!process.env.IN_TEST) {
|
||||||
|
dispatch(updateAndSetCustomRpc('https://ganache-testnet.airswap-dev.codefi.network', '1', 'ETH', 'mainnet', {}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EndOfFlow)
|
||||||
|
@ -12,6 +12,7 @@ describe('End of Flow Screen', function () {
|
|||||||
history: {
|
history: {
|
||||||
push: sinon.spy(),
|
push: sinon.spy(),
|
||||||
},
|
},
|
||||||
|
setSpecialRPC: () => null,
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
@ -13,6 +13,7 @@ import ConnectedSites from '../connected-sites'
|
|||||||
import ConnectedAccounts from '../connected-accounts'
|
import ConnectedAccounts from '../connected-accounts'
|
||||||
import { Tabs, Tab } from '../../components/ui/tabs'
|
import { Tabs, Tab } from '../../components/ui/tabs'
|
||||||
import { EthOverview } from '../../components/app/wallet-overview'
|
import { EthOverview } from '../../components/app/wallet-overview'
|
||||||
|
import SwapsIntroPopup from '../swaps/intro-popup'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ASSET_ROUTE,
|
ASSET_ROUTE,
|
||||||
@ -23,6 +24,9 @@ import {
|
|||||||
CONNECT_ROUTE,
|
CONNECT_ROUTE,
|
||||||
CONNECTED_ROUTE,
|
CONNECTED_ROUTE,
|
||||||
CONNECTED_ACCOUNTS_ROUTE,
|
CONNECTED_ACCOUNTS_ROUTE,
|
||||||
|
AWAITING_SWAP_ROUTE,
|
||||||
|
BUILD_QUOTE_ROUTE,
|
||||||
|
VIEW_QUOTE_ROUTE,
|
||||||
} from '../../helpers/constants/routes'
|
} from '../../helpers/constants/routes'
|
||||||
|
|
||||||
const LEARN_MORE_URL = 'https://metamask.zendesk.com/hc/en-us/articles/360045129011-Intro-to-MetaMask-v8-extension'
|
const LEARN_MORE_URL = 'https://metamask.zendesk.com/hc/en-us/articles/360045129011-Intro-to-MetaMask-v8-extension'
|
||||||
@ -54,6 +58,12 @@ export default class Home extends PureComponent {
|
|||||||
connectedStatusPopoverHasBeenShown: PropTypes.bool,
|
connectedStatusPopoverHasBeenShown: PropTypes.bool,
|
||||||
defaultHomeActiveTabName: PropTypes.string,
|
defaultHomeActiveTabName: PropTypes.string,
|
||||||
onTabClick: PropTypes.func.isRequired,
|
onTabClick: PropTypes.func.isRequired,
|
||||||
|
setSwapsWelcomeMessageHasBeenShown: PropTypes.func.isRequired,
|
||||||
|
swapsWelcomeMessageHasBeenShown: PropTypes.bool.isRequired,
|
||||||
|
haveSwapsQuotes: PropTypes.bool.isRequired,
|
||||||
|
showAwaitingSwapScreen: PropTypes.bool.isRequired,
|
||||||
|
swapsFetchParams: PropTypes.object,
|
||||||
|
swapsEnabled: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -68,11 +78,20 @@ export default class Home extends PureComponent {
|
|||||||
suggestedTokens = {},
|
suggestedTokens = {},
|
||||||
totalUnapprovedCount,
|
totalUnapprovedCount,
|
||||||
unconfirmedTransactionsCount,
|
unconfirmedTransactionsCount,
|
||||||
|
haveSwapsQuotes,
|
||||||
|
showAwaitingSwapScreen,
|
||||||
|
swapsFetchParams,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
this.setState({ mounted: true })
|
this.setState({ mounted: true })
|
||||||
if (isNotification && totalUnapprovedCount === 0) {
|
if (isNotification && totalUnapprovedCount === 0) {
|
||||||
global.platform.closeCurrentWindow()
|
global.platform.closeCurrentWindow()
|
||||||
|
} else if (showAwaitingSwapScreen) {
|
||||||
|
history.push(AWAITING_SWAP_ROUTE)
|
||||||
|
} else if (haveSwapsQuotes) {
|
||||||
|
history.push(VIEW_QUOTE_ROUTE)
|
||||||
|
} else if (swapsFetchParams) {
|
||||||
|
history.push(BUILD_QUOTE_ROUTE)
|
||||||
} else if (firstPermissionsRequestId) {
|
} else if (firstPermissionsRequestId) {
|
||||||
history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`)
|
history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`)
|
||||||
} else if (unconfirmedTransactionsCount > 0) {
|
} else if (unconfirmedTransactionsCount > 0) {
|
||||||
@ -89,13 +108,16 @@ export default class Home extends PureComponent {
|
|||||||
suggestedTokens,
|
suggestedTokens,
|
||||||
totalUnapprovedCount,
|
totalUnapprovedCount,
|
||||||
unconfirmedTransactionsCount,
|
unconfirmedTransactionsCount,
|
||||||
|
haveSwapsQuotes,
|
||||||
|
showAwaitingSwapScreen,
|
||||||
|
swapsFetchParams,
|
||||||
},
|
},
|
||||||
{ mounted },
|
{ mounted },
|
||||||
) {
|
) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
if (isNotification && totalUnapprovedCount === 0) {
|
if (isNotification && totalUnapprovedCount === 0) {
|
||||||
return { closing: true }
|
return { closing: true }
|
||||||
} else if (firstPermissionsRequestId || unconfirmedTransactionsCount > 0 || Object.keys(suggestedTokens).length > 0) {
|
} else if (firstPermissionsRequestId || unconfirmedTransactionsCount > 0 || Object.keys(suggestedTokens).length > 0 || showAwaitingSwapScreen || haveSwapsQuotes || swapsFetchParams) {
|
||||||
return { redirecting: true }
|
return { redirecting: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,6 +257,9 @@ export default class Home extends PureComponent {
|
|||||||
history,
|
history,
|
||||||
connectedStatusPopoverHasBeenShown,
|
connectedStatusPopoverHasBeenShown,
|
||||||
isPopup,
|
isPopup,
|
||||||
|
swapsWelcomeMessageHasBeenShown,
|
||||||
|
setSwapsWelcomeMessageHasBeenShown,
|
||||||
|
swapsEnabled,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
if (forgottenPassword) {
|
if (forgottenPassword) {
|
||||||
@ -248,6 +273,9 @@ export default class Home extends PureComponent {
|
|||||||
<Route path={CONNECTED_ROUTE} component={ConnectedSites} exact />
|
<Route path={CONNECTED_ROUTE} component={ConnectedSites} exact />
|
||||||
<Route path={CONNECTED_ACCOUNTS_ROUTE} component={ConnectedAccounts} exact />
|
<Route path={CONNECTED_ACCOUNTS_ROUTE} component={ConnectedAccounts} exact />
|
||||||
<div className="home__container">
|
<div className="home__container">
|
||||||
|
{!swapsWelcomeMessageHasBeenShown && swapsEnabled ? (
|
||||||
|
<SwapsIntroPopup onClose={setSwapsWelcomeMessageHasBeenShown} />
|
||||||
|
) : null}
|
||||||
{ isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() : null }
|
{ isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() : null }
|
||||||
<div className="home__main-view">
|
<div className="home__main-view">
|
||||||
<MenuBar />
|
<MenuBar />
|
||||||
|
@ -15,8 +15,10 @@ import {
|
|||||||
setShowRestorePromptToFalse,
|
setShowRestorePromptToFalse,
|
||||||
setConnectedStatusPopoverHasBeenShown,
|
setConnectedStatusPopoverHasBeenShown,
|
||||||
setDefaultHomeActiveTabName,
|
setDefaultHomeActiveTabName,
|
||||||
|
setSwapsWelcomeMessageHasBeenShown,
|
||||||
} from '../../store/actions'
|
} from '../../store/actions'
|
||||||
import { setThreeBoxLastUpdated } from '../../ducks/app/app'
|
import { setThreeBoxLastUpdated } from '../../ducks/app/app'
|
||||||
|
import { getSwapsWelcomeMessageSeenStatus, getSwapsFeatureLiveness } from '../../ducks/swaps/swaps'
|
||||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util'
|
import { getEnvironmentType } from '../../../../app/scripts/lib/util'
|
||||||
import {
|
import {
|
||||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||||
@ -35,10 +37,12 @@ const mapStateToProps = (state) => {
|
|||||||
selectedAddress,
|
selectedAddress,
|
||||||
connectedStatusPopoverHasBeenShown,
|
connectedStatusPopoverHasBeenShown,
|
||||||
defaultHomeActiveTabName,
|
defaultHomeActiveTabName,
|
||||||
|
swapsState,
|
||||||
} = metamask
|
} = metamask
|
||||||
const accountBalance = getCurrentEthBalance(state)
|
const accountBalance = getCurrentEthBalance(state)
|
||||||
const { forgottenPassword, threeBoxLastUpdated } = appState
|
const { forgottenPassword, threeBoxLastUpdated } = appState
|
||||||
const totalUnapprovedCount = getTotalUnapprovedCount(state)
|
const totalUnapprovedCount = getTotalUnapprovedCount(state)
|
||||||
|
const swapsEnabled = getSwapsFeatureLiveness(state)
|
||||||
|
|
||||||
const envType = getEnvironmentType()
|
const envType = getEnvironmentType()
|
||||||
const isPopup = envType === ENVIRONMENT_TYPE_POPUP
|
const isPopup = envType === ENVIRONMENT_TYPE_POPUP
|
||||||
@ -52,6 +56,7 @@ const mapStateToProps = (state) => {
|
|||||||
return {
|
return {
|
||||||
forgottenPassword,
|
forgottenPassword,
|
||||||
suggestedTokens,
|
suggestedTokens,
|
||||||
|
swapsEnabled,
|
||||||
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
|
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
|
||||||
shouldShowSeedPhraseReminder: !seedPhraseBackedUp && (parseInt(accountBalance, 16) > 0 || tokens.length > 0),
|
shouldShowSeedPhraseReminder: !seedPhraseBackedUp && (parseInt(accountBalance, 16) > 0 || tokens.length > 0),
|
||||||
isPopup,
|
isPopup,
|
||||||
@ -64,6 +69,10 @@ const mapStateToProps = (state) => {
|
|||||||
totalUnapprovedCount,
|
totalUnapprovedCount,
|
||||||
connectedStatusPopoverHasBeenShown,
|
connectedStatusPopoverHasBeenShown,
|
||||||
defaultHomeActiveTabName,
|
defaultHomeActiveTabName,
|
||||||
|
swapsWelcomeMessageHasBeenShown: getSwapsWelcomeMessageSeenStatus(state),
|
||||||
|
haveSwapsQuotes: Boolean(Object.values(swapsState.quotes || {}).length),
|
||||||
|
swapsFetchParams: swapsState.fetchParams,
|
||||||
|
showAwaitingSwapScreen: swapsState.routeState === 'awaiting',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +93,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
setShowRestorePromptToFalse: () => dispatch(setShowRestorePromptToFalse()),
|
setShowRestorePromptToFalse: () => dispatch(setShowRestorePromptToFalse()),
|
||||||
setConnectedStatusPopoverHasBeenShown: () => dispatch(setConnectedStatusPopoverHasBeenShown()),
|
setConnectedStatusPopoverHasBeenShown: () => dispatch(setConnectedStatusPopoverHasBeenShown()),
|
||||||
onTabClick: (name) => dispatch(setDefaultHomeActiveTabName(name)),
|
onTabClick: (name) => dispatch(setDefaultHomeActiveTabName(name)),
|
||||||
|
setSwapsWelcomeMessageHasBeenShown: () => dispatch(setSwapsWelcomeMessageHasBeenShown()),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
|
@ -16,5 +16,5 @@
|
|||||||
@import 'permissions-connect/index';
|
@import 'permissions-connect/index';
|
||||||
@import 'send/send';
|
@import 'send/send';
|
||||||
@import 'settings/index';
|
@import 'settings/index';
|
||||||
@import 'token/index';
|
@import 'swaps/index';
|
||||||
@import 'unlock-page/index';
|
@import 'unlock-page/index';
|
||||||
|