merge beta to production (#317)
* update SNX exchange and token address (#301) * update SNX exchange and token address * checksum exchange address * Account improvements (#302) * begin account migration * prep for styling * improve multi-network support allow deprecated window.web3 providers * kill old modal * remove tests * clean up css * Style account modal * kill scss * bug fixes * use suspense for code-splitting and i18n move abis add ENS support to Web3Status * Account modal mobile styles * style tweaks * finalize migration * fix account styling * fix ethereum svg * fixes * Split wallet modal into components * refactor wallet modal and add connect button styles * removing tap highlights fix spacing on some mobile browsers * Styling fixes for account and warning * clean up injected connector logic * remove console * add wrong network copy * restore border radius change wallet logo * various improvements (#313) * add unchecked signer * remove focus underline from tabs * update tokens * remove console log * remove snx for now * make slippage warnings more robust * memo-ize contexts * improve slippage styling * fix warnings + bugs * clear stale network error * Feather icons (#314) * Remove fontawesome * Update new SNX exchange and new token address (#316) * Update new SNX exchange and new token address * remove whitespace * checksum exchange address
@ -1,3 +1,2 @@
|
|||||||
REACT_APP_NETWORK_ID="1"
|
REACT_APP_NETWORK_ID="1"
|
||||||
REACT_APP_NETWORK_URL=""
|
REACT_APP_NETWORK_URL=""
|
||||||
REACT_APP_NETWORK_NAME="Main Ethereum Network"
|
|
@ -11,4 +11,3 @@ install: yarn
|
|||||||
script:
|
script:
|
||||||
- yarn check:all
|
- yarn check:all
|
||||||
- yarn build
|
- yarn build
|
||||||
- yarn test
|
|
||||||
|
@ -24,17 +24,15 @@ yarn
|
|||||||
|
|
||||||
### Configure Environment
|
### Configure Environment
|
||||||
|
|
||||||
Rename `.env.example` to `.env` and fill in the appropriate variables.
|
Rename `.env.local.example` to `.env.local` and fill in the appropriate variables.
|
||||||
|
|
||||||
### Run
|
### Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start
|
yarn start
|
||||||
# or
|
|
||||||
yarn start:rinkeby
|
|
||||||
```
|
```
|
||||||
|
|
||||||
More robust support for other testnets is in the works!
|
To run on a testnet, simply update the `.env.local` file appropriately.
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
|
15
package.json
@ -5,32 +5,31 @@
|
|||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reach/dialog": "^0.2.8",
|
||||||
"@reach/tooltip": "^0.2.0",
|
"@reach/tooltip": "^0.2.0",
|
||||||
"classnames": "^2.2.6",
|
"copy-to-clipboard": "^3.2.0",
|
||||||
"escape-string-regexp": "^2.0.0",
|
"escape-string-regexp": "^2.0.0",
|
||||||
"ethers": "^4.0.27",
|
"ethers": "^4.0.28",
|
||||||
"i18next": "^15.0.9",
|
"i18next": "^15.0.9",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
"i18next-xhr-backend": "^2.0.1",
|
"i18next-xhr-backend": "^2.0.1",
|
||||||
"jazzicon": "^1.5.0",
|
"jazzicon": "^1.5.0",
|
||||||
"node-sass": "^4.11.0",
|
"polished": "^3.3.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-aria-modal": "^4.0.0",
|
"react-device-detect": "^1.6.2",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
|
"react-feather": "^1.1.6",
|
||||||
"react-ga": "^2.5.7",
|
"react-ga": "^2.5.7",
|
||||||
"react-i18next": "^10.7.0",
|
"react-i18next": "^10.7.0",
|
||||||
"react-router-dom": "^5.0.0",
|
"react-router-dom": "^5.0.0",
|
||||||
"react-scripts": "^3.0.1",
|
"react-scripts": "^3.0.1",
|
||||||
"react-transition-group": "1.x",
|
"react-spring": "^8.0.20",
|
||||||
"styled-components": "^4.2.0",
|
"styled-components": "^4.2.0",
|
||||||
"ua-parser-js": "^0.7.18",
|
|
||||||
"web3-react": "^5.0.4"
|
"web3-react": "^5.0.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"start:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK_NAME='Rinkeby Test Network' yarn start",
|
|
||||||
"build:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK_NAME='Rinkeby Test Network' yarn build",
|
|
||||||
"test": "react-scripts test --env=jsdom",
|
"test": "react-scripts test --env=jsdom",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"lint:base": "yarn eslint './src/**/*.{js,jsx}'",
|
"lint:base": "yarn eslint './src/**/*.{js,jsx}'",
|
||||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 31 KiB |
@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html />
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
@ -23,8 +23,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root" />
|
||||||
<div id="modal-root"></div>
|
|
||||||
<!--
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
If you open it directly in the browser, you will see an empty page.
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
(function(){
|
|
||||||
'use strict';var aa="function"==typeof Object.defineProperties?Object.defineProperty:function(d,c,a){d!=Array.prototype&&d!=Object.prototype&&(d[c]=a.value)},e="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this;function ba(){ba=function(){};e.Symbol||(e.Symbol=da)}var da=function(){var d=0;return function(c){return"jscomp_symbol_"+(c||"")+d++}}();
|
|
||||||
function sa(){ba();var d=e.Symbol.iterator;d||(d=e.Symbol.iterator=e.Symbol("iterator"));"function"!=typeof Array.prototype[d]&&aa(Array.prototype,d,{configurable:!0,writable:!0,value:function(){return ta(this)}});sa=function(){}}function ta(d){var c=0;return ua(function(){return c<d.length?{done:!1,value:d[c++]}:{done:!0}})}function ua(d){sa();d={next:d};d[e.Symbol.iterator]=function(){return this};return d}function va(d){sa();ba();sa();var c=d[Symbol.iterator];return c?c.call(d):ta(d)}
|
|
||||||
function Oa(d,c){if(c){var a=e;d=d.split(".");for(var b=0;b<d.length-1;b++){var g=d[b];g in a||(a[g]={});a=a[g]}d=d[d.length-1];b=a[d];c=c(b);c!=b&&null!=c&&aa(a,d,{configurable:!0,writable:!0,value:c})}}
|
|
||||||
Oa("String.prototype.startsWith",function(d){return d?d:function(c,a){if(null==this)throw new TypeError("The 'this' value for String.prototype.startsWith must not be null or undefined");if(c instanceof RegExp)throw new TypeError("First argument to String.prototype.startsWith must not be a regular expression");var b=this+"";c+="";var g=b.length,d=c.length;a=Math.max(0,Math.min(a|0,b.length));for(var h=0;h<d&&a<g;)if(b[a++]!=c[h++])return!1;return h>=d}});
|
|
||||||
Oa("Array.prototype.find",function(d){return d?d:function(c,a){a:{var b=this;b instanceof String&&(b=String(b));for(var g=b.length,d=0;d<g;d++){var h=b[d];if(c.call(a,h,d,b)){c=h;break a}}c=void 0}return c}});
|
|
||||||
self.onmessage=function(d){var c=d.data.data;switch(d.data.type){case "setDebug":Pa=c;break;case "decode":var a=null;try{Qa=c;f=c.width;n=c.height;var b=Qa.data,g=0,k=new Uint8ClampedArray(b.buffer,g,f*n);g+=f*n;var h=new Uint8ClampedArray(b.buffer,g,f*n);g+=f*n;var l=va(Ra(f,n));l.next();for(var m=l.next().value,p=l.next().value,w=new Uint8ClampedArray(b.buffer,g,m*p),q=f,u=n,t=0;t<u;t++)for(var x=0;x<q;x++){var v=t*q+x,ca=4*v;k[v]=Sa.red*b[ca]+Sa.blue*b[ca+1]+Sa.green*b[ca+2]+128>>8}var ea=h,Q=
|
|
||||||
w,R=f,E=n;ea=void 0===ea?k:ea;Q=void 0===Q?null:Q;var S=va(Ra(R,E)),z=S.next().value,C=S.next().value,X=S.next().value;if(Q){if(!(Q instanceof Uint8ClampedArray)||Q.byteLength!==C*X)throw Error("QR Error: Illegal Buffer.");var Y=Q}else Y=new Uint8ClampedArray(C*X);for(var U=0;U<X;++U)for(var V=0;V<C;++V){for(var M=255,Z=0,Va=Math.min(U*z,E-z)*R+Math.min(V*z,R-z),Wa=0;Wa<z;++Wa){for(var wa=0;wa<z;++wa){var fa=k[Va+wa];fa<M&&(M=fa);fa>Z&&(Z=fa)}Va+=R}if(Z-M>Ta){var Xa=(M+Z)/2;var xa=Math.min(255,Xa+
|
|
||||||
(M+Z)/4,1.1*Xa)}else if(0===V||0===U)xa=M-1;else{var ya=U*C+V,Ya=(Y[ya-1]+Y[ya-C]+C[ya-C-1])/3;xa=Ya>M?Ya:M-1}Y[U*C+V]=xa}for(var ha=0;ha<X;++ha)for(var ia=0;ia<C;++ia){for(var Za=0,za=-2;2>=za;++za)for(var Aa=-2;2>=Aa;++Aa)Za+=Y[Math.max(0,Math.min(X-1,ha+Aa))*C+Math.max(0,Math.min(C-1,ia+za))];var $a=k,Ba=R,Kb=Za/25,ja=ea;ja=void 0===ja?$a:ja;for(var ab=Math.min(ha*z,E-z)*Ba+Math.min(ia*z,Ba-z),bb=0;bb<z;++bb){for(var Ca=0;Ca<z;++Ca){var cb=ab+Ca;ja[cb]=$a[cb]<=Kb}ab+=Ba}}if(Pa){var F=new ImageData(new Uint8ClampedArray(f*
|
|
||||||
n*4),f,n);for(var G=0;G<n;G++)for(var H=0;H<f;H++){var N=4*H+G*f*4,Da=h[G*f+H]?0:255;F.data[N]=Da;F.data[N+1]=Da;F.data[N+2]=Da;F.data[N+3]=255}}try{var ka=(new Ua(h)).Oa();if(Pa)for(G=0;G<ka.i.height;G++)for(H=0;H<ka.i.width;H++){N=8*H+2*G*f*4;var db=ka.i.ga(H,G);F.data[N]=db?0:255;F.data[N+1]=db?0:255;F.data[N+2]=255}}finally{Pa&&self.postMessage({type:"debugImage",data:F},[F.data.buffer])}var Ea=new tb(ka.i),Fa=Ea.Ba(),eb=Ea.Aa().Pa,la=Ea.jb();if(la.length!=Fa.na)throw Error("QR Error: ArgumentException");
|
|
||||||
for(var Ga=Fa.Za(eb),fb=0,ma=Ga.da,B=0;B<ma.length;B++)fb+=ma[B].count;for(var D=Array(fb),na=0,A=0;A<ma.length;A++){var gb=ma[A];for(B=0;B<gb.count;B++){var hb=gb.ra,Nb=Ga.ka+hb;D[na++]=new ub(hb,Array(Nb))}}for(var ib=D[0].T.length,W=D.length-1;0<=W&&D[W].T.length!=ib;)W--;W++;var Ha=ib-Ga.ka,Ia=0;for(B=0;B<Ha;B++)for(A=0;A<na;A++)D[A].T[B]=la[Ia++];for(A=W;A<na;A++)D[A].T[Ha]=la[Ia++];var Pb=D[0].T.length;for(B=Ha;B<Pb;B++)for(A=0;A<na;A++)D[A].T[A<W?B:B+1]=la[Ia++];for(var jb=0,T=0;T<D.length;T++)jb+=
|
|
||||||
D[T].ya;for(var kb=Array(jb),Qb=0,Ja=0;Ja<D.length;Ja++){for(var lb=D[Ja],mb=lb.T,nb=lb.ya,oa=mb,ob=nb,pb=oa.length,Ka=Array(pb),I=0;I<pb;I++)Ka[I]=oa[I]&255;var Rb=oa.length-ob;try{vb.decode(Ka,Rb)}catch(J){throw J;}for(I=0;I<ob;I++)oa[I]=Ka[I];for(T=0;T<nb;T++)kb[Qb++]=mb[T]}for(var La=(new wb(kb,Fa.oa,eb.i)).Xa(),qb="",pa=0;pa<La.length;pa++)for(var Ma=0;Ma<La[pa].length;Ma++)qb+=String.fromCharCode(La[pa][Ma]);var qa=qb;try{new URL(qa);var rb=!0}catch(J){rb=!1}if(rb){var ra="";try{ra=escape(qa)}catch(J){console.log(J),
|
|
||||||
ra=qa}var Na="";try{Na=decodeURIComponent(ra)}catch(J){console.log(J),Na=ra}var sb=Na}else sb=qa;a=xb=sb}catch(J){if(!J.message.startsWith("QR Error"))throw J;}finally{self.postMessage({type:"qrResult",data:a})}break;case "grayscaleWeights":if(256!==c.red+c.green+c.blue)throw Error("Weights have to sum up to 256");Sa=c}};function Ra(d,c){var a=Math.max(Math.floor(Math.min(d,c)/yb),zb);return[a,Math.ceil(d/a),Math.ceil(c/a)]}var yb=40,zb=16,Ta=12;function r(d,c){this.count=d;this.ra=c}function y(d,c,a){this.ka=d;this.da=a?[c,a]:Array(c)}
|
|
||||||
function K(d,c,a,b,g,k){this.oa=d;this.ca=c;this.da=[a,b,g,k];d=0;c=a.ka;a=a.da;for(b=0;b<a.length;b++)g=a[b],d+=g.count*(g.ra+c);this.na=d;this.fa=function(){return 17+4*this.oa};this.Ia=function(){var b=this.fa(),a=new Ab(b);a.P(0,0,9,9);a.P(b-8,0,8,9);a.P(0,b-8,9,8);for(var g=this.ca.length,c=0;c<g;c++)for(var d=this.ca[c]-2,k=0;k<g;k++)0==c&&(0==k||k==g-1)||c==g-1&&0==k||a.P(this.ca[k]-2,d,5,5);a.P(6,9,1,b-17);a.P(9,6,b-17,1);6<this.oa&&(a.P(b-11,0,3,6),a.P(0,b-11,6,3));return a};this.Za=function(b){return this.da[b.gb]}}
|
|
||||||
var Bb=[31892,34236,39577,42195,48118,51042,55367,58893,63784,68472,70749,76311,79154,84390,87683,92361,96236,102084,102881,110507,110734,117786,119615,126325,127568,133589,136944,141498,145311,150283,152622,158308,161089,167017],Cb=[new K(1,[],new y(7,new r(1,19)),new y(10,new r(1,16)),new y(13,new r(1,13)),new y(17,new r(1,9))),new K(2,[6,18],new y(10,new r(1,34)),new y(16,new r(1,28)),new y(22,new r(1,22)),new y(28,new r(1,16))),new K(3,[6,22],new y(15,new r(1,55)),new y(26,new r(1,44)),new y(18,
|
|
||||||
new r(2,17)),new y(22,new r(2,13))),new K(4,[6,26],new y(20,new r(1,80)),new y(18,new r(2,32)),new y(26,new r(2,24)),new y(16,new r(4,9))),new K(5,[6,30],new y(26,new r(1,108)),new y(24,new r(2,43)),new y(18,new r(2,15),new r(2,16)),new y(22,new r(2,11),new r(2,12))),new K(6,[6,34],new y(18,new r(2,68)),new y(16,new r(4,27)),new y(24,new r(4,19)),new y(28,new r(4,15))),new K(7,[6,22,38],new y(20,new r(2,78)),new y(18,new r(4,31)),new y(18,new r(2,14),new r(4,15)),new y(26,new r(4,13),new r(1,14))),
|
|
||||||
new K(8,[6,24,42],new y(24,new r(2,97)),new y(22,new r(2,38),new r(2,39)),new y(22,new r(4,18),new r(2,19)),new y(26,new r(4,14),new r(2,15))),new K(9,[6,26,46],new y(30,new r(2,116)),new y(22,new r(3,36),new r(2,37)),new y(20,new r(4,16),new r(4,17)),new y(24,new r(4,12),new r(4,13))),new K(10,[6,28,50],new y(18,new r(2,68),new r(2,69)),new y(26,new r(4,43),new r(1,44)),new y(24,new r(6,19),new r(2,20)),new y(28,new r(6,15),new r(2,16))),new K(11,[6,30,54],new y(20,new r(4,81)),new y(30,new r(1,
|
|
||||||
50),new r(4,51)),new y(28,new r(4,22),new r(4,23)),new y(24,new r(3,12),new r(8,13))),new K(12,[6,32,58],new y(24,new r(2,92),new r(2,93)),new y(22,new r(6,36),new r(2,37)),new y(26,new r(4,20),new r(6,21)),new y(28,new r(7,14),new r(4,15))),new K(13,[6,34,62],new y(26,new r(4,107)),new y(22,new r(8,37),new r(1,38)),new y(24,new r(8,20),new r(4,21)),new y(22,new r(12,11),new r(4,12))),new K(14,[6,26,46,66],new y(30,new r(3,115),new r(1,116)),new y(24,new r(4,40),new r(5,41)),new y(20,new r(11,16),
|
|
||||||
new r(5,17)),new y(24,new r(11,12),new r(5,13))),new K(15,[6,26,48,70],new y(22,new r(5,87),new r(1,88)),new y(24,new r(5,41),new r(5,42)),new y(30,new r(5,24),new r(7,25)),new y(24,new r(11,12),new r(7,13))),new K(16,[6,26,50,74],new y(24,new r(5,98),new r(1,99)),new y(28,new r(7,45),new r(3,46)),new y(24,new r(15,19),new r(2,20)),new y(30,new r(3,15),new r(13,16))),new K(17,[6,30,54,78],new y(28,new r(1,107),new r(5,108)),new y(28,new r(10,46),new r(1,47)),new y(28,new r(1,22),new r(15,23)),new y(28,
|
|
||||||
new r(2,14),new r(17,15))),new K(18,[6,30,56,82],new y(30,new r(5,120),new r(1,121)),new y(26,new r(9,43),new r(4,44)),new y(28,new r(17,22),new r(1,23)),new y(28,new r(2,14),new r(19,15))),new K(19,[6,30,58,86],new y(28,new r(3,113),new r(4,114)),new y(26,new r(3,44),new r(11,45)),new y(26,new r(17,21),new r(4,22)),new y(26,new r(9,13),new r(16,14))),new K(20,[6,34,62,90],new y(28,new r(3,107),new r(5,108)),new y(26,new r(3,41),new r(13,42)),new y(30,new r(15,24),new r(5,25)),new y(28,new r(15,15),
|
|
||||||
new r(10,16))),new K(21,[6,28,50,72,94],new y(28,new r(4,116),new r(4,117)),new y(26,new r(17,42)),new y(28,new r(17,22),new r(6,23)),new y(30,new r(19,16),new r(6,17))),new K(22,[6,26,50,74,98],new y(28,new r(2,111),new r(7,112)),new y(28,new r(17,46)),new y(30,new r(7,24),new r(16,25)),new y(24,new r(34,13))),new K(23,[6,30,54,74,102],new y(30,new r(4,121),new r(5,122)),new y(28,new r(4,47),new r(14,48)),new y(30,new r(11,24),new r(14,25)),new y(30,new r(16,15),new r(14,16))),new K(24,[6,28,54,
|
|
||||||
80,106],new y(30,new r(6,117),new r(4,118)),new y(28,new r(6,45),new r(14,46)),new y(30,new r(11,24),new r(16,25)),new y(30,new r(30,16),new r(2,17))),new K(25,[6,32,58,84,110],new y(26,new r(8,106),new r(4,107)),new y(28,new r(8,47),new r(13,48)),new y(30,new r(7,24),new r(22,25)),new y(30,new r(22,15),new r(13,16))),new K(26,[6,30,58,86,114],new y(28,new r(10,114),new r(2,115)),new y(28,new r(19,46),new r(4,47)),new y(28,new r(28,22),new r(6,23)),new y(30,new r(33,16),new r(4,17))),new K(27,[6,
|
|
||||||
34,62,90,118],new y(30,new r(8,122),new r(4,123)),new y(28,new r(22,45),new r(3,46)),new y(30,new r(8,23),new r(26,24)),new y(30,new r(12,15),new r(28,16))),new K(28,[6,26,50,74,98,122],new y(30,new r(3,117),new r(10,118)),new y(28,new r(3,45),new r(23,46)),new y(30,new r(4,24),new r(31,25)),new y(30,new r(11,15),new r(31,16))),new K(29,[6,30,54,78,102,126],new y(30,new r(7,116),new r(7,117)),new y(28,new r(21,45),new r(7,46)),new y(30,new r(1,23),new r(37,24)),new y(30,new r(19,15),new r(26,16))),
|
|
||||||
new K(30,[6,26,52,78,104,130],new y(30,new r(5,115),new r(10,116)),new y(28,new r(19,47),new r(10,48)),new y(30,new r(15,24),new r(25,25)),new y(30,new r(23,15),new r(25,16))),new K(31,[6,30,56,82,108,134],new y(30,new r(13,115),new r(3,116)),new y(28,new r(2,46),new r(29,47)),new y(30,new r(42,24),new r(1,25)),new y(30,new r(23,15),new r(28,16))),new K(32,[6,34,60,86,112,138],new y(30,new r(17,115)),new y(28,new r(10,46),new r(23,47)),new y(30,new r(10,24),new r(35,25)),new y(30,new r(19,15),new r(35,
|
|
||||||
16))),new K(33,[6,30,58,86,114,142],new y(30,new r(17,115),new r(1,116)),new y(28,new r(14,46),new r(21,47)),new y(30,new r(29,24),new r(19,25)),new y(30,new r(11,15),new r(46,16))),new K(34,[6,34,62,90,118,146],new y(30,new r(13,115),new r(6,116)),new y(28,new r(14,46),new r(23,47)),new y(30,new r(44,24),new r(7,25)),new y(30,new r(59,16),new r(1,17))),new K(35,[6,30,54,78,102,126,150],new y(30,new r(12,121),new r(7,122)),new y(28,new r(12,47),new r(26,48)),new y(30,new r(39,24),new r(14,25)),new y(30,
|
|
||||||
new r(22,15),new r(41,16))),new K(36,[6,24,50,76,102,128,154],new y(30,new r(6,121),new r(14,122)),new y(28,new r(6,47),new r(34,48)),new y(30,new r(46,24),new r(10,25)),new y(30,new r(2,15),new r(64,16))),new K(37,[6,28,54,80,106,132,158],new y(30,new r(17,122),new r(4,123)),new y(28,new r(29,46),new r(14,47)),new y(30,new r(49,24),new r(10,25)),new y(30,new r(24,15),new r(46,16))),new K(38,[6,32,58,84,110,136,162],new y(30,new r(4,122),new r(18,123)),new y(28,new r(13,46),new r(32,47)),new y(30,
|
|
||||||
new r(48,24),new r(14,25)),new y(30,new r(42,15),new r(32,16))),new K(39,[6,26,54,82,110,138,166],new y(30,new r(20,117),new r(4,118)),new y(28,new r(40,47),new r(7,48)),new y(30,new r(43,24),new r(22,25)),new y(30,new r(10,15),new r(67,16))),new K(40,[6,30,58,86,114,142,170],new y(30,new r(19,118),new r(6,119)),new y(28,new r(18,47),new r(31,48)),new y(30,new r(34,24),new r(34,25)),new y(30,new r(20,15),new r(61,16)))];
|
|
||||||
function Db(d){if(1>d||40<d)throw Error("QR Error: ArgumentException");return Cb[d-1]}function Eb(d){for(var c=4294967295,a=0,b=0;b<Bb.length;b++){var g=Bb[b];if(g==d)return Db(b+7);g=Fb(d,g);g<c&&(a=b+7,c=g)}return 3>=c?Db(a):null};function Gb(d,c,a,b,g,k,h,l,m){this.o=d;this.s=b;this.u=h;this.v=c;this.w=g;this.A=l;this.B=a;this.C=k;this.D=m;this.sb=function(b){for(var a=b.length,c=this.o,g=this.s,d=this.u,k=this.v,h=this.w,l=this.A,m=this.B,p=this.C,R=this.D,E=0;E<a;E+=2){var S=b[E],z=b[E+1],C=d*S+l*z+R;b[E]=(c*S+k*z+m)/C;b[E+1]=(g*S+h*z+p)/C}};this.Ha=function(){return new Gb(this.w*this.D-this.A*this.C,this.A*this.B-this.v*this.D,this.v*this.C-this.w*this.B,this.u*this.C-this.s*this.D,this.o*this.D-this.u*this.B,this.s*this.B-
|
|
||||||
this.o*this.C,this.s*this.A-this.u*this.w,this.u*this.v-this.o*this.A,this.o*this.w-this.s*this.v)};this.pb=function(b){return new Gb(this.o*b.o+this.v*b.s+this.B*b.u,this.o*b.v+this.v*b.w+this.B*b.A,this.o*b.B+this.v*b.C+this.B*b.D,this.s*b.o+this.w*b.s+this.C*b.u,this.s*b.v+this.w*b.w+this.C*b.A,this.s*b.B+this.w*b.C+this.C*b.D,this.u*b.o+this.A*b.s+this.D*b.u,this.u*b.v+this.A*b.w+this.D*b.A,this.u*b.B+this.A*b.C+this.D*b.D)}}
|
|
||||||
function Hb(d,c,a,b,g,k,h,l){var m=l-k,p=c-b+k-l;if(0==m&&0==p)return new Gb(a-d,g-a,d,b-c,k-b,c,0,0,1);var w=a-g,q=h-g;g=d-a+g-h;k=b-k;var u=w*m-q*k;m=(g*m-q*p)/u;p=(w*p-g*k)/u;return new Gb(a-d+m*a,h-d+p*h,d,b-c+m*b,l-c+p*l,c,m,p,1)}function Ib(d){this.i=d}
|
|
||||||
function Ua(d){this.H=d;this.U=null;this.Ca=function(c,a,b,g){var d=Math.abs(g-a)>Math.abs(b-c);if(d){var h=c;c=a;a=h;h=b;b=g;g=h}var l=Math.abs(b-c),m=Math.abs(g-a),p=-l>>1,w=a<g?1:-1,q=c<b?1:-1,u=0,t=c;for(h=a;t!=b;t+=q){var x=d?h:t,v=d?t:h;1==u?this.H[x+v*f]&&u++:this.H[x+v*f]||u++;if(3==u)return g=t-c,a=h-a,Math.sqrt(g*g+a*a);p+=m;if(0<p){if(h==g)break;h+=w;p-=l}}c=b-c;a=g-a;return Math.sqrt(c*c+a*a)};this.Da=function(c,a,b,g){var d=this.Ca(c,a,b,g),h=1;b=c-(b-c);0>b?(h=c/(c-b),b=0):b>=f&&(h=
|
|
||||||
(f-1-c)/(b-c),b=f-1);g=Math.floor(a-(g-a)*h);h=1;0>g?(h=a/(a-g),g=0):g>=n&&(h=(n-1-a)/(g-a),g=n-1);b=Math.floor(c+(b-c)*h);d+=this.Ca(c,a,b,g);return d-1};this.qa=function(c,a){var b=this.Da(Math.floor(c.g()),Math.floor(c.h()),Math.floor(a.g()),Math.floor(a.h()));c=this.Da(Math.floor(a.g()),Math.floor(a.h()),Math.floor(c.g()),Math.floor(c.h()));return isNaN(b)?c/7:isNaN(c)?b/7:(b+c)/14};this.Ja=function(c,a,b){return(this.qa(c,a)+this.qa(c,b))/2};this.sa=function(c,a){var b=c.g()-a.g();c=c.h()-a.h();
|
|
||||||
return Math.sqrt(b*b+c*c)};this.Ka=function(c,a,b,g){c=(Math.round(this.sa(c,a)/g)+Math.round(this.sa(c,b)/g)>>1)+7;switch(c&3){case 0:c++;break;case 2:c--;break;case 3:throw Error("QR Error: in detector");}return c};this.Qa=function(c,a,b,g){g=Math.floor(g*c);var d=Math.max(0,a-g);a=Math.min(f-1,a+g);if(a-d<3*c)throw Error("QR Error: in detector");var h=Math.max(0,b-g);return(new Jb(this.H,d,h,a-d,Math.min(n-1,b+g)-h,c,this.U)).find()};this.La=function(c,a,b,g,d){d-=3.5;var k;if(null!=g){var l=g.g();
|
|
||||||
g=g.h();var m=k=d-3}else l=a.g()-c.g()+b.g(),g=a.h()-c.h()+b.h(),m=k=d;return Hb(c.g(),c.h(),a.g(),a.h(),l,g,b.g(),b.h()).pb(Hb(3.5,3.5,d,3.5,m,k,3.5,d).Ha())};this.lb=function(c,a,b){for(var g=new Ab(b),d=Array(b<<1),h=0;h<b;h++){for(var l=d.length,m=h+.5,p=0;p<l;p+=2)d[p]=(p>>1)+.5,d[p+1]=m;a.sb(d);m=d;for(var w=f,q=n,u=!0,t=0;t<m.length&&u;t+=2){var x=Math.floor(m[t]),v=Math.floor(m[t+1]);if(-1>x||x>w||-1>v||v>q)throw Error("QR Error: Error.checkAndNudgePoints");u=!1;-1==x?(m[t]=0,u=!0):x==w&&
|
|
||||||
(m[t]=w-1,u=!0);-1==v?(m[t+1]=0,u=!0):v==q&&(m[t+1]=q-1,u=!0)}u=!0;for(t=m.length-2;0<=t&&u;t-=2){x=Math.floor(m[t]);v=Math.floor(m[t+1]);if(-1>x||x>w||-1>v||v>q)throw Error("QR Error: Error.checkAndNudgePoints");u=!1;-1==x?(m[t]=0,u=!0):x==w&&(m[t]=w-1,u=!0);-1==v?(m[t+1]=0,u=!0):v==q&&(m[t+1]=q-1,u=!0)}try{for(p=0;p<l;p+=2)c[Math.floor(d[p])+f*Math.floor(d[p+1])]&&g.nb(p>>1,h)}catch(ca){throw Error("QR Error: Error.checkAndNudgePoints");}}return g};this.ib=function(c){var a=c.qb,b=c.rb;c=c.Ga;var g=
|
|
||||||
this.Ja(a,b,c);if(1>g)throw Error("QR Error: in detector");var d=this.Ka(a,b,c,g);if(1!=d%4)throw Error("QR Error: Error getProvisionalVersionForDimension");try{var h=Db(d-17>>2)}catch(w){throw Error("QR Error: Error getVersionForNumber");}var l=h.fa()-7,m=null;if(0<h.ca.length){l=1-3/l;h=Math.floor(a.g()+l*(b.g()-a.g()+c.g()-a.g()));l=Math.floor(a.h()+l*(b.h()-a.h()+c.h()-a.h()));for(var p=4;16>=p;p<<=1)try{m=this.Qa(g,h,l,p);break}catch(w){}}a=this.lb(this.H,this.La(a,b,c,m,d),d);return new Ib(a)};
|
|
||||||
this.Oa=function(){var c=(new Lb).Ta(this.H);return this.ib(c)}};var Mb=[[21522,0],[20773,1],[24188,2],[23371,3],[17913,4],[16590,5],[20375,6],[19104,7],[30660,8],[29427,9],[32170,10],[30877,11],[26159,12],[25368,13],[27713,14],[26998,15],[5769,16],[5054,17],[7399,18],[6608,19],[1890,20],[597,21],[3340,22],[2107,23],[13663,24],[12392,25],[16177,26],[14854,27],[9396,28],[8579,29],[11994,30],[11245,31]],L=[0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4];function Ob(d){var c=d>>3&3;if(0>c||c>=Sb.length)throw Error("QR Error: ArgumentException");this.Pa=Sb[c];this.Na=d&7}
|
|
||||||
function Fb(d,c){d^=c;return L[d&15]+L[O(d,4)&15]+L[O(d,8)&15]+L[O(d,12)&15]+L[O(d,16)&15]+L[O(d,20)&15]+L[O(d,24)&15]+L[O(d,28)&15]}function Tb(d){var c=Ub(d);return null!=c?c:Ub(d^21522)}function Ub(d){for(var c=4294967295,a=0,b=0;b<Mb.length;b++){var g=Mb[b],k=g[0];if(k==d)return new Ob(g[1]);k=Fb(d,k);k<c&&(a=g[1],c=k)}return 3>=c?new Ob(a):null};function Vb(d,c,a){this.gb=d;this.i=c;this.name=a;this.getName=function(){return this.name}}var Sb=[new Vb(1,0,"M"),new Vb(0,1,"L"),new Vb(3,2,"H"),new Vb(2,3,"Q")];function Ab(d){var c;c||(c=d);if(1>d||1>c)throw Error("QR Error: Both dimensions must be greater than 0");this.width=d;this.height=c;var a=d>>5;0!=(d&31)&&a++;this.X=a;this.i=Array(a*c);for(d=0;d<this.i.length;d++)this.i[d]=0;this.ea=function(){if(this.width!=this.height)throw Error("QR Error: Can't call getDimension() on a non-square matrix");return this.width};this.ga=function(b,a){return 0!=(O(this.i[a*this.X+(b>>5)],b&31)&1)};this.nb=function(b,a){this.i[a*this.X+(b>>5)]|=1<<(b&31)};this.M=function(b,
|
|
||||||
a){this.i[a*this.X+(b>>5)]^=1<<(b&31)};this.clear=function(){for(var b=this.i.length,a=0;a<b;a++)this.i[a]=0};this.P=function(b,a,c,d){if(0>a||0>b)throw Error("QR Error: Left and top must be nonnegative");if(1>d||1>c)throw Error("QR Error: Height and width must be at least 1");c=b+c;d=a+d;if(d>this.height||c>this.width)throw Error("QR Error: The region must fit inside the matrix");for(;a<d;a++)for(var g=a*this.X,k=b;k<c;k++)this.i[g+(k>>5)]|=1<<(k&31)}};function ub(d,c){this.ya=d;this.T=c};function tb(d){var c=d.ea();if(21>c||1!=(c&3))throw Error("QR Error: Error BitMatrixParser");this.V=d;this.O=this.J=null;this.K=function(a,b,c){return this.V.ga(a,b)?c<<1|1:c<<1};this.Aa=function(){if(null!=this.O)return this.O;for(var a=0,b=0;6>b;b++)a=this.K(b,8,a);a=this.K(7,8,a);a=this.K(8,8,a);a=this.K(8,7,a);for(b=5;0<=b;b--)a=this.K(8,b,a);this.O=Tb(a);if(null!=this.O)return this.O;var c=this.V.ea();a=0;var d=c-8;for(b=c-1;b>=d;b--)a=this.K(b,8,a);for(b=c-7;b<c;b++)a=this.K(8,b,a);this.O=Tb(a);
|
|
||||||
if(null!=this.O)return this.O;throw Error("QR Error: Error readFormatInformation");};this.Ba=function(){if(null!=this.J)return this.J;var a=this.V.ea(),b=a-17>>2;if(6>=b)return Db(b);b=0;for(var c=a-11,d=5;0<=d;d--)for(var h=a-9;h>=c;h--)b=this.K(h,d,b);this.J=Eb(b);if(null!=this.J&&this.J.fa()==a)return this.J;b=0;for(h=5;0<=h;h--)for(d=a-9;d>=c;d--)b=this.K(h,d,b);this.J=Eb(b);if(null!=this.J&&this.J.fa()==a)return this.J;throw Error("QR Error: Error readVersion");};this.jb=function(){var a=this.Aa(),
|
|
||||||
b=this.Ba();a=a.Na;if(0>a||7<a)throw Error("QR Error: System.ArgumentException");var c=Wb[a];a=this.V.ea();c.R(this.V,a);c=b.Ia();for(var d=!0,h=Array(b.na),l=0,m=0,p=0,w=a-1;0<w;w-=2){6==w&&w--;for(var q=0;q<a;q++)for(var u=d?a-1-q:q,t=0;2>t;t++)c.ga(w-t,u)||(p++,m<<=1,this.V.ga(w-t,u)&&(m|=1),8==p&&(h[l++]=m,m=p=0));d^=1}if(l!=b.na)throw Error("QR Error: Error readCodewords");return h}};var Wb=[new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){return 0==(d+c&1)}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d){return 0==(d&1)}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){return 0==c%3}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,
|
|
||||||
b)&&d.M(b,a)};this.j=function(d,c){return 0==(d+c)%3}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){return 0==(O(d,1)+c/3&1)}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){d*=c;return 0==(d&1)+d%3}},new function(){this.R=function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){d*=c;return 0==((d&1)+d%3&1)}},new function(){this.R=
|
|
||||||
function(d,c){for(var a=0;a<c;a++)for(var b=0;b<c;b++)this.j(a,b)&&d.M(b,a)};this.j=function(d,c){return 0==((d+c&1)+d*c%3&1)}}];function P(d,c){if(null==c||0==c.length)throw Error("QR Error: System.ArgumentException");this.a=d;var a=c.length;if(1<a&&0==c[0]){for(var b=1;b<a&&0==c[b];)b++;if(b==a)this.f=d.l().f;else{this.f=Array(a-b);for(a=0;a<this.f.length;a++)this.f[a]=0;for(a=0;a<this.f.length;a++)this.f[a]=c[b+a]}}else this.f=c;this.l=function(){return 0==this.f[0]};this.I=function(){return this.f.length-1};this.Z=function(b){return this.f[this.f.length-1-b]};this.la=function(b){if(0==b)return this.Z(0);var a=this.f.length;
|
|
||||||
if(1==b){for(var c=b=0;c<a;c++)b^=this.f[c];return b}var d=this.f[0];for(c=1;c<a;c++)d=Xb(this.a.multiply(b,d),this.f[c]);return d};this.ba=function(b){if(this.a!=b.a)throw Error("QR Error: GF256Polys do not have same GF256 field");if(this.l())return b;if(b.l())return this;var a=this.f;b=b.f;if(a.length>b.length){var c=a;a=b;b=c}c=Array(b.length);for(var g=b.length-a.length,m=0;m<g;m++)c[m]=b[m];for(m=g;m<b.length;m++)c[m]=a[m-g]^b[m];return new P(d,c)};this.wa=function(b){if(this.a!=b.a)throw Error("QR Error: GF256Polys do not have same GF256 field");
|
|
||||||
if(this.l()||b.l())return this.a.l();var a=this.f,c=a.length;b=b.f;for(var d=b.length,g=Array(c+d-1),p=0;p<c;p++)for(var w=a[p],q=0;q<d;q++)g[p+q]=Xb(g[p+q],this.a.multiply(w,b[q]));return new P(this.a,g)};this.xa=function(b){if(0==b)return this.a.l();if(1==b)return this;for(var a=this.f.length,c=Array(a),d=0;d<a;d++)c[d]=this.a.multiply(this.f[d],b);return new P(this.a,c)};this.eb=function(b,a){if(0>b)throw Error("QR Error: System.ArgumentException");if(0==a)return this.a.l();var c=this.f.length;
|
|
||||||
b=Array(c+b);for(var d=0;d<b.length;d++)b[d]=0;for(d=0;d<c;d++)b[d]=this.a.multiply(this.f[d],a);return new P(this.a,b)}};function Yb(d){this.Y=Array(256);this.aa=Array(256);for(var c=1,a=0;256>a;a++)this.Y[a]=c,c<<=1,256<=c&&(c^=d);for(a=0;255>a;a++)this.aa[this.Y[a]]=a;d=Array(1);d[0]=0;this.Ea=new P(this,Array(d));d=Array(1);d[0]=1;this.za=new P(this,Array(d));this.l=function(){return this.Ea};this.pa=function(b,a){if(0>b)throw Error("QR Error: System.ArgumentException");if(0==a)return this.Ea;b=Array(b+1);for(var c=0;c<b.length;c++)b[c]=0;b[0]=a;return new P(this,b)};this.exp=function(b){return this.Y[b]};this.log=
|
|
||||||
function(b){if(0==b)throw Error("QR Error: System.ArgumentException");return this.aa[b]};this.inverse=function(b){if(0==b)throw Error("QR Error: System.ArithmeticException");return this.Y[255-this.aa[b]]};this.multiply=function(b,a){return 0==b||0==a?0:1==b?a:1==a?b:this.Y[(this.aa[b]+this.aa[a])%255]}}var Zb=new Yb(285);new Yb(301);function Xb(d,c){return d^c};var vb=new function(d){this.a=d;this.decode=function(c,a){for(var b=new P(this.a,c),d=Array(a),k=0;k<d.length;k++)d[k]=0;var h=!0;for(k=0;k<a;k++){var l=b.la(this.a.exp(k));d[d.length-1-k]=l;0!=l&&(h=!1)}if(!h)for(k=new P(this.a,d),a=this.kb(this.a.pa(a,1),k,a),k=a[1],a=this.Ra(a[0]),b=this.Sa(k,a),k=0;k<a.length;k++){d=c.length-1-this.a.log(a[k]);if(0>d)throw Error("QR Error: ReedSolomonException Bad error location");c[d]^=b[k]}};this.kb=function(c,a,b){if(c.I()<a.I()){var d=c;c=a;a=d}d=this.a.za;
|
|
||||||
for(var k=this.a.l(),h=this.a.l(),l=this.a.za;a.I()>=Math.floor(b/2);){var m=c,p=d,w=h;c=a;d=k;h=l;if(c.l())throw Error("QR Error: r_{i-1} was zero");a=m;l=this.a.l();for(k=this.a.inverse(c.Z(c.I()));a.I()>=c.I()&&!a.l();){m=a.I()-c.I();var q=this.a.multiply(a.Z(a.I()),k);l=l.ba(this.a.pa(m,q));a=a.ba(c.eb(m,q))}k=l.wa(d).ba(p);l=l.wa(h).ba(w)}b=l.Z(0);if(0==b)throw Error("QR Error: ReedSolomonException sigmaTilde(0) was zero");b=this.a.inverse(b);c=l.xa(b);b=a.xa(b);return[c,b]};this.Ra=function(c){var a=
|
|
||||||
c.I();if(1==a)return Array(c.Z(1));for(var b=Array(a),d=0,k=1;256>k&&d<a;k++)0==c.la(k)&&(b[d]=this.a.inverse(k),d++);if(d!=a)throw Error("QR Error: Error locator degree does not match number of roots");return b};this.Sa=function(c,a){for(var b=a.length,d=Array(b),k=0;k<b;k++){for(var h=this.a.inverse(a[k]),l=1,m=0;m<b;m++)k!=m&&(l=this.a.multiply(l,Xb(1,this.a.multiply(a[m],h))));d[k]=this.a.multiply(c.la(h),this.a.inverse(l))}return d}}(Zb);var xb,Qa=null,f=0,n=0,Pa=!1,$b=[[10,9,8,8],[12,11,16,10],[14,13,16,12]],Sa={red:77,blue:150,green:29};function O(d,c){return 0<=d?d>>c:(d>>c)+(2<<~c)};var ac=3,bc=57,cc=2;function dc(d){function c(b,a){var c=b.g()-a.g();b=b.h()-a.h();return Math.sqrt(c*c+b*b)}var a=c(d[0],d[1]),b=c(d[1],d[2]),g=c(d[0],d[2]);b>=a&&b>=g?(b=d[0],a=d[1],g=d[2]):g>=b&&g>=a?(b=d[1],a=d[0],g=d[2]):(b=d[2],a=d[0],g=d[1]);if(0>function(b,a,c){var d=a.x;a=a.y;return(c.x-d)*(b.y-a)-(c.y-a)*(b.x-d)}(a,b,g)){var k=a;a=g;g=k}d[0]=a;d[1]=b;d[2]=g}
|
|
||||||
function ec(d,c,a){this.x=d;this.y=c;this.count=1;this.G=a;this.g=function(){return this.x};this.h=function(){return this.y};this.va=function(){this.count++};this.ha=function(b,a,c){return Math.abs(a-this.y)<=b&&Math.abs(c-this.x)<=b?(b=Math.abs(b-this.G),1>=b||1>=b/this.G):!1}}function fc(d){this.Ga=d[0];this.qb=d[1];this.rb=d[2]}
|
|
||||||
function Lb(){this.H=null;this.c=[];this.ma=!1;this.L=[0,0,0,0,0];this.U=null;this.ta=function(){this.L[0]=0;this.L[1]=0;this.L[2]=0;this.L[3]=0;this.L[4]=0;return this.L};this.N=function(d){for(var c=0,a=0;5>a;a++){var b=d[a];if(0==b)return!1;c+=b}if(7>c)return!1;c=Math.floor(c/7);a=Math.floor(.7*c);return Math.abs(c-d[0])<a&&Math.abs(c-d[1])<a&&Math.abs(3*c-d[2])<3*a&&Math.abs(c-d[3])<a&&Math.abs(c-d[4])<a};this.W=function(d,c){return c-d[4]-d[3]-d[2]/2};this.ia=function(d,c,a,b){for(var g=this.H,
|
|
||||||
k=n,h=this.ta(),l=d;0<=l&&g[c+l*f];)h[2]++,l--;if(0>l)return NaN;for(;0<=l&&!g[c+l*f]&&h[1]<=a;)h[1]++,l--;if(0>l||h[1]>a)return NaN;for(;0<=l&&g[c+l*f]&&h[0]<=a;)h[0]++,l--;if(h[0]>a)return NaN;for(l=d+1;l<k&&g[c+l*f];)h[2]++,l++;if(l==k)return NaN;for(;l<k&&!g[c+l*f]&&h[3]<a;)h[3]++,l++;if(l==k||h[3]>=a)return NaN;for(;l<k&&g[c+l*f]&&h[4]<a;)h[4]++,l++;return h[4]>=a||5*Math.abs(h[0]+h[1]+h[2]+h[3]+h[4]-b)>=2*b?NaN:this.N(h)?this.W(h,l):NaN};this.Ma=function(d,c,a,b){for(var g=this.H,k=f,h=this.ta(),
|
|
||||||
l=d;0<=l&&g[l+c*f];)h[2]++,l--;if(0>l)return NaN;for(;0<=l&&!g[l+c*f]&&h[1]<=a;)h[1]++,l--;if(0>l||h[1]>a)return NaN;for(;0<=l&&g[l+c*f]&&h[0]<=a;)h[0]++,l--;if(h[0]>a)return NaN;for(l=d+1;l<k&&g[l+c*f];)h[2]++,l++;if(l==k)return NaN;for(;l<k&&!g[l+c*f]&&h[3]<a;)h[3]++,l++;if(l==k||h[3]>=a)return NaN;for(;l<k&&g[l+c*f]&&h[4]<a;)h[4]++,l++;return h[4]>=a||5*Math.abs(h[0]+h[1]+h[2]+h[3]+h[4]-b)>=b?NaN:this.N(h)?this.W(h,l):NaN};this.$=function(d,c,a){var b=d[0]+d[1]+d[2]+d[3]+d[4];a=this.W(d,a);c=this.ia(c,
|
|
||||||
Math.floor(a),d[2],b);if(!isNaN(c)&&(a=this.Ma(Math.floor(a),Math.floor(c),d[2],b),!isNaN(a))){d=b/7;b=!1;for(var g=this.c.length,k=0;k<g;k++){var h=this.c[k];if(h.ha(d,c,a)){h.va();b=!0;break}}b||(a=new ec(a,c,d),this.c.push(a),null!=this.U&&this.U.Va(a));return!0}return!1};this.mb=function(){var d=this.c.length;if(3>d)throw Error("QR Error: Couldn't find enough finder patterns (found "+d+")");if(3<d){for(var c=0,a=0,b=0;b<d;b++){var g=this.c[b].G;c+=g;a+=g*g}var k=c/d;this.c.sort(function(b,a){a=
|
|
||||||
Math.abs(a.G-k);b=Math.abs(b.G-k);return a<b?-1:a==b?0:1});d=Math.max(.2*k,Math.sqrt(a/d-k*k));for(b=this.c.length-1;0<=b;b--)Math.abs(this.c[b].G-k)>d&&this.c.splice(b,1)}3<this.c.length&&this.c.sort(function(b,a){return b.count>a.count?-1:b.count<a.count?1:0});return[this.c[0],this.c[1],this.c[2]]};this.Ua=function(){var d=this.c.length;if(1>=d)return 0;for(var c=null,a=0;a<d;a++){var b=this.c[a];if(b.count>=cc)if(null==c)c=b;else return this.ma=!0,Math.floor((Math.abs(c.g()-b.g())-Math.abs(c.h()-
|
|
||||||
b.h()))/2)}return 0};this.ua=function(){for(var d=0,c=0,a=this.c.length,b=0;b<a;b++){var g=this.c[b];g.count>=cc&&(d++,c+=g.G)}if(3>d)return!1;d=c/a;var k=0;for(b=0;b<a;b++)g=this.c[b],k+=Math.abs(g.G-d);return k<=.05*c};this.Ta=function(d){this.H=d;var c=n,a=f,b=Math.floor(3*c/(4*bc));b<ac&&(b=ac);for(var g=!1,k=Array(5),h=b-1;h<c&&!g;h+=b){k[0]=0;k[1]=0;k[2]=0;k[3]=0;for(var l=k[4]=0,m=0;m<a;m++)if(d[m+h*f])1==(l&1)&&l++,k[l]++;else if(0==(l&1))if(4==l)if(this.N(k)){if(l=this.$(k,h,m))b=2,this.ma?
|
|
||||||
g=this.ua():(l=this.Ua(),l>k[2]&&(h+=l-k[2]-b,m=a-1));else{do m++;while(m<a&&!d[m+h*f]);m--}l=0;k[0]=0;k[1]=0;k[2]=0;k[3]=0;k[4]=0}else k[0]=k[2],k[1]=k[3],k[2]=k[4],k[3]=1,k[4]=0,l=3;else k[++l]++;else k[l]++;this.N(k)&&(l=this.$(k,h,a))&&(b=k[0],this.ma&&(g=this.ua()))}d=this.mb();dc(d);return new fc(d)}};function gc(d,c,a){this.x=d;this.y=c;this.count=1;this.G=a;this.g=function(){return Math.floor(this.x)};this.h=function(){return Math.floor(this.y)};this.va=function(){this.count++};this.ha=function(b,a,c){return Math.abs(a-this.y)<=b&&Math.abs(c-this.x)<=b?(b=Math.abs(b-this.G),1>=b||1>=b/this.G):!1}}
|
|
||||||
function Jb(d,c,a,b,g,k,h){this.H=d;this.c=[];this.ob=c;this.width=b;this.height=g;this.cb=k;this.L=[0,0,0];this.U=h;this.W=function(b,a){return a-b[2]-b[1]/2};this.N=function(b){for(var a=this.cb,c=a/2,d=0;3>d;d++)if(Math.abs(a-b[d])>=c)return!1;return!0};this.ia=function(b,a,c,d){var g=this.H,k=n,h=this.L;h[0]=0;h[1]=0;h[2]=0;for(var l=b;0<=l&&g[a+l*f]&&h[1]<=c;)h[1]++,l--;if(0>l||h[1]>c)return NaN;for(;0<=l&&!g[a+l*f]&&h[0]<=c;)h[0]++,l--;if(h[0]>c)return NaN;for(l=b+1;l<k&&g[a+l*f]&&h[1]<=c;)h[1]++,
|
|
||||||
l++;if(l==k||h[1]>c)return NaN;for(;l<k&&!g[a+l*f]&&h[2]<=c;)h[2]++,l++;return h[2]>c||5*Math.abs(h[0]+h[1]+h[2]-d)>=2*d?NaN:this.N(h)?this.W(h,l):NaN};this.$=function(b,a,c){c=this.W(b,c);a=this.ia(a,Math.floor(c),2*b[1],b[0]+b[1]+b[2]);if(!isNaN(a)){b=(b[0]+b[1]+b[2])/3;for(var d=this.c.length,g=0;g<d;g++)if(this.c[g].ha(b,a,c))return new gc(c,a,b);c=new gc(c,a,b);this.c.push(c);null!=this.U&&this.U.Va(c)}return null};this.find=function(){for(var c=this.ob,g=this.height,k=c+b,h=a+(g>>1),q=[0,0,
|
|
||||||
0],u=0;u<g;u++){var t=h+(0==(u&1)?u+1>>1:-(u+1>>1));q[0]=0;q[1]=0;q[2]=0;for(var x=c;x<k&&!d[x+f*t];)x++;for(var v=0;x<k;){if(d[x+t*f])if(1==v)q[v]++;else if(2==v){if(this.N(q)&&(v=this.$(q,t,x),null!=v))return v;q[0]=q[2];q[1]=1;q[2]=0;v=1}else q[++v]++;else 1==v&&v++,q[v]++;x++}if(this.N(q)&&(v=this.$(q,t,k),null!=v))return v}if(0!=this.c.length)return this.c[0];throw Error("QR Error: Couldn't find enough alignment patterns");}};function wb(d,c,a){this.F=0;this.b=7;this.S=d;this.fb=a;9>=c?this.ja=0:10<=c&&26>=c?this.ja=1:27<=c&&40>=c&&(this.ja=2);this.m=function(b){var a;if(b<this.b+1){var c=0;for(a=0;a<b;a++)c+=1<<a;c<<=this.b-b+1;a=(this.S[this.F]&c)>>this.b-b+1;this.b-=b;return a}if(b<this.b+1+8){var d=0;for(a=0;a<this.b+1;a++)d+=1<<a;a=(this.S[this.F]&d)<<b-(this.b+1);this.F++;a+=this.S[this.F]>>8-(b-(this.b+1));this.b-=b%8;0>this.b&&(this.b=8+this.b);return a}if(b<this.b+1+16){for(a=c=d=0;a<this.b+1;a++)d+=1<<a;d=(this.S[this.F]&
|
|
||||||
d)<<b-(this.b+1);this.F++;var l=this.S[this.F]<<b-(this.b+1+8);this.F++;for(a=0;a<b-(this.b+1+8);a++)c+=1<<a;c<<=8-(b-(this.b+1+8));a=d+l+((this.S[this.F]&c)>>8-(b-(this.b+1+8)));this.b-=(b-8)%8;0>this.b&&(this.b=8+this.b);return a}return 0};this.Fa=function(){return this.F>this.S.length-this.fb-2?0:this.m(4)};this.Ya=function(b){for(var a=0;1!=b>>a;)a++;return this.m($b[this.ja][a])};this.bb=function(b){var a="",c="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".split("");do if(1<b){var d=this.m(11);
|
|
||||||
var l=d%45;a+=c[Math.floor(d/45)];a+=c[l];b-=2}else 1==b&&(d=this.m(6),a+=c[d],--b);while(0<b);return a};this.$a=function(b){var a=0,c="";do 3<=b?(a=this.m(10),100>a&&(c+="0"),10>a&&(c+="0"),b-=3):2==b?(a=this.m(7),10>a&&(c+="0"),b-=2):1==b&&(a=this.m(4),--b),c+=a;while(0<b);return c};this.Wa=function(b){var a=[];do{var c=this.m(8);a.push(c);b--}while(0<b);return a};this.ab=function(b){var a="";do{var c=this.m(13);c=(c/192<<8)+c%192;a+=String.fromCharCode(40956>=c+33088?c+33088:c+49472);b--}while(0<
|
|
||||||
b);return a};this.hb=function(){var b=this.m(8);128==(b&192)&&this.m(8);192==(b&224)&&this.m(8)};this.Xa=function(){var b=[];do{var a=this.Fa();if(0==a)if(0<b.length)break;else throw Error("QR Error: Empty data block");if(1!=a&&2!=a&&4!=a&&8!=a&&7!=a)throw Error("QR Error: Invalid mode: "+a+" in (block:"+this.F+" bit:"+this.b+")");if(7==a)this.hb();else{var c=this.Ya(a);if(1>c)throw Error("QR Error: Invalid data length: "+c);switch(a){case 1:a=this.$a(c);c=Array(a.length);for(var d=0;d<a.length;d++)c[d]=
|
|
||||||
a.charCodeAt(d);b.push(c);break;case 2:a=this.bb(c);c=Array(a.length);for(d=0;d<a.length;d++)c[d]=a.charCodeAt(d);b.push(c);break;case 4:a=this.Wa(c);b.push(a);break;case 8:a=this.ab(c),b.push(a)}}}while(1);return b}};
|
|
||||||
}).call(this)
|
|
||||||
|
|
||||||
//# sourceMappingURL=qr-scanner-worker.min.js.map
|
|
@ -6,7 +6,9 @@
|
|||||||
"installMetamask": "Please visit us after installing Metamask on Chrome or Brave.",
|
"installMetamask": "Please visit us after installing Metamask on Chrome or Brave.",
|
||||||
"disconnected": "Disconnected",
|
"disconnected": "Disconnected",
|
||||||
"swap": "Swap",
|
"swap": "Swap",
|
||||||
|
"swapAnyway": "Swap Anyway",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
|
"sendAnyway": "Send Anyway",
|
||||||
"pool": "Pool",
|
"pool": "Pool",
|
||||||
"betaWarning": "This project is in beta. Use at your own risk.",
|
"betaWarning": "This project is in beta. Use at your own risk.",
|
||||||
"input": "Input",
|
"input": "Input",
|
||||||
@ -19,6 +21,7 @@
|
|||||||
"searchOrPaste": "Search Token or Paste Address",
|
"searchOrPaste": "Search Token or Paste Address",
|
||||||
"noExchange": "No Exchange Found",
|
"noExchange": "No Exchange Found",
|
||||||
"exchangeRate": "Exchange Rate",
|
"exchangeRate": "Exchange Rate",
|
||||||
|
"unknownError": "Oops! An unknown error occurred. Please refresh the page, or visit from another browser or device.",
|
||||||
"enterValueCont": "Enter a {{ missingCurrencyValue }} value to continue.",
|
"enterValueCont": "Enter a {{ missingCurrencyValue }} value to continue.",
|
||||||
"selectTokenCont": "Select a token to continue.",
|
"selectTokenCont": "Select a token to continue.",
|
||||||
"noLiquidity": "No liquidity.",
|
"noLiquidity": "No liquidity.",
|
||||||
@ -27,6 +30,7 @@
|
|||||||
"transactionDetails": "Transaction Details",
|
"transactionDetails": "Transaction Details",
|
||||||
"hideDetails": "Hide Details",
|
"hideDetails": "Hide Details",
|
||||||
"slippageWarning": "Slippage Warning",
|
"slippageWarning": "Slippage Warning",
|
||||||
|
"highSlippageWarning": "High Slippage Warning",
|
||||||
"youAreSelling": "You are selling",
|
"youAreSelling": "You are selling",
|
||||||
"orTransFail": "or the transaction will fail.",
|
"orTransFail": "or the transaction will fail.",
|
||||||
"youWillReceive": "You will receive at least",
|
"youWillReceive": "You will receive at least",
|
||||||
|
100
src/InjectedConnector.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Connectors } from 'web3-react'
|
||||||
|
const { Connector, ErrorCodeMixin } = Connectors
|
||||||
|
|
||||||
|
const InjectedConnectorErrorCodes = ['ETHEREUM_ACCESS_DENIED', 'NO_WEB3', 'UNLOCK_REQUIRED']
|
||||||
|
export default class InjectedConnector extends ErrorCodeMixin(Connector, InjectedConnectorErrorCodes) {
|
||||||
|
constructor(args = {}) {
|
||||||
|
super(args)
|
||||||
|
this.runOnDeactivation = []
|
||||||
|
|
||||||
|
this.networkChangedHandler = this.networkChangedHandler.bind(this)
|
||||||
|
this.accountsChangedHandler = this.accountsChangedHandler.bind(this)
|
||||||
|
|
||||||
|
const { ethereum } = window
|
||||||
|
if (ethereum && ethereum.isMetaMask) {
|
||||||
|
ethereum.autoRefreshOnNetworkChange = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onActivation() {
|
||||||
|
const { ethereum, web3 } = window
|
||||||
|
|
||||||
|
if (ethereum) {
|
||||||
|
await ethereum.enable().catch(error => {
|
||||||
|
const deniedAccessError = Error(error)
|
||||||
|
deniedAccessError.code = InjectedConnector.errorCodes.ETHEREUM_ACCESS_DENIED
|
||||||
|
throw deniedAccessError
|
||||||
|
})
|
||||||
|
|
||||||
|
// initialize event listeners
|
||||||
|
if (ethereum.on) {
|
||||||
|
ethereum.on('networkChanged', this.networkChangedHandler)
|
||||||
|
ethereum.on('accountsChanged', this.accountsChangedHandler)
|
||||||
|
|
||||||
|
this.runOnDeactivation.push(() => {
|
||||||
|
if (ethereum.removeListener) {
|
||||||
|
ethereum.removeListener('networkChanged', this.networkChangedHandler)
|
||||||
|
ethereum.removeListener('accountsChanged', this.accountsChangedHandler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (web3) {
|
||||||
|
console.warn('Your web3 provider is outdated, please upgrade to a modern provider.')
|
||||||
|
} else {
|
||||||
|
const noWeb3Error = Error('Your browser is not equipped with web3 capabilities.')
|
||||||
|
noWeb3Error.code = InjectedConnector.errorCodes.NO_WEB3
|
||||||
|
throw noWeb3Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProvider() {
|
||||||
|
const { ethereum, web3 } = window
|
||||||
|
return ethereum || web3.currentProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccount(provider) {
|
||||||
|
const account = await super.getAccount(provider)
|
||||||
|
|
||||||
|
if (account === null) {
|
||||||
|
const unlockRequiredError = Error('Ethereum account locked.')
|
||||||
|
unlockRequiredError.code = InjectedConnector.errorCodes.UNLOCK_REQUIRED
|
||||||
|
throw unlockRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeactivation() {
|
||||||
|
this.runOnDeactivation.forEach(runner => runner())
|
||||||
|
this.runOnDeactivation = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// event handlers
|
||||||
|
networkChangedHandler(networkId) {
|
||||||
|
const networkIdNumber = Number(networkId)
|
||||||
|
|
||||||
|
try {
|
||||||
|
super._validateNetworkId(networkIdNumber)
|
||||||
|
|
||||||
|
super._web3ReactUpdateHandler({
|
||||||
|
updateNetworkId: true,
|
||||||
|
networkId: networkIdNumber
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
super._web3ReactErrorHandler(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accountsChangedHandler(accounts) {
|
||||||
|
if (!accounts[0]) {
|
||||||
|
const unlockRequiredError = Error('Ethereum account locked.')
|
||||||
|
unlockRequiredError.code = InjectedConnector.errorCodes.UNLOCK_REQUIRED
|
||||||
|
super._web3ReactErrorHandler(unlockRequiredError)
|
||||||
|
} else {
|
||||||
|
super._web3ReactUpdateHandler({
|
||||||
|
updateAccount: true,
|
||||||
|
account: accounts[0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1736 2036.92">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-2{fill:#fff}
|
|
||||||
</style>
|
|
||||||
<linearGradient id="linear-gradient" x1="868.25" y1="-1245.53" x2="868.25" y2="791.39" gradientTransform="translate(-.25 1245.53)" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#f9ba16"/>
|
|
||||||
<stop offset=".04" stop-color="#faa612"/>
|
|
||||||
<stop offset=".11" stop-color="#fc8b0c"/>
|
|
||||||
<stop offset=".19" stop-color="#fd7508"/>
|
|
||||||
<stop offset=".28" stop-color="#fe6404"/>
|
|
||||||
<stop offset=".4" stop-color="#ff5902"/>
|
|
||||||
<stop offset=".56" stop-color="#ff5200"/>
|
|
||||||
<stop offset="1" stop-color="#ff5000"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g id="Layer_2" data-name="Layer 2">
|
|
||||||
<g id="Layer_1-2" data-name="Layer 1">
|
|
||||||
<path d="M1736 659l-148.2-256.1 17.6-82.9-134.9-137-111.9 15.4L1177.4 0h-310l-310 2.5-180.8 198-111.9-16.1-134.1 138.5 17.2 82.5L0 660.5 40.5 814l184 700.6a358.61 358.61 0 0 0 138.1 199.8s223.9 157.8 444.5 301a98.76 98.76 0 0 0 123.1 0c247.9-162.5 444.1-301.7 444.1-301.7a359 359 0 0 0 137.8-199.8l183.3-701z" fill="url(#linear-gradient)"/>
|
|
||||||
<path class="cls-2" d="M915.5 1213.3a266.26 266.26 0 0 0-35.9-13.3h-22.2a253.36 253.36 0 0 0-35.9 13.3l-54.9 23c-17.6 7.2-45.2 20.4-62.1 29.1L603.4 1318a19.39 19.39 0 0 0-2.5 35.9l87.5 62.1c15.4 10.8 39.8 29.8 53.8 42.3l24.8 21.2 51.7 45.2 23.3 20.8a40.33 40.33 0 0 0 51.7 0L918 1524l51.7-45.2 24.8-21.5c14.3-12.6 38.4-31.6 53.8-42.3l87.9-62.8a19 19 0 0 0-2.5-35.8l-102.2-51.3c-16.9-8.6-44.8-21.5-62.4-28.7l-53.6-23.1z"/>
|
|
||||||
<path class="cls-2" d="M1536.9 698.5l2.9-9a239.82 239.82 0 0 0-2.5-35.9 307.9 307.9 0 0 0-32.6-61.3l-56.7-83.6c-10.4-15.8-28.7-40.5-40.5-55.2l-76.1-94.7a259.16 259.16 0 0 0-22.6-26.5l-33.7 6.1-118.7 20.4-50.9 9.7a131.88 131.88 0 0 1-50.2-7.2l-92.2-29.8c-17.9-5.7-47.7-14-66-18.3a141.51 141.51 0 0 0-29.1 0 141.51 141.51 0 0 0-29.1 0c-18.3 4.3-48.1 12.6-66 18.3l-92.2 29.8a133.86 133.86 0 0 1-50.2 7.2l-51.3-9.7-117-22.6-33.7-6.1a259.16 259.16 0 0 0-22.6 26.5l-77.8 96.9c-11.8 14.7-30.1 39.5-40.5 55.2l-56.7 83.9a461.89 461.89 0 0 0-26.9 44.8 203.06 203.06 0 0 0-7.9 53.1l3.2 9a127.14 127.14 0 0 0 5.4 17.2 578.09 578.09 0 0 0 46.6 49.9l196.6 181.9a52.81 52.81 0 0 1 9.7 56L425 1082a101.22 101.22 0 0 0 0 63.9l6.5 17.9a172.65 172.65 0 0 0 53.1 74.6l31.2 25.5a59.06 59.06 0 0 0 57.4 6.8l111.6-53.1a284 284 0 0 0 56.3-37.7l89.3-80.7a35.89 35.89 0 0 0 2.4-50.7l-.6-.6L678 915.5a51.14 51.14 0 0 1-12.2-54.5l51.3-127a89.25 89.25 0 0 0 0-59.9 88.69 88.69 0 0 0-44.5-40.9l-245-92.6c-17.6-6.8-16.9-13.6 2.2-15.4l127-11.8a184.69 184.69 0 0 1 66.4 8.3l126.6 45.2a40 40 0 0 1 24.8 44.8L728.3 814c-4.3 18.3-8.6 38.7-10 45.2a35.78 35.78 0 0 0 31.2 18.3l88.3 17.2a204.2 204.2 0 0 0 67.1 0l80.7-16.9a35.71 35.71 0 0 0 31.2-18.7c0-6.5-5.7-26.5-10-44.8l-47-202.7a39.36 39.36 0 0 1 24.8-44.8l126.6-45.2a184.14 184.14 0 0 1 66.4-8.3l127 11.8c18.7 0 19.7 8.6 2.2 15.4l-244.7 92.9a88.54 88.54 0 0 0-44.5 40.2 89.25 89.25 0 0 0 0 59.9l53.1 127.4a51.39 51.39 0 0 1-12.9 54.2L904 1047.9a35.77 35.77 0 0 0 1.2 50.7l.6.6 89.7 79.6a285.94 285.94 0 0 0 56.3 35.9l111.6 53.1a59.06 59.06 0 0 0 57.4-6.8l31.2-25.5a173.23 173.23 0 0 0 53.1-74.6l6.5-17.9a101.22 101.22 0 0 0 0-63.9l-34.4-74.6a52.92 52.92 0 0 1 11.8-54.9l196.2-182.2a581.78 581.78 0 0 0 46.6-50.2 123.28 123.28 0 0 0 5.1-18.7z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.4 KiB |
3
src/assets/images/circle.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375 19.1414 5" stroke="#2F80ED" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 321 B |
Before Width: | Height: | Size: 3.9 KiB |
@ -1,8 +1,11 @@
|
|||||||
<svg width="256" height="417" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<path fill="#343434" d="M127.961 0l-2.795 9.5v275.668l2.795 2.79 127.962-75.638z"/>
|
<svg width="256px" height="417px" viewBox="0 0 256 417" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
<path fill="#8C8C8C" d="M127.962 0L0 212.32l127.962 75.639V154.158z"/>
|
<g>
|
||||||
<path fill="#3C3C3B" d="M127.961 312.187l-1.575 1.92v98.199l1.575 4.6L256 236.587z"/>
|
<polygon fill="#343434" points="127.9611 0 125.1661 9.5 125.1661 285.168 127.9611 287.958 255.9231 212.32"/>
|
||||||
<path fill="#8C8C8C" d="M127.962 416.905v-104.72L0 236.585z"/>
|
<polygon fill="#8C8C8C" points="127.962 0 0 212.32 127.962 287.959 127.962 154.158"/>
|
||||||
<path fill="#141414" d="M127.961 287.958l127.96-75.637-127.96-58.162z"/>
|
<polygon fill="#3C3C3B" points="127.9611 312.1866 126.3861 314.1066 126.3861 412.3056 127.9611 416.9066 255.9991 236.5866"/>
|
||||||
<path fill="#393939" d="M0 212.32l127.96 75.638v-133.8z"/>
|
<polygon fill="#8C8C8C" points="127.962 416.9052 127.962 312.1852 0 236.5852"/>
|
||||||
</svg>
|
<polygon fill="#141414" points="127.9611 287.9577 255.9211 212.3207 127.9611 154.1587"/>
|
||||||
|
<polygon fill="#393939" points="0.0009 212.3208 127.9609 287.9578 127.9609 154.1588"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 14 KiB |
@ -1,60 +0,0 @@
|
|||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 318.6 318.6" xml:space="preserve">
|
|
||||||
<style>
|
|
||||||
.st1{fill:#e4761b;stroke:#e4761b}.st1,.st10{stroke-linecap:round;stroke-linejoin:round}.st2{fill:#763d16;stroke:#763d16}.st2,.st3,.st4,.st5,.st6,.st7,.st8{stroke-linecap:round;stroke-linejoin:round}.st3{fill:#f6851b;stroke:#f6851b}.st4{fill:#e2761b;stroke:#e2761b}.st5{fill:#cd6116;stroke:#cd6116}.st6{fill:#c0ad9e;stroke:#c0ad9e}.st7{fill:#d7c1b3;stroke:#d7c1b3}.st8{fill:#e4751f;stroke:#e4751f}.st10{fill:#161616;stroke:#161616}
|
|
||||||
</style>
|
|
||||||
<path fill="#161616" stroke="#161616" d="M277.3 145.6l-5-3.6 8-7.3-6.1-4.8 8-6.1-5.3-4 8.4-40.8-12.6-37.9-81.1 30.3h-67.5L43 41.1 30.4 79l8.5 40.8-5.4 4 8 6.1-6.1 4.8 8 7.3-5 3.6 11.5 13.5-17.4 54.2 16.1 55.3 56.7-15.6 11 9 22.4 15.5H177l22.4-15.5 11-9 56.7 15.6 16.2-55.3-17.5-54.2z"/>
|
|
||||||
<path class="st1" d="M105.3 253l-56.7 15.6-16.1-55.3zm178-39.7l-16.2 55.3-56.7-15.6z"/>
|
|
||||||
<path class="st2" d="M265.8 159.1l-52.3-15.3 18.3-4.8zm-215.9 0L84 139l18.2 4.8zM43.4 142l-1.9-12.1L84 139z"/>
|
|
||||||
<path class="st2" d="M272.3 142l-40.5-3 42.4-9.1z"/>
|
|
||||||
<path class="st2" d="M272.3 142l-6.5 17.1-34-20.1zm-228.9 0l40.6-3-34.1 20.1zm188.4-3l45.1-19.2-2.7 10.1zM84 139l-42.5-9.1-2.6-10.1z"/>
|
|
||||||
<path class="st3" d="M124.1 71.4h67.5l-15.1 41.1z"/>
|
|
||||||
<path class="st3" d="M176.5 112.5h-37.3l-15.1-41.1z"/>
|
|
||||||
<path class="st2" d="M276.9 119.8L231.8 139l-.8-51.6zm-174.7 24L84 139l.7-51.6z"/>
|
|
||||||
<path class="st2" d="M84.7 87.4L84 139l-45.1-19.2zm146.3 0l.8 51.6-18.3 4.8z"/>
|
|
||||||
<path class="st1" d="M139.2 112.5L43 41.1l81.1 30.3z"/>
|
|
||||||
<path class="st4" d="M272.7 41.1l-96.2 71.4 15.1-41.1z"/>
|
|
||||||
<path class="st1" d="M210.4 253l26.5-39.7h46.4zM32.5 213.3h46.4l26.4 39.7z"/>
|
|
||||||
<path class="st3" d="M229.3 167.7l54 45.6h-46.4zm-142.9 0l-53.9 45.6 17.4-54.2zm-7.5 45.6H32.5l53.9-45.6z"/>
|
|
||||||
<path class="st3" d="M229.3 167.7l36.5-8.6 17.5 54.2z"/>
|
|
||||||
<path class="st2" d="M84.7 87.4l54.5 25.1-37 31.3zm128.8 56.4l-37-31.3L231 87.4zm52.3 15.3l6.5-17.1 5 3.6zm-215.9 0l-11.5-13.5 5-3.6zM272.3 142l1.9-12.1 6.1 4.8zm-228.9 0l-8-7.3 6.1-4.8z"/>
|
|
||||||
<path class="st2" d="M33.5 123.8l5.4-4 2.6 10.1zm248.7 0l-8 6.1 2.7-10.1z"/>
|
|
||||||
<path class="st3" d="M49.9 159.1l52.3-15.3-15.8 23.9zm215.9 0l-36.5 8.6-15.8-23.9z"/>
|
|
||||||
<path class="st2" d="M38.9 119.8L30.4 79l54.3 8.4zM231 87.4l54.3-8.4-8.4 40.8z"/>
|
|
||||||
<path class="st1" d="M102.2 143.8l37-31.3 3.4 57.7zm111.3 0l15.8 23.9-56.2 2.5zm-40.4 26.4l3.4-57.7 37 31.3z"/>
|
|
||||||
<path class="st1" d="M142.6 170.2l-56.2-2.5 15.8-23.9z"/>
|
|
||||||
<path class="st2" d="M272.7 41.1L285.3 79 231 87.4zM43 41.1l96.2 71.4-54.5-25.1zm188 46.3l-54.5 25.1 96.2-71.4z"/>
|
|
||||||
<path class="st2" d="M84.7 87.4L30.4 79 43 41.1z"/>
|
|
||||||
<path class="st5" d="M105.3 253l-26.4-39.7 31.1.4zm105.1 0l-4.7-39.3 31.2-.4z"/>
|
|
||||||
<path class="st3" d="M173.1 170.2h-30.5l-3.4-57.7z"/>
|
|
||||||
<path class="st3" d="M139.2 112.5h37.3l-3.4 57.7z"/>
|
|
||||||
<path class="st6" d="M116.3 262l-11-9 31.5 14.9zm62.6 5.9l31.5-14.9-11 9z"/>
|
|
||||||
<path class="st7" d="M136.6 258.6l.2 9.3-31.5-14.9zm42.6 0l31.2-5.6-31.5 14.9z"/>
|
|
||||||
<path class="st3" d="M86.4 167.7l23.6 46-31.1-.4zm150.5 45.6l-31.2.4 23.6-46z"/>
|
|
||||||
<path class="st8" d="M86.4 167.7l22.8 23.1.8 22.9zm142.9 0l-23.6 46 .9-22.9z"/>
|
|
||||||
<path class="st7" d="M105.3 253l33.9-16.5-2.6 22.1zm105.1 0l-31.2 5.6-2.7-22.1z"/>
|
|
||||||
<path class="st1" d="M139.2 236.5L105.3 253l4.7-39.3zm37.3 0l29.2-22.8 4.7 39.3z"/>
|
|
||||||
<path class="st5" d="M173.1 170.2l56.2-2.5-22.7 23.1zm-63.9 20.6l-22.8-23.1 56.2 2.5z"/>
|
|
||||||
<path class="st5" d="M142.6 170.2l-13.5 11.5-19.9 9.1zm64 20.6l-20-9.1-13.5-11.5z"/>
|
|
||||||
<path class="st3" d="M205.7 213.7l-27.4-14.6 28.3-8.3zm-95.7 0l-.8-22.9 28.2 8.3z"/>
|
|
||||||
<path d="M137.4 199.1l-28.2-8.3 19.9-9.1zm40.9 0l8.3-17.4 20 9.1z" fill="#233447" stroke="#233447" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path class="st5" d="M186.6 181.7l-8.3 17.4-5.2-28.9zm-57.5 0l13.5-11.5-5.2 28.9z"/>
|
|
||||||
<path class="st6" d="M199.4 262L177 277.5l1.9-9.6zm-62.6 5.9l1.9 9.6-22.4-15.5z"/>
|
|
||||||
<path class="st4" d="M178.3 199.1l-6.5-10.7 1.3-18.2z"/>
|
|
||||||
<path class="st8" d="M137.4 199.1l5.2-28.9 1.3 18.2z"/>
|
|
||||||
<path class="st3" d="M173.1 170.2l-1.3 18.2h-27.9z"/>
|
|
||||||
<path class="st3" d="M143.9 188.4l-1.3-18.2h30.5zm34.4 10.7l27.4 14.6-29.2 22.8zm-39.1 37.4L110 213.7l27.4-14.6z"/>
|
|
||||||
<path class="st3" d="M137.4 199.1l6.6 34.1-4.8 3.3zm39.1 37.4l-4.8-3.3 6.6-34.1z"/>
|
|
||||||
<path class="st8" d="M171.8 188.4l6.5 10.7-6.6 34.1zm-27.9 0l.1 44.8-6.6-34.1z"/>
|
|
||||||
<path class="st3" d="M143.9 188.4h27.9l-.1 44.8zm27.8 44.8H144l-.1-44.8z"/>
|
|
||||||
<path class="st6" d="M179.2 258.6l-.3 9.3-1.9 9.6zm-40.5 18.9l-1.9-9.6-.2-9.3z"/>
|
|
||||||
<path class="st6" d="M136.6 258.6l2.4-2.2-.3 21.1zm40.4 18.9l-.3-21.1 2.5 2.2z"/>
|
|
||||||
<path class="st6" d="M138.7 277.5l.3-21.1h37.7zm38-21.1l.3 21.1h-38.3z"/>
|
|
||||||
<path class="st10" d="M176.5 236.5l2.7 22.1-2.5-2.2zM139 256.4l-2.4 2.2 2.6-22.1zm.2-19.9l1.5 4.7-1.7 15.2z"/>
|
|
||||||
<path class="st10" d="M176.7 256.4l-1.7-15.2 1.5-4.7zm-33-18.7l-3 3.5-1.5-4.7z"/>
|
|
||||||
<path class="st10" d="M176.5 236.5l-1.5 4.7-3-3.5z"/>
|
|
||||||
<path class="st10" d="M172 237.7l-.3-4.5 4.8 3.3zm-32.8-1.2l4.8-3.3-.3 4.5z"/>
|
|
||||||
<path class="st10" d="M171.7 233.2l.3 4.5h-28.3z"/>
|
|
||||||
<path class="st10" d="M143.7 237.7l.3-4.5h27.7zm-3 3.5H175l1.7 15.2z"/>
|
|
||||||
<path class="st10" d="M176.7 256.4H139l1.7-15.2zm-36-15.2l3-3.5H172z"/>
|
|
||||||
<path class="st10" d="M172 237.7l3 3.5h-34.3z"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 5.3 KiB |
@ -1,3 +0,0 @@
|
|||||||
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M12.1603 6C10.7795 8.39146 7.72159 9.21084 5.33013 7.83013C2.93866 6.44942 2.11929 3.39146 3.5 1" stroke="#2F80ED" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 276 B |
@ -1,3 +0,0 @@
|
|||||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 1.5H1.5V7.5H7.5V1.5ZM1.5 0H0V1.5V7.5V9H1.5H7.5H9V7.5V1.5V0H7.5H1.5ZM4.5 3H3V4.5V6H4.5H6V4.5V3H4.5ZM1.5 19.5V13.5H7.5V19.5H1.5ZM0 12H1.5H7.5H9V13.5V19.5V21H7.5H1.5H0V19.5V13.5V12ZM4.5 15H3V16.5V18H4.5H6V16.5V15H4.5ZM13.5 1.5H19.5V7.5H13.5V1.5ZM12 0H13.5H19.5H21V1.5V7.5V9H19.5H13.5H12V7.5V1.5V0ZM16.5 3H15V4.5V6H16.5H18V4.5V3H16.5ZM16.5 12H12V21H13.5V16.5H15V18H21V12H19.5V13.5H16.5V12ZM18 19.5H16.5V21H18V19.5ZM19.5 19.5H21V21H19.5V19.5Z" fill="black"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 613 B |
17
src/assets/images/spinner.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
||||||
|
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g transform="translate(1 1)" stroke-width="2">
|
||||||
|
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
|
||||||
|
<path d="M36 18c0-9.94-8.06-18-18-18">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0 18 18"
|
||||||
|
to="360 18 18"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"/>
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 694 B |
@ -1,18 +0,0 @@
|
|||||||
<svg width="288" height="288" viewBox="0 0 288 288" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>logo_solid_square_white</title>
|
|
||||||
<desc>Created using Figma</desc>
|
|
||||||
<g id="Canvas" transform="translate(60 145)">
|
|
||||||
<g id="logo_solid_square_white">
|
|
||||||
<g id="background">
|
|
||||||
<use xlink:href="#path0_fill" transform="translate(-60 -145)" fill="#3375BB"/>
|
|
||||||
</g>
|
|
||||||
<g id="logo">
|
|
||||||
<use xlink:href="#path1_stroke" transform="translate(11.2 -85.7)" fill="#FFFFFF"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<path id="path0_fill" d="M 288 0L -1.2207e-05 0L -1.2207e-05 288L 288 288L 288 0Z"/>
|
|
||||||
<path id="path1_stroke" d="M 72.4 -7.62939e-07L 76.6355 -5.46695C 74.1422 -7.39862 70.6578 -7.39862 68.1645 -5.46695L 72.4 -7.62939e-07ZM 144.9 21.3L 151.815 21.4125C 151.845 19.5591 151.13 17.7711 149.83 16.4498C 148.53 15.1284 146.754 14.3843 144.9 14.3843L 144.9 21.3ZM 72.4 169.4L 68.5688 175.157C 70.8895 176.702 73.9105 176.702 76.2313 175.157L 72.4 169.4ZM 0 21.3L 0 14.3843C -1.85365 14.3843 -3.62972 15.1284 -4.92975 16.4498C -6.22977 17.7711 -6.94493 19.5591 -6.91479 21.4125L 0 21.3ZM 68.1645 5.46695C 99.5499 29.7826 135.462 28.2157 144.9 28.2157L 144.9 14.3843C 135.138 14.3843 103.85 15.6174 76.6355 -5.46695L 68.1645 5.46695ZM 137.985 21.1875C 137.464 53.3107 136.105 75.9691 133.501 92.7573C 130.933 109.367 127.26 119.322 122.533 126.314C 117.743 133.578 111.735 137.889 102.832 143.208C 93.8514 148.587 82.5075 154.381 68.5688 163.643L 76.2313 175.157C 89.4926 166.319 100.549 160.688 109.934 155.076C 119.396 149.405 127.719 143.159 133.992 134.061C 140.327 124.69 144.448 112.476 147.17 94.8708C 149.858 77.4434 151.286 53.8393 151.815 21.4125L 137.985 21.1875ZM 76.2313 163.643C 62.2932 154.381 50.957 148.588 41.9868 143.21C 33.0938 137.892 27.0986 133.583 22.3207 126.32C 17.6046 119.329 13.9431 109.373 11.3835 92.7609C 8.7891 75.9718 7.43568 53.3117 6.91479 21.1875L -6.91479 21.4125C -6.38568 53.8383 -4.96409 77.4407 -2.2866 94.8672C 0.425666 112.47 4.53296 124.684 10.8543 134.055C 17.1139 143.154 25.425 149.402 34.8789 155.075C 44.2556 160.687 55.3068 166.319 68.5688 175.157L 76.2313 163.643ZM 0 28.2157C 9.34002 28.2157 45.2509 29.782 76.6355 5.46695L 68.1645 -5.46695C 40.9491 15.618 9.65998 14.3843 0 14.3843L 0 28.2157Z"/>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.2 KiB |
@ -1,47 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.address-input-panel {
|
|
||||||
@extend %col-nowrap;
|
|
||||||
|
|
||||||
&__input {
|
|
||||||
font-size: 1rem;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
color: $royal-blue;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $chalice-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--error {
|
|
||||||
color: $salmon-red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__recipient-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__input-container {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__qr-container {
|
|
||||||
padding: 10px;
|
|
||||||
background: $concrete-gray;
|
|
||||||
border: 1px solid $mercury-gray;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-right: 16px;
|
|
||||||
height: 21px;
|
|
||||||
|
|
||||||
& > img {
|
|
||||||
height: 21px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,73 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import classnames from 'classnames'
|
import styled from 'styled-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
|
import { lighten } from 'polished'
|
||||||
|
|
||||||
import { isAddress } from '../../utils'
|
import { isAddress } from '../../utils'
|
||||||
import { useDebounce } from '../../hooks'
|
import { useDebounce } from '../../hooks'
|
||||||
// import QrCode from '../QrCode' // commented out pending further review
|
|
||||||
|
|
||||||
import './address-input-panel.scss'
|
const InputPanel = styled.div`
|
||||||
|
${({ theme }) => theme.flexColumnNoWrap}
|
||||||
|
box-shadow: 0 4px 8px 0 ${({ theme }) => lighten(0.9, theme.royalBlue)};
|
||||||
|
position: relative;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
z-index: 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ContainerRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
box-shadow: 0 0 0 0.5px ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
transition: box-shadow 200ms ease-in-out;
|
||||||
|
`
|
||||||
|
|
||||||
|
const InputContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LabelRow = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LabelContainer = styled.div`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`
|
||||||
|
|
||||||
|
const InputRow = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25rem 0.85rem 0.75rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Input = styled.input`
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.royalBlue)};
|
||||||
|
transition: color 200ms ease-in-out;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: ${({ theme }) => theme.chaliceGray};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default function AddressInputPanel({ title, initialInput = '', onChange = () => {}, onError = () => {} }) {
|
export default function AddressInputPanel({ title, initialInput = '', onChange = () => {}, onError = () => {} }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -82,40 +142,29 @@ export default function AddressInputPanel({ title, initialInput = '', onChange =
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="currency-input-panel">
|
<InputPanel>
|
||||||
<div
|
<ContainerRow error={input !== '' && error}>
|
||||||
className={classnames('currency-input-panel__container address-input-panel__recipient-row', {
|
<InputContainer>
|
||||||
'currency-input-panel__container--error': input !== '' && error
|
<LabelRow>
|
||||||
})}
|
<LabelContainer>
|
||||||
>
|
<span>{title || t('recipientAddress')}</span>
|
||||||
<div className="address-input-panel__input-container">
|
</LabelContainer>
|
||||||
<div className="currency-input-panel__label-row">
|
</LabelRow>
|
||||||
<div className="currency-input-panel__label-container">
|
<InputRow>
|
||||||
<span className="currency-input-panel__label">{title || t('recipientAddress')}</span>
|
<Input
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="currency-input-panel__input-row">
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
className={classnames('address-input-panel__input', {
|
|
||||||
'address-input-panel__input--error': input !== '' && error
|
|
||||||
})}
|
|
||||||
placeholder="0x1234..."
|
placeholder="0x1234..."
|
||||||
|
error={input !== '' && error}
|
||||||
onChange={onInput}
|
onChange={onInput}
|
||||||
value={input}
|
value={input}
|
||||||
/>
|
/>
|
||||||
</div>
|
</InputRow>
|
||||||
</div>
|
</InputContainer>
|
||||||
{/* commented out pending further review
|
</ContainerRow>
|
||||||
<div className="address-input-panel__qr-container">
|
</InputPanel>
|
||||||
<QrCode onValueReceived={value => onChange(value)} />
|
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.contextual-info {
|
|
||||||
&__summary-wrapper {
|
|
||||||
color: $dove-gray;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--error {
|
|
||||||
color: $salmon-red;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__details {
|
|
||||||
background-color: $concrete-gray;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__open-details-container {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: $royal-blue;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 0.75rem;
|
|
||||||
width: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,38 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import c from 'classnames'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import DropdownBlue from '../../assets/images/dropdown-blue.svg'
|
import DropdownBlue from '../../assets/images/dropdown-blue.svg'
|
||||||
import DropupBlue from '../../assets/images/dropup-blue.svg'
|
import DropupBlue from '../../assets/images/dropup-blue.svg'
|
||||||
import './contextual-info.scss'
|
|
||||||
|
const SummaryWrapper = styled.div`
|
||||||
|
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.doveGray)};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SummaryWrapperContainer = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 0.75rem;
|
||||||
|
width: 0.75rem;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
class ContextualInfo extends Component {
|
class ContextualInfo extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -38,18 +66,12 @@ class ContextualInfo extends Component {
|
|||||||
const { openDetailsText, closeDetailsText, contextualInfo, isError } = this.props
|
const { openDetailsText, closeDetailsText, contextualInfo, isError } = this.props
|
||||||
|
|
||||||
if (contextualInfo) {
|
if (contextualInfo) {
|
||||||
return (
|
return <SummaryWrapper error={isError}>{contextualInfo}</SummaryWrapper>
|
||||||
<div className={c({ 'contextual-info--error': isError }, 'contextual-info__summary-wrapper')}>
|
|
||||||
<div>{contextualInfo}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<SummaryWrapperContainer
|
||||||
key="open-details"
|
|
||||||
className="contextual-info__summary-wrapper contextual-info__open-details-container"
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
return { showDetails: !prevState.showDetails }
|
return { showDetails: !prevState.showDetails }
|
||||||
@ -67,7 +89,7 @@ class ContextualInfo extends Component {
|
|||||||
<img src={DropupBlue} alt="dropup" />
|
<img src={DropupBlue} alt="dropup" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SummaryWrapperContainer>
|
||||||
{this.renderDetails()}
|
{this.renderDetails()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.contextual-info {
|
|
||||||
&__summary-wrapper {
|
|
||||||
color: $dove-gray;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--error {
|
|
||||||
color: $salmon-red;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__details {
|
|
||||||
background-color: $concrete-gray;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__open-details-container {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: $royal-blue;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 0.75rem;
|
|
||||||
width: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +1,86 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled, { css } from 'styled-components'
|
||||||
import c from 'classnames'
|
import { transparentize } from 'polished'
|
||||||
|
|
||||||
import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg'
|
import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg'
|
||||||
import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
|
import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
|
||||||
|
|
||||||
import './contextual-info.scss'
|
const SummaryWrapper = styled.div`
|
||||||
|
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.doveGray)};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
const WrappedDropup = ({ isError, ...rest }) => <Dropup {...rest} />
|
const SummaryWrapperContainer = styled.div`
|
||||||
const ColoredDropup = styled(WrappedDropup)`
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
path {
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
stroke: ${props => props.isError && props.theme.salmonRed};
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 0.75rem;
|
||||||
|
width: 0.75rem;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const WrappedDropdown = ({ isError, ...rest }) => <Dropdown {...rest} />
|
const Details = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.concreteGray};
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ErrorSpan = styled.span`
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 0.75rem;
|
||||||
|
|
||||||
|
color: ${({ isError, theme }) => isError && theme.salmonRed};
|
||||||
|
${({ slippageWarning, highSlippageWarning, theme }) =>
|
||||||
|
highSlippageWarning
|
||||||
|
? css`
|
||||||
|
color: ${theme.salmonRed};
|
||||||
|
font-weight: 600;
|
||||||
|
`
|
||||||
|
: slippageWarning &&
|
||||||
|
css`
|
||||||
|
background-color: ${transparentize(0.6, theme.warningYellow)};
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.25rem;
|
||||||
|
`}
|
||||||
|
`
|
||||||
|
|
||||||
|
const WrappedDropup = ({ isError, highSlippageWarning, ...rest }) => <Dropup {...rest} />
|
||||||
|
const ColoredDropup = styled(WrappedDropup)`
|
||||||
|
path {
|
||||||
|
stroke: ${({ isError, theme }) => isError && theme.salmonRed};
|
||||||
|
|
||||||
|
${({ highSlippageWarning, theme }) =>
|
||||||
|
highSlippageWarning &&
|
||||||
|
css`
|
||||||
|
stroke: ${theme.salmonRed};
|
||||||
|
`}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => <Dropdown {...rest} />
|
||||||
const ColoredDropdown = styled(WrappedDropdown)`
|
const ColoredDropdown = styled(WrappedDropdown)`
|
||||||
path {
|
path {
|
||||||
stroke: ${props => props.isError && props.theme.salmonRed};
|
stroke: ${({ isError, theme }) => isError && theme.salmonRed};
|
||||||
|
|
||||||
|
${({ highSlippageWarning, theme }) =>
|
||||||
|
highSlippageWarning &&
|
||||||
|
css`
|
||||||
|
stroke: ${theme.salmonRed};
|
||||||
|
`}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -27,29 +90,34 @@ export default function ContextualInfo({
|
|||||||
contextualInfo = '',
|
contextualInfo = '',
|
||||||
allowExpand = false,
|
allowExpand = false,
|
||||||
renderTransactionDetails = () => {},
|
renderTransactionDetails = () => {},
|
||||||
isError = false
|
isError = false,
|
||||||
|
slippageWarning,
|
||||||
|
highSlippageWarning
|
||||||
}) {
|
}) {
|
||||||
const [showDetails, setShowDetails] = useState(false)
|
const [showDetails, setShowDetails] = useState(false)
|
||||||
|
|
||||||
return !allowExpand ? (
|
return !allowExpand ? (
|
||||||
<div className={c({ 'contextual-info--error': isError }, 'contextual-info__summary-wrapper')}>
|
<SummaryWrapper>{contextualInfo}</SummaryWrapper>
|
||||||
<div>{contextualInfo}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<SummaryWrapperContainer onClick={() => setShowDetails(s => !s)}>
|
||||||
key="open-details"
|
|
||||||
className="contextual-info__summary-wrapper contextual-info__open-details-container"
|
|
||||||
onClick={() => setShowDetails(s => !s)}
|
|
||||||
>
|
|
||||||
<>
|
<>
|
||||||
<span className={c({ 'contextual-info--error': isError })}>
|
<ErrorSpan isError={isError} slippageWarning={slippageWarning} highSlippageWarning={highSlippageWarning}>
|
||||||
|
{(slippageWarning || highSlippageWarning) && (
|
||||||
|
<span role="img" aria-label="warning">
|
||||||
|
⚠️
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{contextualInfo ? contextualInfo : showDetails ? closeDetailsText : openDetailsText}
|
{contextualInfo ? contextualInfo : showDetails ? closeDetailsText : openDetailsText}
|
||||||
</span>
|
</ErrorSpan>
|
||||||
{showDetails ? <ColoredDropup isError={isError} /> : <ColoredDropdown isError={isError} />}
|
{showDetails ? (
|
||||||
|
<ColoredDropup isError={isError} highSlippageWarning={highSlippageWarning} />
|
||||||
|
) : (
|
||||||
|
<ColoredDropdown isError={isError} highSlippageWarning={highSlippageWarning} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
</div>
|
</SummaryWrapperContainer>
|
||||||
{showDetails && <div className="contextual-info__details">{renderTransactionDetails()}</div>}
|
{showDetails && <Details>{renderTransactionDetails()}</Details>}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,283 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.currency-input-panel {
|
|
||||||
@extend %col-nowrap;
|
|
||||||
box-shadow: 0 4px 8px 0 rgba($royal-blue, 0.1);
|
|
||||||
position: relative;
|
|
||||||
border-radius: 1.25rem;
|
|
||||||
z-index: 200;
|
|
||||||
|
|
||||||
&__container {
|
|
||||||
border-radius: 1.25rem;
|
|
||||||
box-shadow: 0 0 0 0.5px $mercury-gray;
|
|
||||||
background-color: $white;
|
|
||||||
transition: box-shadow 200ms ease-in-out;
|
|
||||||
|
|
||||||
&--error {
|
|
||||||
box-shadow: 0 0 0 0.5px $salmon-red;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
box-shadow: 0 0 0.5px 0.5px $malibu-blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--error:focus-within {
|
|
||||||
box-shadow: 0 0 0.5px 0.5px $salmon-red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label-row {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
color: $dove-gray;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label-container {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label-description {
|
|
||||||
opacity: 0.75;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__input-row {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.25rem 0.85rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__input {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
@extend %borderless-input;
|
|
||||||
&--error {
|
|
||||||
color: $salmon-red;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[type='number'] {
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-outer-spin-button,
|
|
||||||
&::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__extra-text {
|
|
||||||
&--error {
|
|
||||||
color: $salmon-red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__currency-select {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: $royal-blue;
|
|
||||||
height: 2rem;
|
|
||||||
padding: 0 1rem;
|
|
||||||
border: 1px solid $royal-blue;
|
|
||||||
border-radius: 2.5rem;
|
|
||||||
background-color: $zumthor-blue;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: rgba($zumthor-blue, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
box-shadow: 0 0 0.5px 0.5px $malibu-blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--selected {
|
|
||||||
background-color: $concrete-gray;
|
|
||||||
border-color: $mercury-gray;
|
|
||||||
color: $black;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
|
|
||||||
.currency-input-panel__dropdown-icon {
|
|
||||||
background-image: url(../../assets/images/dropdown.svg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--disabled {
|
|
||||||
.currency-input-panel__dropdown-icon {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sub-currency-select {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
line-height: 0;
|
|
||||||
background: $zumthor-blue;
|
|
||||||
border: 1px solid $royal-blue;
|
|
||||||
color: $royal-blue;
|
|
||||||
height: 2rem;
|
|
||||||
padding: 10px 50px 10px 10px;
|
|
||||||
margin-right: -40px;
|
|
||||||
border-radius: 2.5rem;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&--pending {
|
|
||||||
line-height: 0.9;
|
|
||||||
.loader {
|
|
||||||
height: 0.5rem;
|
|
||||||
width: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__dropdown-icon {
|
|
||||||
height: 1rem;
|
|
||||||
width: 0.75rem;
|
|
||||||
margin-left: 0.7rem;
|
|
||||||
background-image: url(../../assets/images/dropdown-blue.svg);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
background-position: 50% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__selected-token-logo {
|
|
||||||
margin-right: 0.4rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-modal {
|
|
||||||
background-color: $white;
|
|
||||||
position: relative;
|
|
||||||
bottom: 21rem;
|
|
||||||
width: 100%;
|
|
||||||
height: 21rem;
|
|
||||||
z-index: 2000;
|
|
||||||
border-top-left-radius: 1rem;
|
|
||||||
border-top-right-radius: 1rem;
|
|
||||||
transition: 250ms ease-in-out;
|
|
||||||
|
|
||||||
&__search-container {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
padding: 1rem;
|
|
||||||
border-bottom: 1px solid $mercury-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__search-input {
|
|
||||||
@extend %borderless-input;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__search-icon {
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-list {
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
height: 17.5rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-row {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
margin: 0.25rem 0.5rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&--selected {
|
|
||||||
background-color: $concrete-gray;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid $mercury-gray;
|
|
||||||
border-radius: 2.5rem;
|
|
||||||
|
|
||||||
.token-modal__token-label {
|
|
||||||
color: $black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--searching {
|
|
||||||
justify-content: center;
|
|
||||||
.loader {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $concrete-gray;
|
|
||||||
|
|
||||||
.token-modal__token-label {
|
|
||||||
color: $black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: darken($concrete-gray, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--no-exchange {
|
|
||||||
color: $dove-gray;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: darken($concrete-gray, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--create-exchange {
|
|
||||||
color: $white;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: $malibu-blue;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($malibu-blue, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: darken($malibu-blue, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-logo {
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-label {
|
|
||||||
color: $dove-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 768px) {
|
|
||||||
max-width: 560px;
|
|
||||||
max-height: 768px;
|
|
||||||
position: absolute;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
top: 0;
|
|
||||||
margin-top: 4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-modal-appear {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-modal-appear.modal-container-appear-active {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
@ -1,25 +1,196 @@
|
|||||||
import React, { useState, useRef, useEffect, useMemo } from 'react'
|
import React, { useState, useRef, useMemo } from 'react'
|
||||||
import { CSSTransitionGroup } from 'react-transition-group'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
|
import styled from 'styled-components'
|
||||||
import escapeStringRegex from 'escape-string-regexp'
|
import escapeStringRegex from 'escape-string-regexp'
|
||||||
|
import { lighten, darken } from 'polished'
|
||||||
|
import Tooltip from '@reach/tooltip'
|
||||||
|
import '@reach/tooltip/styles.css'
|
||||||
|
|
||||||
|
import { BorderlessInput } from '../../theme'
|
||||||
import { useTokenContract } from '../../hooks'
|
import { useTokenContract } from '../../hooks'
|
||||||
import { isAddress, calculateGasMargin } from '../../utils'
|
import { isAddress, calculateGasMargin } from '../../utils'
|
||||||
|
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||||
import Modal from '../Modal'
|
import Modal from '../Modal'
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
import SearchIcon from '../../assets/images/magnifying-glass.svg'
|
import SearchIcon from '../../assets/images/magnifying-glass.svg'
|
||||||
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
|
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
|
||||||
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens'
|
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens'
|
||||||
|
|
||||||
import './currency-panel.scss'
|
|
||||||
|
|
||||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
||||||
|
|
||||||
|
const SubCurrencySelect = styled.button`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
background: ${({ theme }) => theme.zumthorBlue};
|
||||||
|
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
line-height: 0;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 10px 50px 10px 15px;
|
||||||
|
margin-right: -40px;
|
||||||
|
border-radius: 2.5rem;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
const InputRow = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25rem 0.85rem 0.75rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Input = styled(BorderlessInput)`
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: ${({ error, theme }) => error && theme.salmonRed};
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledBorderlessInput = styled(BorderlessInput)`
|
||||||
|
min-height: 1.75rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const CurrencySelect = styled.button`
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: ${({ selected, theme }) => (selected ? theme.black : theme.royalBlue)};
|
||||||
|
height: 2rem;
|
||||||
|
border: 1px solid ${({ selected, theme }) => (selected ? theme.mercuryGray : theme.royalBlue)};
|
||||||
|
border-radius: 2.5rem;
|
||||||
|
background-color: ${({ selected, theme }) => (selected ? theme.concreteGray : theme.zumthorBlue)};
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
border: 1px solid
|
||||||
|
${({ selected, theme }) => (selected ? darken(0.1, theme.mercuryGray) : darken(0.1, theme.royalBlue))};
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
box-shadow: 0 0 0.5px 0.5px ${({ theme }) => theme.malibuBlue};
|
||||||
|
}
|
||||||
|
|
||||||
|
:active {
|
||||||
|
background-color: ${({ theme }) => theme.zumthorBlue};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Aligner = styled.span`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledDropDown = styled(DropDown)`
|
||||||
|
margin: 0 0.5rem 0 0.5rem;
|
||||||
|
height: 35%;
|
||||||
|
|
||||||
|
path {
|
||||||
|
stroke: ${({ selected, theme }) => (selected ? theme.black : theme.royalBlue)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const InputPanel = styled.div`
|
||||||
|
${({ theme }) => theme.flexColumnNoWrap}
|
||||||
|
box-shadow: 0 4px 8px 0 ${({ theme }) => lighten(0.9, theme.royalBlue)};
|
||||||
|
position: relative;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
z-index: 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
box-shadow: 0 0 0 0.5px ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
transition: box-shadow 200ms ease-in-out;
|
||||||
|
|
||||||
|
:focus-within {
|
||||||
|
box-shadow: 0 0 0.5px 0.5px ${({ theme }) => theme.malibuBlue};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LabelRow = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
span:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${({ theme }) => darken(0.2, theme.doveGray)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LabelContainer = styled.div`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ErrorSpan = styled.span`
|
||||||
|
color: ${({ error, theme }) => error && theme.salmonRed};
|
||||||
|
:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${({ error, theme }) => error && darken(0.1, theme.salmonRed)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TokenModal = styled.div`
|
||||||
|
${({ theme }) => theme.flexColumnNoWrap}
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SearchContainer = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.mercuryGray};
|
||||||
|
`
|
||||||
|
|
||||||
|
const TokenModalInfo = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin: 0.25rem 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TokenList = styled.div`
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TokenModalRow = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin: 0.25rem 0.5rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
#symbol {
|
||||||
|
color: ${({ theme }) => theme.doveGrey};
|
||||||
|
}
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
background-color: ${({ theme }) => theme.concreteGray};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledTokenName = styled.span`
|
||||||
|
margin: 0 0.25rem 0 0.25rem;
|
||||||
|
`
|
||||||
|
|
||||||
export default function CurrencyInputPanel({
|
export default function CurrencyInputPanel({
|
||||||
filteredTokens = [],
|
|
||||||
onValueChange = () => {},
|
onValueChange = () => {},
|
||||||
renderInput,
|
renderInput,
|
||||||
onCurrencySelected = () => {},
|
onCurrencySelected = () => {},
|
||||||
@ -36,7 +207,7 @@ export default function CurrencyInputPanel({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [isShowingModal, setIsShowingModal] = useState(false)
|
const [modalIsOpen, setModalIsOpen] = useState(false)
|
||||||
|
|
||||||
const tokenContract = useTokenContract(selectedTokenAddress)
|
const tokenContract = useTokenContract(selectedTokenAddress)
|
||||||
const { exchangeAddress: selectedTokenExchangeAddress } = useTokenDetails(selectedTokenAddress)
|
const { exchangeAddress: selectedTokenExchangeAddress } = useTokenDetails(selectedTokenAddress)
|
||||||
@ -44,25 +215,16 @@ export default function CurrencyInputPanel({
|
|||||||
const pendingApproval = usePendingApproval(selectedTokenAddress)
|
const pendingApproval = usePendingApproval(selectedTokenAddress)
|
||||||
|
|
||||||
const addTransaction = useTransactionAdder()
|
const addTransaction = useTransactionAdder()
|
||||||
const inputRef = useRef()
|
|
||||||
|
|
||||||
const allTokens = useAllTokenDetails()
|
const allTokens = useAllTokenDetails()
|
||||||
|
|
||||||
// manage focus on modal show
|
|
||||||
useEffect(() => {
|
|
||||||
if (inputRef.current && isShowingModal) {
|
|
||||||
inputRef.current.focus()
|
|
||||||
}
|
|
||||||
}, [isShowingModal])
|
|
||||||
|
|
||||||
function renderUnlockButton() {
|
function renderUnlockButton() {
|
||||||
if (disableUnlock || !showUnlock || selectedTokenAddress === 'ETH' || !selectedTokenAddress) {
|
if (disableUnlock || !showUnlock || selectedTokenAddress === 'ETH' || !selectedTokenAddress) {
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
if (!pendingApproval) {
|
if (!pendingApproval) {
|
||||||
return (
|
return (
|
||||||
<button
|
<SubCurrencySelect
|
||||||
className="currency-input-panel__sub-currency-select"
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const estimatedGas = await tokenContract.estimate.approve(
|
const estimatedGas = await tokenContract.estimate.approve(
|
||||||
selectedTokenExchangeAddress,
|
selectedTokenExchangeAddress,
|
||||||
@ -74,20 +236,15 @@ export default function CurrencyInputPanel({
|
|||||||
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
|
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
addTransaction(response)
|
addTransaction(response, { approval: selectedTokenAddress })
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('unlock')}
|
{t('unlock')}
|
||||||
</button>
|
</SubCurrencySelect>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return <SubCurrencySelect>{t('pending')}</SubCurrencySelect>
|
||||||
<button className="currency-input-panel__sub-currency-select currency-input-panel__sub-currency-select--pending">
|
|
||||||
<div className="loader" />
|
|
||||||
{t('pending')}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,13 +255,11 @@ export default function CurrencyInputPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="currency-input-panel__input-row">
|
<InputRow>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
className={classnames('currency-input-panel__input', {
|
error={!!errorMessage}
|
||||||
'currency-input-panel__input--error': errorMessage
|
|
||||||
})}
|
|
||||||
placeholder="0.0"
|
placeholder="0.0"
|
||||||
onChange={e => onValueChange(e.target.value)}
|
onChange={e => onValueChange(e.target.value)}
|
||||||
onKeyPress={e => {
|
onKeyPress={e => {
|
||||||
@ -119,65 +274,74 @@ export default function CurrencyInputPanel({
|
|||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
{renderUnlockButton()}
|
{renderUnlockButton()}
|
||||||
<button
|
<CurrencySelect
|
||||||
className={classnames('currency-input-panel__currency-select', {
|
selected={!!selectedTokenAddress}
|
||||||
'currency-input-panel__currency-select--selected': selectedTokenAddress,
|
|
||||||
'currency-input-panel__currency-select--disabled': disableTokenSelect
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!disableTokenSelect) {
|
if (!disableTokenSelect) {
|
||||||
setIsShowingModal(true)
|
setModalIsOpen(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedTokenAddress ? (
|
<Aligner>
|
||||||
<TokenLogo className="currency-input-panel__selected-token-logo" address={selectedTokenAddress} />
|
{selectedTokenAddress ? <TokenLogo address={selectedTokenAddress} /> : null}
|
||||||
) : null}
|
{
|
||||||
{(allTokens[selectedTokenAddress] && allTokens[selectedTokenAddress].symbol) || t('selectToken')}
|
<StyledTokenName>
|
||||||
<span className="currency-input-panel__dropdown-icon" />
|
{(allTokens[selectedTokenAddress] && allTokens[selectedTokenAddress].symbol) || t('selectToken')}
|
||||||
</button>
|
</StyledTokenName>
|
||||||
</div>
|
}
|
||||||
|
{!disableTokenSelect && <StyledDropDown selected={!!selectedTokenAddress} />}
|
||||||
|
</Aligner>
|
||||||
|
</CurrencySelect>
|
||||||
|
</InputRow>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="currency-input-panel">
|
<InputPanel>
|
||||||
<div
|
<Container error={!!errorMessage}>
|
||||||
className={classnames('currency-input-panel__container', {
|
<LabelRow>
|
||||||
'currency-input-panel__container--error': errorMessage
|
<LabelContainer>
|
||||||
})}
|
<span>{title}</span> <span>{description}</span>
|
||||||
>
|
</LabelContainer>
|
||||||
<div className="currency-input-panel__label-row">
|
|
||||||
<div className="currency-input-panel__label-container">
|
<ErrorSpan
|
||||||
<span className="currency-input-panel__label">{title}</span>
|
data-tip={'Enter max'}
|
||||||
<span className="currency-input-panel__label-description">{description}</span>
|
error={!!errorMessage}
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={classnames('currency-input-panel__extra-text', {
|
|
||||||
'currency-input-panel__extra-text--error': errorMessage
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
extraTextClickHander()
|
extraTextClickHander()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{extraText}
|
<Tooltip
|
||||||
</span>
|
label="Enter Max"
|
||||||
</div>
|
style={{
|
||||||
|
background: 'hsla(0, 0%, 0%, 0.75)',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '24px',
|
||||||
|
padding: '0.5em 1em',
|
||||||
|
marginTop: '-64px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{extraText}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</ErrorSpan>
|
||||||
|
</LabelRow>
|
||||||
{_renderInput()}
|
{_renderInput()}
|
||||||
</div>
|
</Container>
|
||||||
{!disableTokenSelect && isShowingModal && (
|
{!disableTokenSelect && (
|
||||||
<CurrencySelectModal
|
<CurrencySelectModal
|
||||||
onTokenSelect={onCurrencySelected}
|
isOpen={modalIsOpen}
|
||||||
onClose={() => {
|
onDismiss={() => {
|
||||||
setIsShowingModal(false)
|
setModalIsOpen(false)
|
||||||
}}
|
}}
|
||||||
|
onTokenSelect={onCurrencySelected}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</InputPanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CurrencySelectModal({ onClose, onTokenSelect }) {
|
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
@ -187,10 +351,10 @@ function CurrencySelectModal({ onClose, onTokenSelect }) {
|
|||||||
const tokenList = useMemo(() => {
|
const tokenList = useMemo(() => {
|
||||||
return Object.keys(allTokens)
|
return Object.keys(allTokens)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const aSymbol = allTokens[a].symbol
|
const aSymbol = allTokens[a].symbol.toLowerCase()
|
||||||
const bSymbol = allTokens[b].symbol
|
const bSymbol = allTokens[b].symbol.toLowerCase()
|
||||||
if (aSymbol === 'ETH' || bSymbol === 'ETH') {
|
if (aSymbol === 'ETH'.toLowerCase() || bSymbol === 'ETH'.toLowerCase()) {
|
||||||
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH' ? -1 : 1
|
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH'.toLowerCase() ? -1 : 1
|
||||||
} else {
|
} else {
|
||||||
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
|
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
|
||||||
}
|
}
|
||||||
@ -220,66 +384,41 @@ function CurrencySelectModal({ onClose, onTokenSelect }) {
|
|||||||
function _onTokenSelect(address) {
|
function _onTokenSelect(address) {
|
||||||
setSearchQuery('')
|
setSearchQuery('')
|
||||||
onTokenSelect(address)
|
onTokenSelect(address)
|
||||||
onClose()
|
onDismiss()
|
||||||
}
|
|
||||||
|
|
||||||
function _onClose(address) {
|
|
||||||
setSearchQuery('')
|
|
||||||
onClose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTokenList() {
|
function renderTokenList() {
|
||||||
if (isAddress(searchQuery) && exchangeAddress === undefined) {
|
if (isAddress(searchQuery) && exchangeAddress === undefined) {
|
||||||
return (
|
return <TokenModalInfo>Searching for Exchange...</TokenModalInfo>
|
||||||
<div className="token-modal__token-row token-modal__token-row--searching">
|
|
||||||
<div className="loader" />
|
|
||||||
<div>Searching for Exchange...</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) {
|
if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="token-modal__token-row token-modal__token-row--no-exchange">
|
<TokenModalInfo>{t('noExchange')}</TokenModalInfo>
|
||||||
<div>{t('noExchange')}</div>
|
<TokenModalInfo>
|
||||||
</div>
|
<Link to={`/create-exchange/${searchQuery}`}>{t('createExchange')}</Link>
|
||||||
<Link
|
</TokenModalInfo>
|
||||||
to={`/create-exchange/${searchQuery}`}
|
|
||||||
className="token-modal__token-row token-modal__token-row--create-exchange"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<div>{t('createExchange')}</div>
|
|
||||||
</Link>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filteredTokenList.length) {
|
if (!filteredTokenList.length) {
|
||||||
return (
|
return <TokenModalInfo>{t('noExchange')}</TokenModalInfo>
|
||||||
<div className="token-modal__token-row token-modal__token-row--no-exchange">
|
|
||||||
<div>{t('noExchange')}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredTokenList.map(({ address, symbol }) => {
|
return filteredTokenList.map(({ address, symbol }) => {
|
||||||
return (
|
return (
|
||||||
<div key={address} className="token-modal__token-row" onClick={() => _onTokenSelect(address)}>
|
<TokenModalRow key={address} onClick={() => _onTokenSelect(address)}>
|
||||||
<TokenLogo className="token-modal__token-logo" address={address} />
|
<TokenLogo address={address} />
|
||||||
<div className="token-modal__token-label">{symbol}</div>
|
<span id="symbol">{symbol}</span>
|
||||||
</div>
|
</TokenModalRow>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// manage focus on modal show
|
// manage focus on modal show
|
||||||
const inputRef = useRef()
|
const inputRef = useRef()
|
||||||
useEffect(() => {
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.focus()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
function onInput(event) {
|
function onInput(event) {
|
||||||
const input = event.target.value
|
const input = event.target.value
|
||||||
@ -288,29 +427,14 @@ function CurrencySelectModal({ onClose, onTokenSelect }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onClose={_onClose}>
|
<Modal isOpen={isOpen} onDismiss={onDismiss} initialFocusRef={inputRef}>
|
||||||
<CSSTransitionGroup
|
<TokenModal>
|
||||||
transitionName="token-modal"
|
<SearchContainer>
|
||||||
transitionAppear={true}
|
<StyledBorderlessInput ref={inputRef} type="text" placeholder={t('searchOrPaste')} onChange={onInput} />
|
||||||
transitionLeave={true}
|
<img src={SearchIcon} alt="search" />
|
||||||
transitionAppearTimeout={200}
|
</SearchContainer>
|
||||||
transitionLeaveTimeout={200}
|
<TokenList>{renderTokenList()}</TokenList>
|
||||||
transitionEnterTimeout={200}
|
</TokenModal>
|
||||||
>
|
|
||||||
<div className="token-modal">
|
|
||||||
<div className="token-modal__search-container">
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
placeholder={t('searchOrPaste')}
|
|
||||||
className="token-modal__search-input"
|
|
||||||
onChange={onInput}
|
|
||||||
/>
|
|
||||||
<img src={SearchIcon} className="token-modal__search-icon" alt="search" />
|
|
||||||
</div>
|
|
||||||
<div className="token-modal__token-list">{renderTokenList()}</div>
|
|
||||||
</div>
|
|
||||||
</CSSTransitionGroup>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.header {
|
|
||||||
@extend %col-nowrap;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
&__no-decoration {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__top {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
padding: 1.25rem 0.75rem;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid $concrete-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-left: 1rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
color: $wisteria-purple;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__center-group {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__navigation {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__dialog {
|
|
||||||
@extend %col-nowrap;
|
|
||||||
border-radius: 0.875rem;
|
|
||||||
border: 1px solid $mercury-gray;
|
|
||||||
margin: 1rem 0.75rem 0 0.75rem;
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
&__description {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: $dove-gray;
|
|
||||||
margin-top: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--disconnected {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__download {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 3.5rem;
|
|
||||||
width: 3.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
img + img {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 768px) {
|
|
||||||
//position: fixed;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
&__top {
|
|
||||||
border: none;
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,174 +1,58 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import styled from 'styled-components'
|
||||||
import classnames from 'classnames'
|
|
||||||
import { useWeb3Context, Connectors } from 'web3-react'
|
|
||||||
import UAParser from 'ua-parser-js'
|
|
||||||
|
|
||||||
import Logo from '../Logo'
|
import { Link } from '../../theme'
|
||||||
import CoinbaseWalletLogo from '../../assets/images/coinbase-wallet-logo.png'
|
|
||||||
import TrustLogo from '../../assets/images/trust-wallet-logo.svg'
|
|
||||||
import BraveLogo from '../../assets/images/brave-logo.svg'
|
|
||||||
import MetamaskLogo from '../../assets/images/metamask-logo.svg'
|
|
||||||
import Web3Status from '../Web3Status'
|
import Web3Status from '../Web3Status'
|
||||||
|
import { darken } from 'polished'
|
||||||
|
|
||||||
import './header.scss'
|
const HeaderElement = styled.div`
|
||||||
|
margin: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
`
|
||||||
|
|
||||||
const { Connector, InjectedConnector } = Connectors
|
const Title = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
const links = {
|
#image {
|
||||||
coinbaseWallet: {
|
font-size: 1.5rem;
|
||||||
android: 'https://play.google.com/store/apps/details?id=org.toshi',
|
margin-right: 1rem;
|
||||||
ios: 'https://itunes.apple.com/us/app/coinbase-wallet/id1278383455'
|
|
||||||
},
|
|
||||||
trust: {
|
|
||||||
android:
|
|
||||||
'https://links.trustwalletapp.com/a/key_live_lfvIpVeI9TFWxPCqwU8rZnogFqhnzs4D?&event=openURL&url=https://uniswap.exchange/swap',
|
|
||||||
ios:
|
|
||||||
'https://links.trustwalletapp.com/a/key_live_lfvIpVeI9TFWxPCqwU8rZnogFqhnzs4D?&event=openURL&url=https://uniswap.exchange/swap'
|
|
||||||
},
|
|
||||||
metamask: {
|
|
||||||
chrome: 'https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn'
|
|
||||||
},
|
|
||||||
brave: {
|
|
||||||
android: 'https://play.google.com/store/apps/details?id=com.brave.browser',
|
|
||||||
ios: 'https://itunes.apple.com/us/app/brave-browser-fast-adblocker/id1052879175'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTrustLink() {
|
|
||||||
const os = ua.getOS()
|
|
||||||
|
|
||||||
if (os.name === 'Android') {
|
|
||||||
return links.trust.android
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (os.name === 'iOS') {
|
#link {
|
||||||
return links.trust.ios
|
text-decoration-color: ${({ theme }) => theme.wisteriaPurple};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCoinbaseWalletLink() {
|
|
||||||
const os = ua.getOS()
|
|
||||||
|
|
||||||
if (os.name === 'Android') {
|
|
||||||
return links.coinbaseWallet.android
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (os.name === 'iOS') {
|
#title {
|
||||||
return links.coinbaseWallet.ios
|
display: inline;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${({ theme }) => theme.wisteriaPurple};
|
||||||
|
:hover {
|
||||||
|
color: ${({ theme }) => darken(0.2, theme.wisteriaPurple)};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
`
|
||||||
|
|
||||||
function getBraveLink() {
|
|
||||||
const os = ua.getOS()
|
|
||||||
|
|
||||||
if (os.name === 'Mac OS') {
|
|
||||||
return links.brave.ios
|
|
||||||
}
|
|
||||||
|
|
||||||
return links.brave.android
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMetamaskLink() {
|
|
||||||
return links.metamask.chrome
|
|
||||||
}
|
|
||||||
|
|
||||||
const ua = new UAParser(window.navigator.userAgent)
|
|
||||||
function isMobile() {
|
|
||||||
return ua.getDevice().type === 'mobile'
|
|
||||||
}
|
|
||||||
|
|
||||||
function BaseBlockingWarning({ title, description, children }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classnames('header__dialog', {
|
|
||||||
'header__dialog--disconnected': true
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div key="warning-title">{title}</div>
|
|
||||||
<div key="warning-desc" className="header__dialog__description">
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BlockingWarning() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const correctNetwork = process.env.REACT_APP_NETWORK_NAME || 'Main Ethereum Network'
|
|
||||||
const context = useWeb3Context()
|
|
||||||
|
|
||||||
if (context.error && context.error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
|
|
||||||
return <BaseBlockingWarning title={t('wrongNetwork')} description={t('switchNetwork', { correctNetwork })} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is an intermediate state before infura is set
|
|
||||||
if (context.error && context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.error) {
|
|
||||||
console.error(context.error)
|
|
||||||
return <BaseBlockingWarning title={t('disconnected')} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.account) {
|
|
||||||
return (
|
|
||||||
<BaseBlockingWarning
|
|
||||||
title={t('noWallet')}
|
|
||||||
description={isMobile() ? t('installWeb3MobileBrowser') : t('installMetamask')}
|
|
||||||
>
|
|
||||||
<div key="warning-logos" className="header__download">
|
|
||||||
{isMobile() ? (
|
|
||||||
<>
|
|
||||||
<img
|
|
||||||
alt="coinbase"
|
|
||||||
src={CoinbaseWalletLogo}
|
|
||||||
key="coinbase-wallet"
|
|
||||||
onClick={() => window.open(getCoinbaseWalletLink(), '_blank')}
|
|
||||||
/>
|
|
||||||
<img alt="trust" src={TrustLogo} key="trust" onClick={() => window.open(getTrustLink(), '_blank')} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<img
|
|
||||||
alt="metamask"
|
|
||||||
src={MetamaskLogo}
|
|
||||||
key="metamask"
|
|
||||||
onClick={() => window.open(getMetamaskLink(), '_blank')}
|
|
||||||
/>
|
|
||||||
<img alt="brave" src={BraveLogo} key="brave" onClick={() => window.open(getBraveLink(), '_blank')} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</BaseBlockingWarning>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const context = useWeb3Context()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header">
|
<>
|
||||||
<BlockingWarning />
|
<HeaderElement>
|
||||||
<div className={classnames('header__top')}>
|
<Title>
|
||||||
<a className="header__no-decoration" href="https://uniswap.io" target="_blank" rel="noopener noreferrer">
|
<span id="image" role="img" aria-label="Unicorn Emoji">
|
||||||
<Logo />
|
🦄
|
||||||
</a>
|
</span>
|
||||||
|
|
||||||
<div className="header__center-group">
|
<Link id="link" href="https://uniswap.io">
|
||||||
<a className="header__no-decoration" href="https://uniswap.io" target="_blank" rel="noopener noreferrer">
|
<h1 id="title">Uniswap</h1>
|
||||||
<span className="header__title">Uniswap</span>
|
</Link>
|
||||||
</a>
|
</Title>
|
||||||
</div>
|
</HeaderElement>
|
||||||
|
|
||||||
<Web3Status isConnected={!!(context.active && context.account)} />
|
<HeaderElement>
|
||||||
</div>
|
<Web3Status />
|
||||||
</div>
|
</HeaderElement>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import './logo.scss'
|
|
||||||
|
|
||||||
export default function Logo(props) {
|
|
||||||
return (
|
|
||||||
<div className="logo">
|
|
||||||
<span role="img" aria-label="logo">
|
|
||||||
🦄
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
.logo {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
}
|
|
@ -1,52 +1,70 @@
|
|||||||
import React, { Component } from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import styled, { css } from 'styled-components'
|
||||||
import PropTypes from 'prop-types'
|
import { animated, useTransition } from 'react-spring'
|
||||||
import { CSSTransitionGroup } from 'react-transition-group'
|
import { DialogOverlay, DialogContent } from '@reach/dialog'
|
||||||
import './modal.scss'
|
import '@reach/dialog/styles.css'
|
||||||
|
|
||||||
const modalRoot = document.querySelector('#modal-root')
|
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||||
|
const StyledDialogOverlay = styled(AnimatedDialogOverlay).attrs({
|
||||||
export default class Modal extends Component {
|
suppressClassNameWarning: true
|
||||||
static propTypes = {
|
})`
|
||||||
onClose: PropTypes.func.isRequired
|
&[data-reach-dialog-overlay] {
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
|
||||||
componentDidMount() {
|
const FilteredDialogContent = ({ minHeight, ...rest }) => <DialogContent {...rest} />
|
||||||
// The portal element is inserted in the DOM tree after
|
const StyledDialogContent = styled(FilteredDialogContent)`
|
||||||
// the Modal's children are mounted, meaning that children
|
&[data-reach-dialog-content] {
|
||||||
// will be mounted on a detached DOM node. If a child
|
margin: 0 0 2rem 0;
|
||||||
// component requires to be attached to the DOM tree
|
${({ theme }) => theme.mediaWidth.upToMedium`margin: 0;`}
|
||||||
// immediately when mounted, for example to measure a
|
padding: 0;
|
||||||
// DOM node, or uses 'autoFocus' in a descendant, add
|
width: 50vw;
|
||||||
// state to Modal and only render the children when Modal
|
max-width: 650px;
|
||||||
// is inserted in the DOM tree.
|
${({ theme }) => theme.mediaWidth.upToMedium`width: 75vw;`}
|
||||||
// modalRoot.style.display = 'block';
|
${({ theme }) => theme.mediaWidth.upToSmall`width: 90vw;`}
|
||||||
// modalRoot.appendChild(this.el);
|
${({ minHeight }) =>
|
||||||
|
minHeight &&
|
||||||
|
css`
|
||||||
|
min-height: ${minHeight}vh;
|
||||||
|
`}
|
||||||
|
max-height: 50vh;
|
||||||
|
${({ theme }) => theme.mediaHeight.upToMedium`max-height: 75vh;`}
|
||||||
|
${({ theme }) => theme.mediaHeight.upToSmall`max-height: 90vh;`}
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 1.5rem;
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
|
||||||
componentWillUnmount() {
|
const HiddenCloseButton = styled.button`
|
||||||
setTimeout(() => {
|
margin: 0;
|
||||||
// modalRoot.style.display = 'none';
|
padding: 0;
|
||||||
// modalRoot.removeChild(this.el);
|
width: 0;
|
||||||
}, 500)
|
height: 0;
|
||||||
}
|
border: none;
|
||||||
|
`
|
||||||
|
|
||||||
render() {
|
export default function Modal({ isOpen, onDismiss, minHeight = 50, initialFocusRef, children }) {
|
||||||
return ReactDOM.createPortal(
|
const transitions = useTransition(isOpen, null, {
|
||||||
<div>
|
config: { duration: 125 },
|
||||||
<CSSTransitionGroup
|
from: { opacity: 0 },
|
||||||
transitionName="modal-container"
|
enter: { opacity: 1 },
|
||||||
transitionAppear={true}
|
leave: { opacity: 0 }
|
||||||
transitionLeave={true}
|
})
|
||||||
transitionAppearTimeout={200}
|
|
||||||
transitionLeaveTimeout={200}
|
return transitions.map(
|
||||||
transitionEnterTimeout={200}
|
({ item, key, props }) =>
|
||||||
>
|
item && (
|
||||||
<div className="modal-container" onClick={this.props.onClose} key="modal" />
|
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||||
</CSSTransitionGroup>
|
<StyledDialogContent hidden={true} minHeight={minHeight}>
|
||||||
{this.props.children}
|
<HiddenCloseButton onClick={onDismiss} />
|
||||||
</div>,
|
{children}
|
||||||
modalRoot
|
</StyledDialogContent>
|
||||||
)
|
</StyledDialogOverlay>
|
||||||
}
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
position: relative;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
background-color: rgba($black, 0.6);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container-appear {
|
|
||||||
opacity: 0.01;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container-appear.modal-container-appear-active {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 200ms ease-in-out;
|
|
||||||
}
|
|
@ -1,10 +1,10 @@
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { withRouter, NavLink } from 'react-router-dom'
|
import { withRouter, NavLink } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { transparentize, darken } from 'polished'
|
||||||
|
|
||||||
import { useBodyKeyDown } from '../../hooks'
|
import { useBodyKeyDown } from '../../hooks'
|
||||||
|
|
||||||
import './navigation-tabs.scss'
|
|
||||||
import { useBetaMessageManager } from '../../contexts/Application'
|
import { useBetaMessageManager } from '../../contexts/Application'
|
||||||
|
|
||||||
const tabOrder = [
|
const tabOrder = [
|
||||||
@ -25,6 +25,80 @@ const tabOrder = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const BetaMessage = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid ${({ theme }) => transparentize(0.6, theme.wisteriaPurple)};
|
||||||
|
background-color: ${({ theme }) => transparentize(0.9, theme.wisteriaPurple)};
|
||||||
|
border-radius: 2rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: ${({ theme }) => theme.wisteriaPurple};
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '✕';
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 1rem;
|
||||||
|
position: absolute;
|
||||||
|
color: ${({ theme }) => theme.wisteriaPurple};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Tabs = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
height: 2.5rem;
|
||||||
|
background-color: ${({ theme }) => theme.concreteGray};
|
||||||
|
border-radius: 3rem;
|
||||||
|
box-shadow: 0 0 0 1px ${({ theme }) => darken(0.05, theme.concreteGray)};
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const activeClassName = 'ACTIVE'
|
||||||
|
|
||||||
|
const StyledNavLink = styled(NavLink).attrs({
|
||||||
|
activeClassName
|
||||||
|
})`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 2.5rem;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
border-radius: 3rem;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
&.${activeClassName} {
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
border-radius: 3rem;
|
||||||
|
box-shadow: 0 0 0.5px 1px ${({ theme }) => theme.mercuryGray};
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
:hover {
|
||||||
|
box-shadow: 0 0 0.5px 1px ${({ theme }) => darken(0.1, theme.mercuryGray)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:hover,
|
||||||
|
:focus {
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
function NavigationTabs({ location: { pathname }, history }) {
|
function NavigationTabs({ location: { pathname }, history }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -49,26 +123,20 @@ function NavigationTabs({ location: { pathname }, history }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="tabs">
|
<Tabs>
|
||||||
{tabOrder.map(({ path, textKey, regex }) => (
|
{tabOrder.map(({ path, textKey, regex }) => (
|
||||||
<NavLink
|
<StyledNavLink key={path} to={path} isActive={(_, { pathname }) => pathname.match(regex)}>
|
||||||
key={path}
|
|
||||||
to={path}
|
|
||||||
className="tab"
|
|
||||||
activeClassName="tab--selected"
|
|
||||||
isActive={(_, { pathname }) => pathname.match(regex)}
|
|
||||||
>
|
|
||||||
{t(textKey)}
|
{t(textKey)}
|
||||||
</NavLink>
|
</StyledNavLink>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Tabs>
|
||||||
{showBetaMessage && (
|
{showBetaMessage && (
|
||||||
<div className="beta-message" onClick={dismissBetaMessage}>
|
<BetaMessage onClick={dismissBetaMessage}>
|
||||||
<span role="img" aria-label="warning">
|
<span role="img" aria-label="warning">
|
||||||
💀
|
💀
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{t('betaWarning')}
|
{t('betaWarning')}
|
||||||
</div>
|
</BetaMessage>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
@import '../../variables';
|
|
||||||
|
|
||||||
.beta-message {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
cursor: pointer;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border: 1px solid rgba($wisteria-purple, 0.4);
|
|
||||||
background-color: rgba($wisteria-purple, 0.1);
|
|
||||||
border-radius: 2rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
color: $wisteria-purple;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: '✕';
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 1rem;
|
|
||||||
position: absolute;
|
|
||||||
color: $wisteria-purple;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
height: 2.5rem;
|
|
||||||
background-color: $concrete-gray;
|
|
||||||
border-radius: 3rem;
|
|
||||||
box-shadow: 0 0 0 0.5px darken($concrete-gray, 5);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 2.5rem;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
border-radius: 3rem;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
color: $dove-gray;
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
&--selected {
|
|
||||||
background-color: $white;
|
|
||||||
border-radius: 3rem;
|
|
||||||
box-shadow: 0 0 0.5px 0.5px $mercury-gray;
|
|
||||||
font-weight: 500;
|
|
||||||
color: $royal-blue;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,39 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import './oversized-panel.scss'
|
const Panel = styled.div`
|
||||||
|
position: relative;
|
||||||
|
background-color: ${({ theme }) => theme.concreteGray};
|
||||||
|
width: calc(100% - 1rem);
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
`
|
||||||
|
|
||||||
export default function OversizedPanel(props) {
|
const PanelTop = styled.div`
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -0.5rem;
|
||||||
|
left: 0;
|
||||||
|
height: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
background-color: ${({ theme }) => theme.concreteGray};
|
||||||
|
`
|
||||||
|
|
||||||
|
const PanelBottom = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 80%;
|
||||||
|
left: 0;
|
||||||
|
height: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
background-color: ${({ theme }) => theme.concreteGray};
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function OversizedPanel({ hideTop, hideBottom, children }) {
|
||||||
return (
|
return (
|
||||||
<div className="oversized-panel">
|
<Panel>
|
||||||
{props.hideTop || <div className="oversized-panel__top" />}
|
{hideTop || <PanelTop />}
|
||||||
{props.children}
|
{children}
|
||||||
{props.hideBottom || <div className="oversized-panel__bottom" />}
|
{hideBottom || <PanelBottom />}
|
||||||
</div>
|
</Panel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.oversized-panel {
|
|
||||||
position: relative;
|
|
||||||
background-color: $concrete-gray;
|
|
||||||
width: calc(100% - 1rem);
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 0.625rem;
|
|
||||||
|
|
||||||
&__top {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -0.5rem;
|
|
||||||
left: 0;
|
|
||||||
height: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
background-color: $concrete-gray;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__bottom {
|
|
||||||
position: absolute;
|
|
||||||
top: 80%;
|
|
||||||
left: 0;
|
|
||||||
height: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
background-color: $concrete-gray;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { CSSTransitionGroup } from 'react-transition-group'
|
|
||||||
|
|
||||||
import Modal from '../Modal'
|
|
||||||
import QrCodeSVG from '../../assets/images/qr-code.svg'
|
|
||||||
import QrScanner from '../../libraries/qr-scanner'
|
|
||||||
import './qr-code.scss'
|
|
||||||
|
|
||||||
class QrCode extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
onValueReceived: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onValueReceived() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
videoOpen: false,
|
|
||||||
stream: null
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
const { videoOpen, stream } = this.state
|
|
||||||
|
|
||||||
if (videoOpen && !stream && this.videoRef) {
|
|
||||||
this.startStreamingVideo(this.videoRef)
|
|
||||||
} else if (!videoOpen && stream) {
|
|
||||||
this.setState({ stream: null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startStreamingVideo(videoRef) {
|
|
||||||
if (navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
||||||
navigator.mediaDevices
|
|
||||||
.getUserMedia({ video: { facingMode: 'user' }, audio: false })
|
|
||||||
.then(stream => {
|
|
||||||
videoRef.srcObject = stream
|
|
||||||
new QrScanner(videoRef, val => {
|
|
||||||
this.closeVideo()
|
|
||||||
this.props.onValueReceived(val)
|
|
||||||
})
|
|
||||||
this.setState({
|
|
||||||
stream: stream.getTracks()[0]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.closeVideo()
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openVideo = () => {
|
|
||||||
this.setState({ videoOpen: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
closeVideo = () => {
|
|
||||||
if (this.state.stream) {
|
|
||||||
this.state.stream.stop()
|
|
||||||
}
|
|
||||||
this.setState({ videoOpen: false, stream: null })
|
|
||||||
this.videoRef = null
|
|
||||||
}
|
|
||||||
|
|
||||||
setVideoRef = element => {
|
|
||||||
this.videoRef = element
|
|
||||||
}
|
|
||||||
|
|
||||||
renderQrReader() {
|
|
||||||
if (this.state.videoOpen) {
|
|
||||||
return (
|
|
||||||
<Modal key="modal" onClose={this.closeVideo}>
|
|
||||||
<CSSTransitionGroup
|
|
||||||
transitionName="qr-modal"
|
|
||||||
transitionAppear={true}
|
|
||||||
transitionLeave={true}
|
|
||||||
transitionAppearTimeout={200}
|
|
||||||
transitionLeaveTimeout={200}
|
|
||||||
transitionEnterTimeout={200}
|
|
||||||
>
|
|
||||||
<div className="qr-code__modal">
|
|
||||||
<video playsInline muted autoPlay ref={this.setVideoRef} className="qr-code__video" />
|
|
||||||
</div>
|
|
||||||
</CSSTransitionGroup>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return [
|
|
||||||
<img
|
|
||||||
key="icon"
|
|
||||||
src={QrCodeSVG}
|
|
||||||
alt="code"
|
|
||||||
onClick={() => {
|
|
||||||
this.state.videoOpen ? this.closeVideo() : this.openVideo()
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
this.renderQrReader()
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QrCode
|
|
@ -1,15 +0,0 @@
|
|||||||
.qr-code {
|
|
||||||
&__video {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__modal {
|
|
||||||
z-index: 2000;
|
|
||||||
position: absolute;
|
|
||||||
top: 100px;
|
|
||||||
bottom: 100px;
|
|
||||||
right: 100px;
|
|
||||||
left: 100px;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +1,55 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import styled from 'styled-components'
|
||||||
import EthereumLogo from '../../assets/images/ethereum-logo.svg'
|
|
||||||
|
|
||||||
const RINKEBY_TOKEN_MAP = {
|
import { ReactComponent as EthereumLogo } from '../../assets/images/ethereum-logo.svg'
|
||||||
'0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
|
|
||||||
'0x2448eE2641d78CC42D7AD76498917359D961A783': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
|
|
||||||
'0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85': '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2',
|
|
||||||
'0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0xd26114cd6ee289accf82350c8d8487fedb8a0c07',
|
|
||||||
'0xF22e3F33768354c9805d046af3C0926f27741B43': '0xe41d2489571d322189246dafa5ebde1f4699f498'
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/tokens'
|
const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/tokens'
|
||||||
const BAD_IMAGES = {}
|
const BAD_IMAGES = {}
|
||||||
export default class TokenLogo extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
address: PropTypes.string,
|
|
||||||
size: PropTypes.string,
|
|
||||||
className: PropTypes.string
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
const Image = styled.img`
|
||||||
address: '',
|
width: ${({ size }) => size};
|
||||||
size: '1rem',
|
height: ${({ size }) => size};
|
||||||
className: ''
|
border-radius: 1rem;
|
||||||
}
|
`
|
||||||
|
|
||||||
state = {
|
const Emoji = styled.span`
|
||||||
error: false
|
width: ${({ size }) => size};
|
||||||
}
|
font-size: ${({ size }) => size};
|
||||||
|
`
|
||||||
|
|
||||||
render() {
|
const StyledEthereumLogo = styled(EthereumLogo)`
|
||||||
const { address, size, className } = this.props
|
width: ${({ size }) => size};
|
||||||
// let path = GenericTokenLogo;
|
height: ${({ size }) => size};
|
||||||
let path = ''
|
`
|
||||||
const mainAddress = RINKEBY_TOKEN_MAP[address] ? RINKEBY_TOKEN_MAP[address] : address
|
|
||||||
|
|
||||||
if (mainAddress === 'ETH') {
|
export default function TokenLogo({ address, size = '1rem', ...rest }) {
|
||||||
path = EthereumLogo
|
const [error, setError] = useState(false)
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.error && !BAD_IMAGES[mainAddress] && mainAddress !== 'ETH') {
|
|
||||||
path = `${TOKEN_ICON_API}/${mainAddress.toLowerCase()}.png`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
return (
|
|
||||||
<div className={className} style={{ width: size, fontSize: size }}>
|
|
||||||
<span role="img" aria-label="thinking">
|
|
||||||
🤔
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let path = ''
|
||||||
|
if (address === 'ETH') {
|
||||||
|
return <StyledEthereumLogo size={size} />
|
||||||
|
} else if (!error && !BAD_IMAGES[address]) {
|
||||||
|
path = `${TOKEN_ICON_API}/${address.toLowerCase()}.png`
|
||||||
|
} else {
|
||||||
return (
|
return (
|
||||||
<img
|
<Emoji {...rest}>
|
||||||
alt="images"
|
<span role="img" aria-label="Thinking">
|
||||||
src={path}
|
🤔
|
||||||
className={className}
|
</span>
|
||||||
style={{
|
</Emoji>
|
||||||
width: size,
|
|
||||||
height: size
|
|
||||||
}}
|
|
||||||
onError={() => {
|
|
||||||
this.setState({ error: true })
|
|
||||||
BAD_IMAGES[mainAddress] = true
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
{...rest}
|
||||||
|
alt={address}
|
||||||
|
src={path}
|
||||||
|
size={size}
|
||||||
|
onError={() => {
|
||||||
|
BAD_IMAGES[address] = true
|
||||||
|
setError(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
44
src/components/WalletModal/Copy.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { useCopyClipboard } from '../../hooks'
|
||||||
|
|
||||||
|
import { Link } from '../../theme'
|
||||||
|
import { CheckCircle, Copy } from 'react-feather'
|
||||||
|
|
||||||
|
const CopyIcon = styled(Link)`
|
||||||
|
color: ${({ theme }) => theme.silverGray};
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
:hover,
|
||||||
|
:active,
|
||||||
|
:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const TransactionStatusText = styled.span`
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function CopyHelper({ toCopy }) {
|
||||||
|
const [isCopied, setCopied] = useCopyClipboard()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CopyIcon onClick={() => setCopied(toCopy)}>
|
||||||
|
{isCopied ? (
|
||||||
|
<TransactionStatusText>
|
||||||
|
<CheckCircle size={'16'} />
|
||||||
|
<TransactionStatusText>Copied</TransactionStatusText>
|
||||||
|
</TransactionStatusText>
|
||||||
|
) : (
|
||||||
|
<TransactionStatusText>
|
||||||
|
<Copy size={'16'} />
|
||||||
|
</TransactionStatusText>
|
||||||
|
)}
|
||||||
|
</CopyIcon>
|
||||||
|
)
|
||||||
|
}
|
96
src/components/WalletModal/Info.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { useWeb3Context } from 'web3-react'
|
||||||
|
|
||||||
|
import { getEtherscanLink } from '../../utils'
|
||||||
|
import { Link, Spinner } from '../../theme'
|
||||||
|
import Copy from './Copy'
|
||||||
|
|
||||||
|
import { Check } from 'react-feather'
|
||||||
|
import Circle from '../../assets/images/circle.svg'
|
||||||
|
|
||||||
|
import { transparentize } from 'polished'
|
||||||
|
|
||||||
|
const TransactionStatusWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TransactionWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
a {
|
||||||
|
/* flex: 1 1 auto; */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TransactionStatusText = styled.span`
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TransactionState = styled.div`
|
||||||
|
background-color: ${({ pending, theme }) =>
|
||||||
|
pending ? transparentize(0.95, theme.royalBlue) : transparentize(0.95, theme.connectedGreen)};
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: ${({ pending, theme }) =>
|
||||||
|
pending ? transparentize(0.75, theme.royalBlue) : transparentize(0.75, theme.connectedGreen)};
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
border-color: ${({ pending, theme }) =>
|
||||||
|
pending ? transparentize(0, theme.royalBlue) : transparentize(0, theme.connectedGreen)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ButtonWrapper = styled.div`
|
||||||
|
a {
|
||||||
|
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.connectedGreen)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function Info({ hash, pending }) {
|
||||||
|
const { networkId } = useWeb3Context()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransactionWrapper key={hash}>
|
||||||
|
<TransactionStatusWrapper>
|
||||||
|
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>{hash} ↗ </Link>
|
||||||
|
<Copy />
|
||||||
|
</TransactionStatusWrapper>
|
||||||
|
{pending ? (
|
||||||
|
<ButtonWrapper pending={pending}>
|
||||||
|
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
|
||||||
|
<TransactionState pending={pending}>
|
||||||
|
<Spinner src={Circle} alt="loader" />
|
||||||
|
<TransactionStatusText>Pending</TransactionStatusText>
|
||||||
|
</TransactionState>
|
||||||
|
</Link>
|
||||||
|
</ButtonWrapper>
|
||||||
|
) : (
|
||||||
|
<ButtonWrapper pending={pending}>
|
||||||
|
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
|
||||||
|
<TransactionState pending={pending}>
|
||||||
|
<Check size="16" />
|
||||||
|
<TransactionStatusText>Confirmed</TransactionStatusText>
|
||||||
|
</TransactionState>
|
||||||
|
</Link>
|
||||||
|
</ButtonWrapper>
|
||||||
|
)}
|
||||||
|
</TransactionWrapper>
|
||||||
|
)
|
||||||
|
}
|
107
src/components/WalletModal/Transaction.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled, { keyframes } from 'styled-components'
|
||||||
|
import { useWeb3Context } from 'web3-react'
|
||||||
|
import Copy from './Copy'
|
||||||
|
|
||||||
|
import { getEtherscanLink } from '../../utils'
|
||||||
|
import { Link, Spinner } from '../../theme'
|
||||||
|
import Circle from '../../assets/images/circle.svg'
|
||||||
|
import { Check } from 'react-feather'
|
||||||
|
|
||||||
|
import { transparentize } from 'polished'
|
||||||
|
|
||||||
|
const TransactionStatusWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TransactionWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
a {
|
||||||
|
/* flex: 1 1 auto; */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TransactionStatusText = styled.span`
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const rotate = keyframes`
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TransactionState = styled.div`
|
||||||
|
display: flex;
|
||||||
|
background-color: ${({ pending, theme }) =>
|
||||||
|
pending ? transparentize(0.95, theme.royalBlue) : transparentize(0.95, theme.connectedGreen)};
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: ${({ pending, theme }) =>
|
||||||
|
pending ? transparentize(0.75, theme.royalBlue) : transparentize(0.75, theme.connectedGreen)};
|
||||||
|
|
||||||
|
#pending {
|
||||||
|
animation: 2s ${rotate} linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
border-color: ${({ pending, theme }) =>
|
||||||
|
pending ? transparentize(0, theme.royalBlue) : transparentize(0, theme.connectedGreen)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const ButtonWrapper = styled.div`
|
||||||
|
a {
|
||||||
|
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.connectedGreen)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function Transaction({ hash, pending }) {
|
||||||
|
const { networkId } = useWeb3Context()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransactionWrapper key={hash}>
|
||||||
|
<TransactionStatusWrapper>
|
||||||
|
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>{hash} ↗ </Link>
|
||||||
|
<Copy toCopy={hash} />
|
||||||
|
</TransactionStatusWrapper>
|
||||||
|
{pending ? (
|
||||||
|
<ButtonWrapper pending={pending}>
|
||||||
|
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
|
||||||
|
<TransactionState pending={pending}>
|
||||||
|
<Spinner src={Circle} id="pending" />
|
||||||
|
<TransactionStatusText>Pending</TransactionStatusText>
|
||||||
|
</TransactionState>
|
||||||
|
</Link>
|
||||||
|
</ButtonWrapper>
|
||||||
|
) : (
|
||||||
|
<ButtonWrapper pending={pending}>
|
||||||
|
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
|
||||||
|
<TransactionState pending={pending}>
|
||||||
|
<Check size="16" />
|
||||||
|
<TransactionStatusText>Confirmed</TransactionStatusText>
|
||||||
|
</TransactionState>
|
||||||
|
</Link>
|
||||||
|
</ButtonWrapper>
|
||||||
|
)}
|
||||||
|
</TransactionWrapper>
|
||||||
|
)
|
||||||
|
}
|
208
src/components/WalletModal/index.js
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled, { css } from 'styled-components'
|
||||||
|
import { useWeb3Context } from 'web3-react'
|
||||||
|
|
||||||
|
import Transaction from './Transaction'
|
||||||
|
import Copy from './Copy'
|
||||||
|
import Modal from '../Modal'
|
||||||
|
|
||||||
|
import { getEtherscanLink } from '../../utils'
|
||||||
|
import { Link } from '../../theme'
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
${({ theme }) => theme.flexColumnNoWrap}
|
||||||
|
`
|
||||||
|
|
||||||
|
const UpperSection = styled.div`
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: ${({ theme }) => theme.concreteGray};
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5:last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const YourAccount = styled.div`
|
||||||
|
h5 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-weight: 400;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LowerSection = styled.div`
|
||||||
|
${({ theme }) => theme.flexColumnNoWrap}
|
||||||
|
padding: 2rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
}
|
||||||
|
|
||||||
|
div:last-child {
|
||||||
|
/* margin-bottom: 0; */
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const AccountControl = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
${({ hasENS, isENS }) =>
|
||||||
|
hasENS &&
|
||||||
|
isENS &&
|
||||||
|
css`
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
`}
|
||||||
|
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? css`500` : css`400`) : css`500`)};
|
||||||
|
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? css`1rem` : css`0.8rem`) : css`1rem`)};
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TransactionListWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexColumnNoWrap} /* margin: 0 0 1rem 0; */
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledLink = styled(Link)`
|
||||||
|
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.royalBlue : theme.doveGray) : theme.royalBlue)};
|
||||||
|
`
|
||||||
|
|
||||||
|
// function getErrorMessage(event) {
|
||||||
|
// switch (event.code) {
|
||||||
|
// case InjectedConnector.errorCodes.ETHEREUM_ACCESS_DENIED: {
|
||||||
|
// return 'Permission Required'
|
||||||
|
// }
|
||||||
|
// case InjectedConnector.errorCodes.UNLOCK_REQUIRED: {
|
||||||
|
// return 'Account Unlock Required'
|
||||||
|
// }
|
||||||
|
// case InjectedConnector.errorCodes.NO_WEB3: {
|
||||||
|
// return 'Not a Web3 Browser'
|
||||||
|
// }
|
||||||
|
// default: {
|
||||||
|
// return 'Connection Error'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default function WalletModal({ isOpen, error, onDismiss, pendingTransactions, confirmedTransactions, ENSName }) {
|
||||||
|
const { account, networkId } = useWeb3Context()
|
||||||
|
|
||||||
|
function renderTransactions(transactions, pending) {
|
||||||
|
return (
|
||||||
|
<TransactionListWrapper>
|
||||||
|
{transactions.map((hash, i) => {
|
||||||
|
return <Transaction key={i} hash={hash} pending={pending} />
|
||||||
|
})}
|
||||||
|
</TransactionListWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrappedOnDismiss() {
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWalletDisplay() {
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UpperSection>
|
||||||
|
<h4>Wrong Network</h4>
|
||||||
|
<h5>Please connect to the main Ethereum network.</h5>
|
||||||
|
</UpperSection>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
} else if (account) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UpperSection>
|
||||||
|
<YourAccount>
|
||||||
|
<h5>Your Account</h5>
|
||||||
|
{ENSName && (
|
||||||
|
<AccountControl hasENS={!!ENSName} isENS={true}>
|
||||||
|
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(networkId, ENSName, 'address')}>
|
||||||
|
{ENSName} ↗{' '}
|
||||||
|
</StyledLink>
|
||||||
|
|
||||||
|
<Copy toCopy={ENSName} />
|
||||||
|
</AccountControl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||||
|
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(networkId, account, 'address')}>
|
||||||
|
{account} ↗{' '}
|
||||||
|
</StyledLink>
|
||||||
|
|
||||||
|
<Copy toCopy={account} />
|
||||||
|
</AccountControl>
|
||||||
|
</YourAccount>
|
||||||
|
</UpperSection>
|
||||||
|
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
|
||||||
|
<LowerSection>
|
||||||
|
<h5>Recent Transactions</h5>
|
||||||
|
{renderTransactions(pendingTransactions, true)}
|
||||||
|
{renderTransactions(confirmedTransactions, false)}
|
||||||
|
</LowerSection>
|
||||||
|
) : (
|
||||||
|
<LowerSection>
|
||||||
|
<h5>Your transactions will appear here...</h5>
|
||||||
|
</LowerSection>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UpperSection>
|
||||||
|
<h4>No Ethereum account found</h4>
|
||||||
|
<h5>Please visit this page in a Web3 enabled browser.</h5>
|
||||||
|
<h5>
|
||||||
|
<Link href={'https://ethereum.org/use/#_3-what-is-a-wallet-and-which-one-should-i-use'}>
|
||||||
|
Learn more ↗
|
||||||
|
</Link>
|
||||||
|
</h5>
|
||||||
|
</UpperSection>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} minHeight={null}>
|
||||||
|
<Wrapper>{getWalletDisplay()}</Wrapper>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
104
src/components/Web3ReactManager/index.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useWeb3Context, Connectors } from 'web3-react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { ethers } from 'ethers'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { isMobile } from 'react-device-detect'
|
||||||
|
|
||||||
|
import { Spinner } from '../../theme'
|
||||||
|
import Circle from '../../assets/images/circle.svg'
|
||||||
|
|
||||||
|
const { Connector } = Connectors
|
||||||
|
|
||||||
|
const MessageWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 20rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Message = styled.h2`
|
||||||
|
color: ${({ theme }) => theme.uniswapPink};
|
||||||
|
`
|
||||||
|
|
||||||
|
const SpinnerWrapper = styled(Spinner)`
|
||||||
|
font-size: 4rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
path {
|
||||||
|
color: ${({ theme }) => theme.uniswapPink};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
function tryToSetConnector(setConnector, setError) {
|
||||||
|
setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
|
||||||
|
setConnector('Network')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Web3ReactManager({ children }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { active, error, setConnector, setError } = useWeb3Context()
|
||||||
|
// control whether or not we render the error, after parsing
|
||||||
|
const blockRender = error && error.code && error.code === Connector.errorCodes.UNSUPPORTED_NETWORK
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active && !error) {
|
||||||
|
if (window.ethereum || window.web3) {
|
||||||
|
if (isMobile) {
|
||||||
|
tryToSetConnector(setConnector, setError)
|
||||||
|
} else {
|
||||||
|
const library = new ethers.providers.Web3Provider(window.ethereum || window.web3)
|
||||||
|
library.listAccounts().then(accounts => {
|
||||||
|
if (accounts.length >= 1) {
|
||||||
|
tryToSetConnector(setConnector, setError)
|
||||||
|
} else {
|
||||||
|
setConnector('Network')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setConnector('Network')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [active, error, setConnector, setError])
|
||||||
|
|
||||||
|
// parse the error
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
// if the user changes to the wrong network, unset the connector
|
||||||
|
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
|
||||||
|
setConnector('Network')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [error, setConnector])
|
||||||
|
|
||||||
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setShowLoader(true)
|
||||||
|
}, 600)
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (blockRender) {
|
||||||
|
return null
|
||||||
|
} else if (error) {
|
||||||
|
return (
|
||||||
|
<MessageWrapper>
|
||||||
|
<Message>{t('unknownError')}</Message>
|
||||||
|
</MessageWrapper>
|
||||||
|
)
|
||||||
|
} else if (!active) {
|
||||||
|
return showLoader ? (
|
||||||
|
<MessageWrapper>
|
||||||
|
<SpinnerWrapper src={Circle} />
|
||||||
|
</MessageWrapper>
|
||||||
|
) : null
|
||||||
|
} else {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
}
|
@ -1,126 +1,283 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useReducer, useEffect, useRef } from 'react'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import styled from 'styled-components'
|
||||||
import classnames from 'classnames'
|
|
||||||
import Jazzicon from 'jazzicon'
|
|
||||||
import { CSSTransitionGroup } from 'react-transition-group'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useWeb3Context, Connectors } from 'web3-react'
|
||||||
|
import { darken, transparentize } from 'polished'
|
||||||
|
import Jazzicon from 'jazzicon'
|
||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
|
import { Activity, ArrowRight } from 'react-feather'
|
||||||
|
|
||||||
import Modal from '../Modal'
|
import { shortenAddress } from '../../utils'
|
||||||
|
import { useENSName } from '../../hooks'
|
||||||
|
import WalletModal from '../WalletModal'
|
||||||
import { useAllTransactions } from '../../contexts/Transactions'
|
import { useAllTransactions } from '../../contexts/Transactions'
|
||||||
|
import { Spinner } from '../../theme'
|
||||||
|
import Circle from '../../assets/images/circle.svg'
|
||||||
|
|
||||||
import './web3-status.scss'
|
const { Connector } = Connectors
|
||||||
|
|
||||||
function getEtherscanLink(tx) {
|
const Web3StatusGeneric = styled.button`
|
||||||
return `https://etherscan.io/tx/${tx}`
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
}
|
width: 100%;
|
||||||
|
font-size: 0.9rem;
|
||||||
function getPendingText(pendingTransactions, pendingLabel) {
|
align-items: center;
|
||||||
return (
|
padding: 0.5rem;
|
||||||
<div className="web3-status__pending-container">
|
border-radius: 2rem;
|
||||||
<div className="loader" />
|
box-sizing: border-box;
|
||||||
<span key="text">
|
cursor: pointer;
|
||||||
{pendingTransactions.length} {pendingLabel}
|
user-select: none;
|
||||||
</span>
|
:focus {
|
||||||
</div>
|
outline: none;
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getText(text, disconnectedText) {
|
|
||||||
if (!text || text.length < 42 || !ethers.utils.isHexString(text)) {
|
|
||||||
return disconnectedText
|
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
const Web3StatusError = styled(Web3StatusGeneric)`
|
||||||
|
background-color: ${({ theme }) => theme.salmonRed};
|
||||||
|
color: ${({ theme }) => theme.white};
|
||||||
|
border: 1px solid ${({ theme }) => theme.salmonRed};
|
||||||
|
font-weight: 500;
|
||||||
|
:hover,
|
||||||
|
:focus {
|
||||||
|
background-color: ${({ theme }) => darken(0.1, theme.salmonRed)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const address = ethers.utils.getAddress(text)
|
const Web3StatusConnect = styled(Web3StatusGeneric)`
|
||||||
return `${address.substring(0, 6)}...${address.substring(38)}`
|
background-color: ${({ theme }) => theme.royalBlue};
|
||||||
|
color: ${({ theme }) => theme.white};
|
||||||
|
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||||
|
font-weight: 500;
|
||||||
|
:hover,
|
||||||
|
:focus {
|
||||||
|
background-color: ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Web3StatusConnected = styled(Web3StatusGeneric)`
|
||||||
|
background-color: ${({ pending, theme }) => (pending ? theme.zumthorBlue : theme.white)};
|
||||||
|
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.doveGray)};
|
||||||
|
border: 1px solid ${({ pending, theme }) => (pending ? theme.royalBlue : theme.mercuryGray)};
|
||||||
|
font-weight: 400;
|
||||||
|
:hover {
|
||||||
|
background-color: ${({ pending, theme }) =>
|
||||||
|
pending ? transparentize(0.9, theme.royalBlue) : transparentize(0.9, theme.mercuryGray)};
|
||||||
|
}
|
||||||
|
:focus {
|
||||||
|
border: 1px solid
|
||||||
|
${({ pending, theme }) => (pending ? darken(0.1, theme.royalBlue) : darken(0.1, theme.mercuryGray))};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Text = styled.p`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
margin: 0 0.5rem 0 0.25rem;
|
||||||
|
font-size: 0.83rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Identicon = styled.div`
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
border-radius: 1.125rem;
|
||||||
|
background-color: ${({ theme }) => theme.silverGray};
|
||||||
|
`
|
||||||
|
|
||||||
|
const NetworkIcon = styled(Activity)`
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ArrowIcon = styled(ArrowRight)`
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SpinnerWrapper = styled(Spinner)`
|
||||||
|
margin: 0 0.25rem 0 0.25rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const walletModalInitialState = {
|
||||||
|
open: false,
|
||||||
|
error: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const WALLET_MODAL_ERROR = 'WALLET_MODAL_ERROR'
|
||||||
|
const WALLET_MODAL_OPEN = 'WALLET_MODAL_OPEN'
|
||||||
|
const WALLET_MODAL_OPEN_ERROR = 'WALLET_MODAL_OPEN_ERROR'
|
||||||
|
const WALLET_MODAL_CLOSE = 'WALLET_MODAL_CLOSE'
|
||||||
|
|
||||||
|
function walletModalReducer(state, { type, payload }) {
|
||||||
|
switch (type) {
|
||||||
|
case WALLET_MODAL_ERROR: {
|
||||||
|
const { error } = payload
|
||||||
|
return { ...state, error }
|
||||||
|
}
|
||||||
|
case WALLET_MODAL_OPEN: {
|
||||||
|
return { ...state, open: true }
|
||||||
|
}
|
||||||
|
case WALLET_MODAL_OPEN_ERROR: {
|
||||||
|
const { error } = payload || {}
|
||||||
|
return { open: true, error }
|
||||||
|
}
|
||||||
|
case WALLET_MODAL_CLOSE: {
|
||||||
|
return { ...state, open: false }
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw Error(`Unexpected action type in walletModalReducer reducer: '${type}'.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Web3Status() {
|
export default function Web3Status() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { active, account } = useWeb3Context()
|
const { active, account, connectorName, setConnector } = useWeb3Context()
|
||||||
|
|
||||||
|
const ENSName = useENSName(account)
|
||||||
|
|
||||||
const allTransactions = useAllTransactions()
|
const allTransactions = useAllTransactions()
|
||||||
const pending = Object.keys(allTransactions).filter(hash => !allTransactions[hash].receipt)
|
const pending = Object.keys(allTransactions).filter(hash => !allTransactions[hash].receipt)
|
||||||
const confirmed = Object.keys(allTransactions).filter(hash => allTransactions[hash].receipt)
|
const confirmed = Object.keys(allTransactions).filter(hash => allTransactions[hash].receipt)
|
||||||
|
|
||||||
const hasPendingTransactions = !!pending.length
|
const hasPendingTransactions = !!pending.length
|
||||||
const hasConfirmedTransactions = !!confirmed.length
|
|
||||||
|
|
||||||
const [isShowingModal, setIsShowingModal] = useState(false)
|
const [{ open: walletModalIsOpen, error: walletModalError }, dispatch] = useReducer(
|
||||||
|
walletModalReducer,
|
||||||
|
walletModalInitialState
|
||||||
|
)
|
||||||
|
function setError(error) {
|
||||||
|
dispatch({ type: WALLET_MODAL_ERROR, payload: { error } })
|
||||||
|
}
|
||||||
|
function openWalletModal(error) {
|
||||||
|
dispatch({ type: WALLET_MODAL_OPEN, ...(error ? { payload: { error } } : {}) })
|
||||||
|
}
|
||||||
|
function closeWalletModal() {
|
||||||
|
dispatch({ type: WALLET_MODAL_CLOSE })
|
||||||
|
}
|
||||||
|
|
||||||
function handleClick() {
|
// janky logic to detect log{ins,outs}...
|
||||||
if (pending.length && !isShowingModal) {
|
useEffect(() => {
|
||||||
setIsShowingModal(true)
|
// if the injected connector is not active...
|
||||||
|
const { ethereum } = window
|
||||||
|
if (connectorName !== 'Injected') {
|
||||||
|
if (connectorName === 'Network' && ethereum && ethereum.on && ethereum.removeListener) {
|
||||||
|
function tryToActivateInjected() {
|
||||||
|
const library = new ethers.providers.Web3Provider(window.ethereum)
|
||||||
|
// if calling enable won't pop an approve modal, then try to activate injected...
|
||||||
|
library.listAccounts().then(accounts => {
|
||||||
|
if (accounts.length >= 1) {
|
||||||
|
setConnector('Injected', { suppressAndThrowErrors: true })
|
||||||
|
.then(() => {
|
||||||
|
setError()
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// ...and if the error is that they're on the wrong network, display it, otherwise eat it
|
||||||
|
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
|
||||||
|
setError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ethereum.on('networkChanged', tryToActivateInjected)
|
||||||
|
ethereum.on('accountsChanged', tryToActivateInjected)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (ethereum.removeListener) {
|
||||||
|
ethereum.removeListener('networkChanged', tryToActivateInjected)
|
||||||
|
ethereum.removeListener('accountsChanged', tryToActivateInjected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ...poll to check the accounts array, and if it's ever 0 i.e. the user logged out, update the connector
|
||||||
|
if (ethereum) {
|
||||||
|
const accountPoll = setInterval(() => {
|
||||||
|
const library = new ethers.providers.Web3Provider(ethereum)
|
||||||
|
library.listAccounts().then(accounts => {
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
setConnector('Network')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 750)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(accountPoll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [connectorName, setConnector])
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
if (walletModalError) {
|
||||||
|
openWalletModal()
|
||||||
|
} else if (connectorName === 'Network' && (window.ethereum || window.web3)) {
|
||||||
|
setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
|
||||||
|
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
|
||||||
|
setError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
openWalletModal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPendingTransactions() {
|
const ref = useRef()
|
||||||
return pending.map(transaction => {
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.innerHTML = ''
|
||||||
|
if (account) {
|
||||||
|
ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [account, walletModalError])
|
||||||
|
|
||||||
|
function getWeb3Status() {
|
||||||
|
if (walletModalError) {
|
||||||
|
// this is ok because we're guaranteed that the error is a wrong network error
|
||||||
return (
|
return (
|
||||||
<div
|
<Web3StatusError onClick={onClick}>
|
||||||
key={transaction}
|
<NetworkIcon />
|
||||||
className={classnames('pending-modal__transaction-row')}
|
<Text>Wrong Network</Text>
|
||||||
onClick={() => window.open(getEtherscanLink(transaction), '_blank')}
|
</Web3StatusError>
|
||||||
>
|
)
|
||||||
<div className="pending-modal__transaction-label">{transaction}</div>
|
} else if (!account) {
|
||||||
<div className="pending-modal__pending-indicator">
|
return (
|
||||||
<div className="loader" /> {t('pending')}
|
<Web3StatusConnect onClick={onClick}>
|
||||||
</div>
|
<Text>{t('Connect')}</Text>
|
||||||
</div>
|
<ArrowIcon />
|
||||||
|
</Web3StatusConnect>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Web3StatusConnected onClick={onClick} pending={hasPendingTransactions}>
|
||||||
|
{hasPendingTransactions && <SpinnerWrapper src={Circle} alt="loader" />}
|
||||||
|
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||||
|
<Identicon ref={ref} />
|
||||||
|
</Web3StatusConnected>
|
||||||
)
|
)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderModal() {
|
|
||||||
if (!isShowingModal) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal onClose={() => setIsShowingModal(false)}>
|
|
||||||
<CSSTransitionGroup
|
|
||||||
transitionName="token-modal"
|
|
||||||
transitionAppear={true}
|
|
||||||
transitionLeave={true}
|
|
||||||
transitionAppearTimeout={200}
|
|
||||||
transitionLeaveTimeout={200}
|
|
||||||
transitionEnterTimeout={200}
|
|
||||||
>
|
|
||||||
<div className="pending-modal">
|
|
||||||
<div className="pending-modal__transaction-list">
|
|
||||||
<div className="pending-modal__header">Transactions</div>
|
|
||||||
{renderPendingTransactions()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CSSTransitionGroup>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
active && (
|
||||||
className={classnames('web3-status', {
|
<>
|
||||||
'web3-status__connected': active,
|
{getWeb3Status()}
|
||||||
'web3-status--pending': hasPendingTransactions,
|
<WalletModal
|
||||||
'web3-status--confirmed': hasConfirmedTransactions
|
isOpen={walletModalIsOpen}
|
||||||
})}
|
error={walletModalError}
|
||||||
onClick={handleClick}
|
onDismiss={closeWalletModal}
|
||||||
>
|
ENSName={ENSName}
|
||||||
<div className="web3-status__text">
|
pendingTransactions={pending}
|
||||||
{hasPendingTransactions ? getPendingText(pending, t('pending')) : getText(account, t('disconnected'))}
|
confirmedTransactions={confirmed}
|
||||||
</div>
|
/>
|
||||||
<div
|
</>
|
||||||
className="web3-status__identicon"
|
)
|
||||||
ref={el => {
|
|
||||||
if (!el || !account) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
el.innerHTML = ''
|
|
||||||
el.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{renderModal()}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.web3-status {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
height: 2rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid $mercury-gray;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 2rem;
|
|
||||||
color: $dove-gray;
|
|
||||||
font-weight: 400;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&__identicon {
|
|
||||||
height: 1rem;
|
|
||||||
width: 1rem;
|
|
||||||
border-radius: 1.125rem;
|
|
||||||
background-color: $silver-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pending-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--pending {
|
|
||||||
background-color: $zumthor-blue;
|
|
||||||
color: $royal-blue;
|
|
||||||
border: 1px solid $royal-blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pending-modal {
|
|
||||||
background-color: $white;
|
|
||||||
position: relative;
|
|
||||||
bottom: 12rem;
|
|
||||||
height: 12rem;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 2000;
|
|
||||||
border-top-left-radius: 1rem;
|
|
||||||
border-top-right-radius: 1rem;
|
|
||||||
transition: 250ms ease-in-out;
|
|
||||||
|
|
||||||
&__transaction-list {
|
|
||||||
height: 12rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__transaction-row {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
box-sizing: border-box;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.25rem 1.5rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__transaction-label {
|
|
||||||
color: $royal-blue;
|
|
||||||
width: 60%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-weight: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pending-indicator {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
color: $royal-blue;
|
|
||||||
border: 1px solid $royal-blue;
|
|
||||||
background-color: $zumthor-blue;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border-radius: 100px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
|
|
||||||
& > .loading {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: $dove-gray;
|
|
||||||
padding: 1rem 1.5rem 0.25rem 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pending-modal-appear {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pending-modal-appear.modal-container-appear-active {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
@ -1,4 +1,6 @@
|
|||||||
export const FACTORY_ADDRESSES = {
|
export const FACTORY_ADDRESSES = {
|
||||||
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
|
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
|
||||||
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
|
3: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
|
||||||
|
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
|
||||||
|
42: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
|
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
|
|
||||||
import { safeAccess, isAddress, getTokenAllowance } from '../utils'
|
import { safeAccess, isAddress, getTokenAllowance } from '../utils'
|
||||||
@ -46,9 +46,11 @@ export default function Provider({ children }) {
|
|||||||
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, spenderAddress, value, blockNumber } })
|
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, spenderAddress, value, blockNumber } })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const contextValue = useMemo(() => [state, { update }], [state, update])
|
return (
|
||||||
|
<AllowancesContext.Provider value={useMemo(() => [state, { update }], [state, update])}>
|
||||||
return <AllowancesContext.Provider value={contextValue}>{children}</AllowancesContext.Provider>
|
{children}
|
||||||
|
</AllowancesContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAddressAllowance(address, tokenAddress, spenderAddress) {
|
export function useAddressAllowance(address, tokenAddress, spenderAddress) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
|
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
import { safeAccess } from '../utils'
|
import { safeAccess } from '../utils'
|
||||||
|
|
||||||
@ -51,13 +51,17 @@ export default function Provider({ children }) {
|
|||||||
dispatch({ type: UPDATE_BLOCK_NUMBER, payload: { networkId, blockNumber } })
|
dispatch({ type: UPDATE_BLOCK_NUMBER, payload: { networkId, blockNumber } })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const contextValue = useMemo(() => [state, { dismissBetaMessage, updateBlockNumber }], [
|
return (
|
||||||
state,
|
<ApplicationContext.Provider
|
||||||
dismissBetaMessage,
|
value={useMemo(() => [state, { dismissBetaMessage, updateBlockNumber }], [
|
||||||
updateBlockNumber
|
state,
|
||||||
])
|
dismissBetaMessage,
|
||||||
|
updateBlockNumber
|
||||||
return <ApplicationContext.Provider value={contextValue}>{children}</ApplicationContext.Provider>
|
])}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ApplicationContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Updater() {
|
export function Updater() {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
|
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
|
|
||||||
import { safeAccess, isAddress, getEtherBalance, getTokenBalance } from '../utils'
|
import { safeAccess, isAddress, getEtherBalance, getTokenBalance } from '../utils'
|
||||||
@ -44,9 +44,11 @@ export default function Provider({ children }) {
|
|||||||
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, value, blockNumber } })
|
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, value, blockNumber } })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const contextValue = useMemo(() => [state, { update }], [state, update])
|
return (
|
||||||
|
<BalancesContext.Provider value={useMemo(() => [state, { update }], [state, update])}>
|
||||||
return <BalancesContext.Provider value={contextValue}>{children}</BalancesContext.Provider>
|
{children}
|
||||||
|
</BalancesContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAddressBalance(address, tokenAddress) {
|
export function useAddressBalance(address, tokenAddress) {
|
||||||
@ -66,14 +68,13 @@ export function useAddressBalance(address, tokenAddress) {
|
|||||||
library
|
library
|
||||||
) {
|
) {
|
||||||
let stale = false
|
let stale = false
|
||||||
|
|
||||||
;(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
|
;(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
|
||||||
.then(value => {
|
.then(value => {
|
||||||
if (!stale) {
|
if (!stale) {
|
||||||
update(networkId, address, tokenAddress, value, globalBlockNumber)
|
update(networkId, address, tokenAddress, value, globalBlockNumber)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(() => {
|
||||||
if (!stale) {
|
if (!stale) {
|
||||||
update(networkId, address, tokenAddress, null, globalBlockNumber)
|
update(networkId, address, tokenAddress, null, globalBlockNumber)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
|
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
|
|
||||||
@ -18,14 +18,17 @@ const EXCHANGE_ADDRESS = 'exchangeAddress'
|
|||||||
|
|
||||||
const UPDATE = 'UPDATE'
|
const UPDATE = 'UPDATE'
|
||||||
|
|
||||||
|
const ETH = {
|
||||||
|
ETH: {
|
||||||
|
[NAME]: 'Ethereum',
|
||||||
|
[SYMBOL]: 'ETH',
|
||||||
|
[DECIMALS]: 18,
|
||||||
|
[EXCHANGE_ADDRESS]: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const INITIAL_TOKENS_CONTEXT = {
|
const INITIAL_TOKENS_CONTEXT = {
|
||||||
1: {
|
1: {
|
||||||
ETH: {
|
|
||||||
[NAME]: 'Ethereum',
|
|
||||||
[SYMBOL]: 'ETH',
|
|
||||||
[DECIMALS]: 18,
|
|
||||||
[EXCHANGE_ADDRESS]: null
|
|
||||||
},
|
|
||||||
'0x960b236A07cf122663c4303350609A66A7B288C0': {
|
'0x960b236A07cf122663c4303350609A66A7B288C0': {
|
||||||
[NAME]: 'Aragon Network Token',
|
[NAME]: 'Aragon Network Token',
|
||||||
[SYMBOL]: 'ANT',
|
[SYMBOL]: 'ANT',
|
||||||
@ -56,6 +59,12 @@ const INITIAL_TOKENS_CONTEXT = {
|
|||||||
[DECIMALS]: 18,
|
[DECIMALS]: 18,
|
||||||
[EXCHANGE_ADDRESS]: '0xF7B5A4b934658025390ff69dB302BC7F2AC4a542'
|
[EXCHANGE_ADDRESS]: '0xF7B5A4b934658025390ff69dB302BC7F2AC4a542'
|
||||||
},
|
},
|
||||||
|
'0xF5DCe57282A584D2746FaF1593d3121Fcac444dC': {
|
||||||
|
[NAME]: 'Compound Dai',
|
||||||
|
[SYMBOL]: 'cDAI',
|
||||||
|
[DECIMALS]: 8,
|
||||||
|
[EXCHANGE_ADDRESS]: '0x45A2FDfED7F7a2c791fb1bdF6075b83faD821ddE'
|
||||||
|
},
|
||||||
'0x41e5560054824eA6B0732E656E3Ad64E20e94E45': {
|
'0x41e5560054824eA6B0732E656E3Ad64E20e94E45': {
|
||||||
[NAME]: 'Civic',
|
[NAME]: 'Civic',
|
||||||
[SYMBOL]: 'CVC',
|
[SYMBOL]: 'CVC',
|
||||||
@ -98,12 +107,6 @@ const INITIAL_TOKENS_CONTEXT = {
|
|||||||
[DECIMALS]: 12,
|
[DECIMALS]: 12,
|
||||||
[EXCHANGE_ADDRESS]: '0x4B17685b330307C751B47f33890c8398dF4Fe407'
|
[EXCHANGE_ADDRESS]: '0x4B17685b330307C751B47f33890c8398dF4Fe407'
|
||||||
},
|
},
|
||||||
'0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd': {
|
|
||||||
[NAME]: 'Gemini dollar',
|
|
||||||
[SYMBOL]: 'GUSD',
|
|
||||||
[DECIMALS]: 2,
|
|
||||||
[EXCHANGE_ADDRESS]: '0xD883264737Ed969d2696eE4B4cAF529c2Fc2A141'
|
|
||||||
},
|
|
||||||
'0x818Fc6C2Ec5986bc6E2CBf00939d90556aB12ce5': {
|
'0x818Fc6C2Ec5986bc6E2CBf00939d90556aB12ce5': {
|
||||||
[NAME]: 'Kin',
|
[NAME]: 'Kin',
|
||||||
[SYMBOL]: 'KIN',
|
[SYMBOL]: 'KIN',
|
||||||
@ -122,6 +125,12 @@ const INITIAL_TOKENS_CONTEXT = {
|
|||||||
[DECIMALS]: 18,
|
[DECIMALS]: 18,
|
||||||
[EXCHANGE_ADDRESS]: '0xF173214C720f58E03e194085B1DB28B50aCDeeaD'
|
[EXCHANGE_ADDRESS]: '0xF173214C720f58E03e194085B1DB28B50aCDeeaD'
|
||||||
},
|
},
|
||||||
|
'0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD': {
|
||||||
|
[NAME]: 'LoopringCoin V2',
|
||||||
|
[SYMBOL]: 'LRC',
|
||||||
|
[DECIMALS]: 18,
|
||||||
|
[EXCHANGE_ADDRESS]: '0xA539BAaa3aCA455c986bB1E25301CEF936CE1B65'
|
||||||
|
},
|
||||||
'0x6c6EE5e31d828De241282B9606C8e98Ea48526E2': {
|
'0x6c6EE5e31d828De241282B9606C8e98Ea48526E2': {
|
||||||
[NAME]: 'HoloToken',
|
[NAME]: 'HoloToken',
|
||||||
[SYMBOL]: 'HOT',
|
[SYMBOL]: 'HOT',
|
||||||
@ -230,11 +239,11 @@ const INITIAL_TOKENS_CONTEXT = {
|
|||||||
[DECIMALS]: 18,
|
[DECIMALS]: 18,
|
||||||
[EXCHANGE_ADDRESS]: '0x1aEC8F11A7E78dC22477e91Ed924Fab46e3A88Fd'
|
[EXCHANGE_ADDRESS]: '0x1aEC8F11A7E78dC22477e91Ed924Fab46e3A88Fd'
|
||||||
},
|
},
|
||||||
'0xEf8a2c1BC94e630463293F71bF5414d13e80F62D': {
|
'0x5a4aDe4f3E934a0885f42884F7077261C3F4f66F': {
|
||||||
[NAME]: 'Synthetix Network Token',
|
[NAME]: 'Synthetix Network Token',
|
||||||
[SYMBOL]: 'SNX',
|
[SYMBOL]: 'SNX',
|
||||||
[DECIMALS]: 18,
|
[DECIMALS]: 18,
|
||||||
[EXCHANGE_ADDRESS]: '0xd9025Ed64BAA7B9046E37fe94670C79fcCB2b5C8'
|
[EXCHANGE_ADDRESS]: '0x8Da198A049426bFCf1522B0Dc52f84beDa6e38FF'
|
||||||
},
|
},
|
||||||
'0x42d6622deCe394b54999Fbd73D108123806f6a18': {
|
'0x42d6622deCe394b54999Fbd73D108123806f6a18': {
|
||||||
[NAME]: 'SPANK',
|
[NAME]: 'SPANK',
|
||||||
@ -290,12 +299,6 @@ const INITIAL_TOKENS_CONTEXT = {
|
|||||||
[DECIMALS]: 18,
|
[DECIMALS]: 18,
|
||||||
[EXCHANGE_ADDRESS]: '0x8dE0d002DC83478f479dC31F76cB0a8aa7CcEa17'
|
[EXCHANGE_ADDRESS]: '0x8dE0d002DC83478f479dC31F76cB0a8aa7CcEa17'
|
||||||
},
|
},
|
||||||
'0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27': {
|
|
||||||
[NAME]: 'Zilliqa',
|
|
||||||
[SYMBOL]: 'ZIL',
|
|
||||||
[DECIMALS]: 12,
|
|
||||||
[EXCHANGE_ADDRESS]: '0x7dc095A5CF7D6208CC680fA9866F80a53911041a'
|
|
||||||
},
|
|
||||||
'0xE41d2489571d322189246DaFA5ebDe1F4699F498': {
|
'0xE41d2489571d322189246DaFA5ebDe1F4699F498': {
|
||||||
[NAME]: '0x Protocol Token',
|
[NAME]: '0x Protocol Token',
|
||||||
[SYMBOL]: 'ZRX',
|
[SYMBOL]: 'ZRX',
|
||||||
@ -341,17 +344,20 @@ export default function Provider({ children }) {
|
|||||||
dispatch({ type: UPDATE, payload: { networkId, tokenAddress, name, symbol, decimals, exchangeAddress } })
|
dispatch({ type: UPDATE, payload: { networkId, tokenAddress, name, symbol, decimals, exchangeAddress } })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const contextValue = useMemo(() => [state, { update }], [state, update])
|
return (
|
||||||
|
<TokensContext.Provider value={useMemo(() => [state, { update }], [state, update])}>
|
||||||
return <TokensContext.Provider value={contextValue}>{children}</TokensContext.Provider>
|
{children}
|
||||||
|
</TokensContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTokenDetails(tokenAddress) {
|
export function useTokenDetails(tokenAddress) {
|
||||||
const { networkId, library } = useWeb3Context()
|
const { networkId, library } = useWeb3Context()
|
||||||
|
|
||||||
const [state, { update }] = useTokensContext()
|
const [state, { update }] = useTokensContext()
|
||||||
|
const allTokensInNetwork = { ...ETH, ...(safeAccess(state, [networkId]) || {}) }
|
||||||
const { [NAME]: name, [SYMBOL]: symbol, [DECIMALS]: decimals, [EXCHANGE_ADDRESS]: exchangeAddress } =
|
const { [NAME]: name, [SYMBOL]: symbol, [DECIMALS]: decimals, [EXCHANGE_ADDRESS]: exchangeAddress } =
|
||||||
safeAccess(state, [networkId, tokenAddress]) || {}
|
safeAccess(allTokensInNetwork, [tokenAddress]) || {}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -390,7 +396,7 @@ export function useAllTokenDetails(requireExchange = true) {
|
|||||||
const { networkId } = useWeb3Context()
|
const { networkId } = useWeb3Context()
|
||||||
|
|
||||||
const [state] = useTokensContext()
|
const [state] = useTokensContext()
|
||||||
const tokenDetails = safeAccess(state, [networkId]) || {}
|
const tokenDetails = { ...ETH, ...(safeAccess(state, [networkId]) || {}) }
|
||||||
|
|
||||||
return requireExchange
|
return requireExchange
|
||||||
? Object.keys(tokenDetails)
|
? Object.keys(tokenDetails)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
|
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
import { ethers } from 'ethers'
|
|
||||||
|
|
||||||
import { safeAccess } from '../utils'
|
import { safeAccess } from '../utils'
|
||||||
import { useBlockNumber } from './Application'
|
import { useBlockNumber } from './Application'
|
||||||
|
|
||||||
const RESPONSE = 'response'
|
const RESPONSE = 'response'
|
||||||
|
const CUSTOM_DATA = 'CUSTOM_DATA'
|
||||||
const BLOCK_NUMBER_CHECKED = 'BLOCK_NUMBER_CHECKED'
|
const BLOCK_NUMBER_CHECKED = 'BLOCK_NUMBER_CHECKED'
|
||||||
const RECEIPT = 'receipt'
|
const RECEIPT = 'receipt'
|
||||||
|
|
||||||
@ -93,9 +93,13 @@ export default function Provider({ children }) {
|
|||||||
dispatch({ type: FINALIZE, payload: { networkId, hash, receipt } })
|
dispatch({ type: FINALIZE, payload: { networkId, hash, receipt } })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const contextValue = useMemo(() => [state, { add, check, finalize }], [state, add, check, finalize])
|
return (
|
||||||
|
<TransactionsContext.Provider
|
||||||
return <TransactionsContext.Provider value={contextValue}>{children}</TransactionsContext.Provider>
|
value={useMemo(() => [state, { add, check, finalize }], [state, add, check, finalize])}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TransactionsContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Updater() {
|
export function Updater() {
|
||||||
@ -145,7 +149,7 @@ export function useTransactionAdder() {
|
|||||||
const [, { add }] = useTransactionsContext()
|
const [, { add }] = useTransactionsContext()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
response => {
|
(response, customData = {}) => {
|
||||||
if (!(networkId || networkId === 0)) {
|
if (!(networkId || networkId === 0)) {
|
||||||
throw Error(`Invalid networkId '${networkId}`)
|
throw Error(`Invalid networkId '${networkId}`)
|
||||||
}
|
}
|
||||||
@ -155,7 +159,7 @@ export function useTransactionAdder() {
|
|||||||
if (!hash) {
|
if (!hash) {
|
||||||
throw Error('No transaction hash found.')
|
throw Error('No transaction hash found.')
|
||||||
}
|
}
|
||||||
add(networkId, hash, response)
|
add(networkId, hash, { ...response, [CUSTOM_DATA]: customData })
|
||||||
},
|
},
|
||||||
[networkId, add]
|
[networkId, add]
|
||||||
)
|
)
|
||||||
@ -178,12 +182,7 @@ export function usePendingApproval(tokenAddress) {
|
|||||||
return false
|
return false
|
||||||
} else if (!allTransactions[hash][RESPONSE]) {
|
} else if (!allTransactions[hash][RESPONSE]) {
|
||||||
return false
|
return false
|
||||||
} else if (allTransactions[hash][RESPONSE].to !== tokenAddress) {
|
} else if (allTransactions[hash][RESPONSE][CUSTOM_DATA].approval !== tokenAddress) {
|
||||||
return false
|
|
||||||
} else if (
|
|
||||||
allTransactions[hash][RESPONSE].data.substring(0, 10) !==
|
|
||||||
ethers.utils.id('approve(address,uint256)').substring(0, 10)
|
|
||||||
) {
|
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { useState, useMemo, useCallback, useEffect } from 'react'
|
import { useState, useMemo, useCallback, useEffect } from 'react'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
|
|
||||||
import ERC20_ABI from '../abi/erc20'
|
import ERC20_ABI from '../constants/abis/erc20'
|
||||||
import { getContract, getFactoryContract, getExchangeContract } from '../utils'
|
import { getContract, getFactoryContract, getExchangeContract, isAddress } from '../utils'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
// modified from https://usehooks.com/useDebounce/
|
// modified from https://usehooks.com/useDebounce/
|
||||||
export function useDebounce(value, delay) {
|
export function useDebounce(value, delay) {
|
||||||
@ -28,8 +29,13 @@ export function useDebounce(value, delay) {
|
|||||||
// modified from https://usehooks.com/useKeyPress/
|
// modified from https://usehooks.com/useKeyPress/
|
||||||
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) {
|
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) {
|
||||||
const downHandler = useCallback(
|
const downHandler = useCallback(
|
||||||
({ target: { tagName }, key }) => {
|
event => {
|
||||||
|
const {
|
||||||
|
target: { tagName },
|
||||||
|
key
|
||||||
|
} = event
|
||||||
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
|
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
|
||||||
|
event.preventDefault()
|
||||||
onKeyDown()
|
onKeyDown()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -44,6 +50,41 @@ export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false)
|
|||||||
}, [downHandler])
|
}, [downHandler])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useENSName(address) {
|
||||||
|
const { library } = useWeb3Context()
|
||||||
|
|
||||||
|
const [ENSName, setENSNname] = useState()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAddress(address)) {
|
||||||
|
let stale = false
|
||||||
|
library
|
||||||
|
.lookupAddress(address)
|
||||||
|
.then(name => {
|
||||||
|
if (!stale) {
|
||||||
|
if (name) {
|
||||||
|
setENSNname(name)
|
||||||
|
} else {
|
||||||
|
setENSNname(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!stale) {
|
||||||
|
setENSNname(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stale = true
|
||||||
|
setENSNname()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [library, address])
|
||||||
|
|
||||||
|
return ENSName
|
||||||
|
}
|
||||||
|
|
||||||
// returns null on errors
|
// returns null on errors
|
||||||
export function useContract(address, ABI, withSignerIfPossible = true) {
|
export function useContract(address, ABI, withSignerIfPossible = true) {
|
||||||
const { library, account } = useWeb3Context()
|
const { library, account } = useWeb3Context()
|
||||||
@ -94,3 +135,26 @@ export function useExchangeContract(exchangeAddress, withSignerIfPossible = true
|
|||||||
}
|
}
|
||||||
}, [exchangeAddress, library, withSignerIfPossible, account])
|
}, [exchangeAddress, library, withSignerIfPossible, account])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCopyClipboard(timeout = 500) {
|
||||||
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
|
|
||||||
|
const staticCopy = useCallback(text => {
|
||||||
|
const didCopy = copy(text)
|
||||||
|
setIsCopied(didCopy)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCopied) {
|
||||||
|
const hide = setTimeout(() => {
|
||||||
|
setIsCopied(false)
|
||||||
|
}, timeout)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(hide)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isCopied, setIsCopied, timeout])
|
||||||
|
|
||||||
|
return [isCopied, staticCopy]
|
||||||
|
}
|
||||||
|
10
src/i18n.js
@ -1,26 +1,22 @@
|
|||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
import XHR from 'i18next-xhr-backend'
|
import XHR from 'i18next-xhr-backend'
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
import { initReactI18next } from 'react-i18next'
|
|
||||||
|
|
||||||
i18next
|
i18next
|
||||||
// load translation using xhr -> see /public/locales
|
|
||||||
// https://github.com/i18next/i18next-xhr-backend
|
|
||||||
.use(XHR)
|
.use(XHR)
|
||||||
// detect user language
|
|
||||||
// https://github.com/i18next/i18next-browser-languageDetector
|
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
// https://www.i18next.com/overview/configuration-options
|
|
||||||
.init({
|
.init({
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: '/locales/{{lng}}.json'
|
loadPath: '/locales/{{lng}}.json'
|
||||||
},
|
},
|
||||||
react: {
|
react: {
|
||||||
useSuspense: false
|
useSuspense: true
|
||||||
},
|
},
|
||||||
lng: 'en',
|
lng: 'en',
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
|
preload: ['en'],
|
||||||
keySeparator: false,
|
keySeparator: false,
|
||||||
interpolation: { escapeValue: false }
|
interpolation: { escapeValue: false }
|
||||||
})
|
})
|
||||||
|
11
src/index.js
@ -11,6 +11,7 @@ import BalancesContextProvider from './contexts/Balances'
|
|||||||
import AllowancesContextProvider from './contexts/Allowances'
|
import AllowancesContextProvider from './contexts/Allowances'
|
||||||
|
|
||||||
import App from './pages/App'
|
import App from './pages/App'
|
||||||
|
import InjectedConnector from './InjectedConnector'
|
||||||
|
|
||||||
import './i18n'
|
import './i18n'
|
||||||
|
|
||||||
@ -21,12 +22,10 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
}
|
}
|
||||||
ReactGA.pageview(window.location.pathname + window.location.search)
|
ReactGA.pageview(window.location.pathname + window.location.search)
|
||||||
|
|
||||||
const { InjectedConnector, NetworkOnlyConnector } = Connectors
|
const { NetworkOnlyConnector } = Connectors
|
||||||
const Injected = new InjectedConnector({ supportedNetworks: [Number(process.env.REACT_APP_NETWORK_ID) || 1] })
|
const Injected = new InjectedConnector({ supportedNetworks: [Number(process.env.REACT_APP_NETWORK_ID || '1')] })
|
||||||
const Infura = new NetworkOnlyConnector({
|
const Network = new NetworkOnlyConnector({ providerURL: process.env.REACT_APP_NETWORK_URL || '' })
|
||||||
providerURL: process.env.REACT_APP_NETWORK_URL || ''
|
const connectors = { Injected, Network }
|
||||||
})
|
|
||||||
const connectors = { Injected, Infura }
|
|
||||||
|
|
||||||
function ContextProviders({ children }) {
|
function ContextProviders({ children }) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,204 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
/*
|
|
||||||
@asset(/libraries/qr-scanner/qr-scanner-worker.min.js) */
|
|
||||||
'use strict'
|
|
||||||
export default class QrScanner {
|
|
||||||
constructor(video, onDecode, canvasSize = QrScanner.DEFAULT_CANVAS_SIZE) {
|
|
||||||
this.$video = video
|
|
||||||
this.$canvas = document.createElement('canvas')
|
|
||||||
this._onDecode = onDecode
|
|
||||||
this._active = false
|
|
||||||
this.$canvas.width = canvasSize
|
|
||||||
this.$canvas.height = canvasSize
|
|
||||||
this._sourceRect = { x: 0, y: 0, width: canvasSize, height: canvasSize }
|
|
||||||
this.$video.addEventListener('canplay', () => this._updateSourceRect())
|
|
||||||
this.$video.addEventListener(
|
|
||||||
'play',
|
|
||||||
() => {
|
|
||||||
this._updateSourceRect()
|
|
||||||
this._scanFrame()
|
|
||||||
},
|
|
||||||
false
|
|
||||||
)
|
|
||||||
this._qrWorker = new Worker(QrScanner.WORKER_PATH)
|
|
||||||
}
|
|
||||||
_updateSourceRect() {
|
|
||||||
const smallestDimension = Math.min(this.$video.videoWidth, this.$video.videoHeight)
|
|
||||||
const sourceRectSize = Math.round((2 / 3) * smallestDimension)
|
|
||||||
this._sourceRect.width = this._sourceRect.height = sourceRectSize
|
|
||||||
this._sourceRect.x = (this.$video.videoWidth - sourceRectSize) / 2
|
|
||||||
this._sourceRect.y = (this.$video.videoHeight - sourceRectSize) / 2
|
|
||||||
}
|
|
||||||
_scanFrame() {
|
|
||||||
if (this.$video.paused || this.$video.ended) return false
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
QrScanner.scanImage(this.$video, this._sourceRect, this._qrWorker, this.$canvas, true)
|
|
||||||
.then(this._onDecode, error => {
|
|
||||||
if (error !== 'QR code not found.') console.error(error)
|
|
||||||
})
|
|
||||||
.then(() => this._scanFrame())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_getCameraStream(facingMode, exact = false) {
|
|
||||||
const constraintsToTry = [{ width: { min: 1024 } }, { width: { min: 768 } }, {}]
|
|
||||||
if (facingMode) {
|
|
||||||
if (exact) facingMode = { exact: facingMode }
|
|
||||||
constraintsToTry.forEach(constraint => (constraint.facingMode = facingMode))
|
|
||||||
}
|
|
||||||
return this._getMatchingCameraStream(constraintsToTry)
|
|
||||||
}
|
|
||||||
_getMatchingCameraStream(constraintsToTry) {
|
|
||||||
if (constraintsToTry.length === 0) return Promise.reject('Camera not found.')
|
|
||||||
return navigator.mediaDevices
|
|
||||||
.getUserMedia({ video: constraintsToTry.shift() })
|
|
||||||
.catch(() => this._getMatchingCameraStream(constraintsToTry))
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
if (this._active) return Promise.resolve()
|
|
||||||
this._active = true
|
|
||||||
clearTimeout(this._offTimeout)
|
|
||||||
let facingMode = 'environment'
|
|
||||||
return this._getCameraStream('environment', true)
|
|
||||||
.catch(() => {
|
|
||||||
facingMode = 'user'
|
|
||||||
return this._getCameraStream()
|
|
||||||
})
|
|
||||||
.then(stream => {
|
|
||||||
this.$video.srcObject = stream
|
|
||||||
this._setVideoMirror(facingMode)
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
this._active = false
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
if (!this._active) return
|
|
||||||
this._active = false
|
|
||||||
this.$video.pause()
|
|
||||||
this._offTimeout = setTimeout(() => {
|
|
||||||
this.$video.srcObject.getTracks()[0].stop()
|
|
||||||
this.$video.srcObject = null
|
|
||||||
}, 3e3)
|
|
||||||
}
|
|
||||||
_setVideoMirror(facingMode) {
|
|
||||||
const scaleFactor = facingMode === 'user' ? -1 : 1
|
|
||||||
this.$video.style.transform = 'scaleX(' + scaleFactor + ')'
|
|
||||||
}
|
|
||||||
setGrayscaleWeights(red, green, blue) {
|
|
||||||
this._qrWorker.postMessage({ type: 'grayscaleWeights', data: { red, green, blue } })
|
|
||||||
}
|
|
||||||
static scanImage(
|
|
||||||
imageOrFileOrUrl,
|
|
||||||
sourceRect = null,
|
|
||||||
worker = null,
|
|
||||||
canvas = null,
|
|
||||||
fixedCanvasSize = false,
|
|
||||||
alsoTryWithoutSourceRect = false
|
|
||||||
) {
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
|
||||||
worker = worker || new Worker(QrScanner.WORKER_PATH)
|
|
||||||
let timeout, onMessage, onError
|
|
||||||
onMessage = event => {
|
|
||||||
if (event.data.type !== 'qrResult') return
|
|
||||||
worker.removeEventListener('message', onMessage)
|
|
||||||
worker.removeEventListener('error', onError)
|
|
||||||
clearTimeout(timeout)
|
|
||||||
if (event.data.data !== null) resolve(event.data.data)
|
|
||||||
else reject('QR code not found.')
|
|
||||||
}
|
|
||||||
onError = () => {
|
|
||||||
worker.removeEventListener('message', onMessage)
|
|
||||||
worker.removeEventListener('error', onError)
|
|
||||||
clearTimeout(timeout)
|
|
||||||
reject('Worker error.')
|
|
||||||
}
|
|
||||||
worker.addEventListener('message', onMessage)
|
|
||||||
worker.addEventListener('error', onError)
|
|
||||||
timeout = setTimeout(onError, 3e3)
|
|
||||||
QrScanner._loadImage(imageOrFileOrUrl)
|
|
||||||
.then(image => {
|
|
||||||
const imageData = QrScanner._getImageData(image, sourceRect, canvas, fixedCanvasSize)
|
|
||||||
worker.postMessage({ type: 'decode', data: imageData }, [imageData.data.buffer])
|
|
||||||
})
|
|
||||||
.catch(reject)
|
|
||||||
})
|
|
||||||
if (sourceRect && alsoTryWithoutSourceRect)
|
|
||||||
return promise.catch(() => QrScanner.scanImage(imageOrFileOrUrl, null, worker, canvas, fixedCanvasSize))
|
|
||||||
else return promise
|
|
||||||
}
|
|
||||||
static _getImageData(image, sourceRect = null, canvas = null, fixedCanvasSize = false) {
|
|
||||||
canvas = canvas || document.createElement('canvas')
|
|
||||||
const sourceRectX = sourceRect && sourceRect.x ? sourceRect.x : 0
|
|
||||||
const sourceRectY = sourceRect && sourceRect.y ? sourceRect.y : 0
|
|
||||||
const sourceRectWidth = sourceRect && sourceRect.width ? sourceRect.width : image.width || image.videoWidth
|
|
||||||
const sourceRectHeight = sourceRect && sourceRect.height ? sourceRect.height : image.height || image.videoHeight
|
|
||||||
if (!fixedCanvasSize && (canvas.width !== sourceRectWidth || canvas.height !== sourceRectHeight)) {
|
|
||||||
canvas.width = sourceRectWidth
|
|
||||||
canvas.height = sourceRectHeight
|
|
||||||
}
|
|
||||||
const context = canvas.getContext('2d', { alpha: false })
|
|
||||||
context.imageSmoothingEnabled = false
|
|
||||||
context.drawImage(
|
|
||||||
image,
|
|
||||||
sourceRectX,
|
|
||||||
sourceRectY,
|
|
||||||
sourceRectWidth,
|
|
||||||
sourceRectHeight,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
canvas.width,
|
|
||||||
canvas.height
|
|
||||||
)
|
|
||||||
return context.getImageData(0, 0, canvas.width, canvas.height)
|
|
||||||
}
|
|
||||||
static _loadImage(imageOrFileOrUrl) {
|
|
||||||
if (
|
|
||||||
imageOrFileOrUrl instanceof HTMLCanvasElement ||
|
|
||||||
imageOrFileOrUrl instanceof HTMLVideoElement ||
|
|
||||||
(window.ImageBitmap && imageOrFileOrUrl instanceof window.ImageBitmap) ||
|
|
||||||
(window.OffscreenCanvas && imageOrFileOrUrl instanceof window.OffscreenCanvas)
|
|
||||||
)
|
|
||||||
return Promise.resolve(imageOrFileOrUrl)
|
|
||||||
else if (imageOrFileOrUrl instanceof Image)
|
|
||||||
return QrScanner._awaitImageLoad(imageOrFileOrUrl).then(() => imageOrFileOrUrl)
|
|
||||||
else if (
|
|
||||||
imageOrFileOrUrl instanceof File ||
|
|
||||||
imageOrFileOrUrl instanceof URL ||
|
|
||||||
typeof imageOrFileOrUrl === 'string'
|
|
||||||
) {
|
|
||||||
const image = new Image()
|
|
||||||
if (imageOrFileOrUrl instanceof File) image.src = URL.createObjectURL(imageOrFileOrUrl)
|
|
||||||
else image.src = imageOrFileOrUrl
|
|
||||||
return QrScanner._awaitImageLoad(image).then(() => {
|
|
||||||
if (imageOrFileOrUrl instanceof File) URL.revokeObjectURL(image.src)
|
|
||||||
return image
|
|
||||||
})
|
|
||||||
} else return Promise.reject('Unsupported image type.')
|
|
||||||
}
|
|
||||||
static _awaitImageLoad(image) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (image.complete && image.naturalWidth !== 0) resolve()
|
|
||||||
else {
|
|
||||||
let onLoad, onError
|
|
||||||
onLoad = () => {
|
|
||||||
image.removeEventListener('load', onLoad)
|
|
||||||
image.removeEventListener('error', onError)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
onError = () => {
|
|
||||||
image.removeEventListener('load', onLoad)
|
|
||||||
image.removeEventListener('error', onError)
|
|
||||||
reject('Image load error')
|
|
||||||
}
|
|
||||||
image.addEventListener('load', onLoad)
|
|
||||||
image.addEventListener('error', onError)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QrScanner.DEFAULT_CANVAS_SIZE = 400
|
|
||||||
QrScanner.WORKER_PATH = '/libraries/qr-scanner/qr-scanner-worker.min.js'
|
|
||||||
|
|
||||||
//# sourceMappingURL=qr-scanner.min.js.map
|
|
@ -1,50 +1,49 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React, { Suspense, lazy } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
|
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
|
||||||
import { useWeb3Context, Connectors } from 'web3-react'
|
|
||||||
|
|
||||||
import NavigationTabs from '../components/NavigationTabs'
|
import Web3ReactManager from '../components/Web3ReactManager'
|
||||||
import Header from '../components/Header'
|
import Header from '../components/Header'
|
||||||
import Swap from './Swap'
|
import NavigationTabs from '../components/NavigationTabs'
|
||||||
import Send from './Send'
|
|
||||||
import Pool from './Pool'
|
|
||||||
|
|
||||||
import './App.scss'
|
const Swap = lazy(() => import('./Swap'))
|
||||||
|
const Send = lazy(() => import('./Send'))
|
||||||
|
const Pool = lazy(() => import('./Pool'))
|
||||||
|
|
||||||
const { Connector, InjectedConnector } = Connectors
|
const HeaderWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
`
|
||||||
|
|
||||||
|
const BodyWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
overflow: auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Body = styled.div`
|
||||||
|
width: 35rem;
|
||||||
|
margin: 1.25rem;
|
||||||
|
`
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { setConnector, setError, error, active, connectorName } = useWeb3Context()
|
return (
|
||||||
|
<>
|
||||||
// start web3-react on page-load
|
<Suspense fallback={null}>
|
||||||
useEffect(() => {
|
<HeaderWrapper>
|
||||||
setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
|
<Header />
|
||||||
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
|
</HeaderWrapper>
|
||||||
setError(error, { connectorName: 'Injected' })
|
<BodyWrapper>
|
||||||
} else {
|
<Body>
|
||||||
setConnector('Infura')
|
<Web3ReactManager>
|
||||||
}
|
<BrowserRouter>
|
||||||
})
|
<NavigationTabs />
|
||||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
{/* this Suspense is for route code-splitting */}
|
||||||
|
<Suspense fallback={null}>
|
||||||
// if the metamask user logs out, set the infura provider
|
|
||||||
useEffect(() => {
|
|
||||||
if (error && error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) {
|
|
||||||
setConnector('Infura')
|
|
||||||
}
|
|
||||||
}, [error, connectorName, setConnector])
|
|
||||||
|
|
||||||
// active state
|
|
||||||
if (active || error) {
|
|
||||||
return (
|
|
||||||
<div id="app-container">
|
|
||||||
<Header />
|
|
||||||
{/* this is an intermediate state before infura is set */}
|
|
||||||
{(!error || error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) && (
|
|
||||||
<div className="app__wrapper">
|
|
||||||
<div className="body">
|
|
||||||
<div className="body__content">
|
|
||||||
<BrowserRouter>
|
|
||||||
<NavigationTabs />
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact strict path="/swap" component={Swap} />
|
<Route exact strict path="/swap" component={Swap} />
|
||||||
<Route exact strict path="/send" component={Send} />
|
<Route exact strict path="/send" component={Send} />
|
||||||
@ -59,15 +58,12 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
<Redirect to="/swap" />
|
<Redirect to="/swap" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</Suspense>
|
||||||
</div>
|
</BrowserRouter>
|
||||||
</div>
|
</Web3ReactManager>
|
||||||
</div>
|
</Body>
|
||||||
)}
|
</BodyWrapper>
|
||||||
</div>
|
</Suspense>
|
||||||
)
|
</>
|
||||||
}
|
)
|
||||||
|
|
||||||
// loading state
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
@import '../variables.scss';
|
|
||||||
|
|
||||||
.app {
|
|
||||||
&__wrapper {
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
margin: auto;
|
|
||||||
max-width: 560px;
|
|
||||||
width: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#app-container {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
@extend %col-nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
@extend %col-nowrap;
|
|
||||||
height: 100%;
|
|
||||||
background-color: $white;
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
padding: 1rem 0.75rem;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import Web3Provider, { Connectors } from 'web3-react'
|
|
||||||
|
|
||||||
import App from './App'
|
|
||||||
|
|
||||||
// TODO, fix this hacky workaround
|
|
||||||
const { NetworkOnlyConnector } = Connectors
|
|
||||||
const Injected = new NetworkOnlyConnector({
|
|
||||||
providerURL: process.env.REACT_APP_NETWORK_URL
|
|
||||||
})
|
|
||||||
export const connectors = { Injected }
|
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
|
||||||
const div = document.createElement('div')
|
|
||||||
ReactDOM.render(
|
|
||||||
<Web3Provider connectors={connectors} libraryName="ethers.js">
|
|
||||||
<App />
|
|
||||||
</Web3Provider>,
|
|
||||||
div
|
|
||||||
)
|
|
||||||
ReactDOM.unmountComponentAtNode(div)
|
|
||||||
})
|
|
@ -1,10 +1,11 @@
|
|||||||
import React, { useReducer, useState, useCallback, useEffect, useMemo } from 'react'
|
import React, { useReducer, useState, useCallback, useEffect, useMemo } from 'react'
|
||||||
import classnames from 'classnames'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
import ReactGA from 'react-ga'
|
import ReactGA from 'react-ga'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { Button } from '../../theme'
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import OversizedPanel from '../../components/OversizedPanel'
|
import OversizedPanel from '../../components/OversizedPanel'
|
||||||
import ContextualInfo from '../../components/ContextualInfo'
|
import ContextualInfo from '../../components/ContextualInfo'
|
||||||
@ -17,8 +18,6 @@ import { useTokenDetails } from '../../contexts/Tokens'
|
|||||||
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
||||||
import { useAddressAllowance } from '../../contexts/Allowances'
|
import { useAddressAllowance } from '../../contexts/Allowances'
|
||||||
|
|
||||||
import './pool.scss'
|
|
||||||
|
|
||||||
const INPUT = 0
|
const INPUT = 0
|
||||||
const OUTPUT = 1
|
const OUTPUT = 1
|
||||||
|
|
||||||
@ -31,6 +30,72 @@ const DEADLINE_FROM_NOW = 60 * 15
|
|||||||
// denominated in bips
|
// denominated in bips
|
||||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
||||||
|
|
||||||
|
const BlueSpan = styled.span`
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
`
|
||||||
|
|
||||||
|
const NewExchangeWarning = styled.div`
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border: 1px solid rgba($pizazz-orange, 0.4);
|
||||||
|
background-color: rgba($pizazz-orange, 0.1);
|
||||||
|
border-radius: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const NewExchangeWarningText = styled.div`
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
padding-bottom: 0.3rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DownArrowBackground = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DownArrow = styled.img`
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.875rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SummaryPanel = styled.div`
|
||||||
|
${({ theme }) => theme.flexColumnNoWrap}
|
||||||
|
padding: 1rem 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRateWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 1rem 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRate = styled.span`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
color: ${({ theme }) => theme.chaliceGray};
|
||||||
|
`
|
||||||
|
|
||||||
|
const Flex = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
function calculateSlippageBounds(value) {
|
function calculateSlippageBounds(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
const offset = value.mul(ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
|
const offset = value.mul(ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
|
||||||
@ -209,15 +274,15 @@ export default function AddLiquidity() {
|
|||||||
action: 'Open'
|
action: 'Open'
|
||||||
})
|
})
|
||||||
|
|
||||||
const b = text => <span className="swap__highlight-text">{text}</span>
|
const b = text => <BlueSpan>{text}</BlueSpan>
|
||||||
|
|
||||||
if (isNewExchange) {
|
if (isNewExchange) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="pool__summary-item">
|
<div>
|
||||||
{t('youAreAdding')} {b(`${inputValue} ETH`)} {t('and')} {b(`${outputValue} ${symbol}`)} {t('intoPool')}
|
{t('youAreAdding')} {b(`${inputValue} ETH`)} {t('and')} {b(`${outputValue} ${symbol}`)} {t('intoPool')}
|
||||||
</div>
|
</div>
|
||||||
<div className="pool__summary-item">
|
<div>
|
||||||
{t('youAreSettingExRate')}{' '}
|
{t('youAreSettingExRate')}{' '}
|
||||||
{b(
|
{b(
|
||||||
`1 ETH = ${amountFormatter(
|
`1 ETH = ${amountFormatter(
|
||||||
@ -229,26 +294,26 @@ export default function AddLiquidity() {
|
|||||||
)}
|
)}
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
<div className="pool__summary-item">
|
<div>
|
||||||
{t('youWillMint')} {b(`${inputValue}`)} {t('liquidityTokens')}
|
{t('youWillMint')} {b(`${inputValue}`)} {t('liquidityTokens')}
|
||||||
</div>
|
</div>
|
||||||
<div className="pool__summary-item">{t('totalSupplyIs0')}</div>
|
<div>{t('totalSupplyIs0')}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="pool__summary-modal__item">
|
<div>
|
||||||
{t('youAreAdding')} {b(`${amountFormatter(inputValueParsed, 18, 4)} ETH`)} {t('and')} {'at most'}{' '}
|
{t('youAreAdding')} {b(`${amountFormatter(inputValueParsed, 18, 4)} ETH`)} {t('and')} {'at most'}{' '}
|
||||||
{b(`${amountFormatter(outputValueMax, decimals, 4)} ${symbol}`)} {t('intoPool')}
|
{b(`${amountFormatter(outputValueMax, decimals, 4)} ${symbol}`)} {t('intoPool')}
|
||||||
</div>
|
</div>
|
||||||
<div className="pool__summary-modal__item">
|
<div>
|
||||||
{t('youWillMint')} {b(amountFormatter(liquidityMinted, 18, 4))} {t('liquidityTokens')}
|
{t('youWillMint')} {b(amountFormatter(liquidityMinted, 18, 4))} {t('liquidityTokens')}
|
||||||
</div>
|
</div>
|
||||||
<div className="pool__summary-modal__item">
|
<div>
|
||||||
{t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))}
|
{t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))}
|
||||||
</div>
|
</div>
|
||||||
<div className="pool__summary-modal__item">
|
<div>
|
||||||
{t('tokenWorth')} {b(amountFormatter(ethPerLiquidityToken, 18, 4))} ETH {t('and')}{' '}
|
{t('tokenWorth')} {b(amountFormatter(ethPerLiquidityToken, 18, 4))} ETH {t('and')}{' '}
|
||||||
{b(amountFormatter(tokenPerLiquidityToken, decimals, Math.min(decimals, 4)))} {symbol}
|
{b(amountFormatter(tokenPerLiquidityToken, decimals, Math.min(decimals, 4)))} {symbol}
|
||||||
</div>
|
</div>
|
||||||
@ -450,15 +515,15 @@ export default function AddLiquidity() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isNewExchange ? (
|
{isNewExchange ? (
|
||||||
<div className="pool__new-exchange-warning">
|
<NewExchangeWarning>
|
||||||
<div className="pool__new-exchange-warning-text">
|
<NewExchangeWarningText>
|
||||||
<span role="img" aria-label="first-liquidity">
|
<span role="img" aria-label="first-liquidity">
|
||||||
🚰
|
🚰
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{t('firstLiquidity')}
|
{t('firstLiquidity')}
|
||||||
</div>
|
</NewExchangeWarningText>
|
||||||
<div className="pool__new-exchange-warning-text">{t('initialExchangeRate', { symbol })}</div>
|
<NewExchangeWarningText>{t('initialExchangeRate', { symbol })}</NewExchangeWarningText>
|
||||||
</div>
|
</NewExchangeWarning>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
@ -473,9 +538,9 @@ export default function AddLiquidity() {
|
|||||||
disableTokenSelect
|
disableTokenSelect
|
||||||
/>
|
/>
|
||||||
<OversizedPanel>
|
<OversizedPanel>
|
||||||
<div className="swap__down-arrow-background">
|
<DownArrowBackground>
|
||||||
<img className="swap__down-arrow" src={isActive ? PlusBlue : PlusGrey} alt="plus" />
|
<DownArrow src={isActive ? PlusBlue : PlusGrey} alt="plus" />
|
||||||
</div>
|
</DownArrowBackground>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
title={t('deposit')}
|
title={t('deposit')}
|
||||||
@ -493,13 +558,13 @@ export default function AddLiquidity() {
|
|||||||
errorMessage={outputError}
|
errorMessage={outputError}
|
||||||
/>
|
/>
|
||||||
<OversizedPanel hideBottom>
|
<OversizedPanel hideBottom>
|
||||||
<div className="pool__summary-panel">
|
<SummaryPanel>
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<ExchangeRateWrapper>
|
||||||
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
|
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
|
||||||
<span>{marketRate ? `1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}` : ' - '}</span>
|
<span>{marketRate ? `1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}` : ' - '}</span>
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<ExchangeRateWrapper>
|
||||||
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
|
<ExchangeRate>{t('currentPoolSize')}</ExchangeRate>
|
||||||
<span>
|
<span>
|
||||||
{exchangeETHBalance && exchangeTokenBalance
|
{exchangeETHBalance && exchangeTokenBalance
|
||||||
? `${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter(
|
? `${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter(
|
||||||
@ -509,11 +574,11 @@ export default function AddLiquidity() {
|
|||||||
)} ${symbol}`
|
)} ${symbol}`
|
||||||
: ' - '}
|
: ' - '}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<ExchangeRateWrapper>
|
||||||
<span className="swap__exchange-rate">
|
<ExchangeRate>
|
||||||
{t('yourPoolShare')} ({exchangeETHBalance && amountFormatter(poolTokenPercentage, 16, 2)}%)
|
{t('yourPoolShare')} ({exchangeETHBalance && amountFormatter(poolTokenPercentage, 16, 2)}%)
|
||||||
</span>
|
</ExchangeRate>
|
||||||
<span>
|
<span>
|
||||||
{ethShare && tokenShare
|
{ethShare && tokenShare
|
||||||
? `${amountFormatter(ethShare, 18, 4)} ETH + ${amountFormatter(
|
? `${amountFormatter(ethShare, 18, 4)} ETH + ${amountFormatter(
|
||||||
@ -523,21 +588,15 @@ export default function AddLiquidity() {
|
|||||||
)} ${symbol}`
|
)} ${symbol}`
|
||||||
: ' - '}
|
: ' - '}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
</div>
|
</SummaryPanel>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
{renderSummary()}
|
{renderSummary()}
|
||||||
<div className="pool__cta-container">
|
<Flex>
|
||||||
<button
|
<Button disabled={!isValid} onClick={onAddLiquidity}>
|
||||||
className={classnames('pool__cta-btn', {
|
|
||||||
'pool__cta-btn--inactive': !isActive
|
|
||||||
})}
|
|
||||||
disabled={!isValid}
|
|
||||||
onClick={onAddLiquidity}
|
|
||||||
>
|
|
||||||
{t('addLiquidity')}
|
{t('addLiquidity')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,58 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import { withRouter } from 'react-router'
|
import { withRouter } from 'react-router'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
import classnames from 'classnames'
|
import styled from 'styled-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ReactGA from 'react-ga'
|
import ReactGA from 'react-ga'
|
||||||
|
|
||||||
|
import { Button } from '../../theme'
|
||||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||||
import OversizedPanel from '../../components/OversizedPanel'
|
import OversizedPanel from '../../components/OversizedPanel'
|
||||||
import { useFactoryContract } from '../../hooks'
|
import { useFactoryContract } from '../../hooks'
|
||||||
import { useTokenDetails } from '../../contexts/Tokens'
|
import { useTokenDetails } from '../../contexts/Tokens'
|
||||||
import { useTransactionAdder } from '../../contexts/Transactions'
|
import { useTransactionAdder } from '../../contexts/Transactions'
|
||||||
|
|
||||||
|
const SummaryPanel = styled.div`
|
||||||
|
${({ theme }) => theme.flexColumnNoWrap};
|
||||||
|
padding: 1rem 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRateWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 1rem 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRate = styled.span`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
color: ${({ theme }) => theme.chaliceGray};
|
||||||
|
`
|
||||||
|
|
||||||
|
const CreateExchangeWrapper = styled.div`
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SummaryText = styled.div`
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: ${({ error, theme }) => error && theme.salmonRed};
|
||||||
|
`
|
||||||
|
|
||||||
|
const Flex = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
function CreateExchange({ history, location }) {
|
function CreateExchange({ history, location }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { account } = useWeb3Context()
|
const { account } = useWeb3Context()
|
||||||
@ -81,35 +123,29 @@ function CreateExchange({ history, location }) {
|
|||||||
onError={setTokenAddressError}
|
onError={setTokenAddressError}
|
||||||
/>
|
/>
|
||||||
<OversizedPanel hideBottom>
|
<OversizedPanel hideBottom>
|
||||||
<div className="pool__summary-panel">
|
<SummaryPanel>
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<ExchangeRateWrapper>
|
||||||
<span className="pool__exchange-rate">{t('name')}</span>
|
<ExchangeRate>{t('name')}</ExchangeRate>
|
||||||
<span>{name ? name : ' - '}</span>
|
<span>{name ? name : ' - '}</span>
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<ExchangeRateWrapper>
|
||||||
<span className="pool__exchange-rate">{t('symbol')}</span>
|
<ExchangeRate>{t('symbol')}</ExchangeRate>
|
||||||
<span>{symbol ? symbol : ' - '}</span>
|
<span>{symbol ? symbol : ' - '}</span>
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<ExchangeRateWrapper>
|
||||||
<span className="swap__exchange-rate">{t('decimals')}</span>
|
<ExchangeRate>{t('decimals')}</ExchangeRate>
|
||||||
<span>{decimals || decimals === 0 ? decimals : ' - '}</span>
|
<span>{decimals || decimals === 0 ? decimals : ' - '}</span>
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
</div>
|
</SummaryPanel>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
<div className="create-exchange__summary-panel">
|
<CreateExchangeWrapper>
|
||||||
<div
|
<SummaryText>{errorMessage ? errorMessage : t('enterTokenCont')}</SummaryText>
|
||||||
className={classnames('create-exchange__summary-text', {
|
</CreateExchangeWrapper>
|
||||||
'create-exchange--error': !!errorMessage
|
<Flex>
|
||||||
})}
|
<Button disabled={!isValid} onClick={createExchange}>
|
||||||
>
|
|
||||||
{errorMessage ? errorMessage : t('enterTokenCont')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pool__cta-container">
|
|
||||||
<button className="pool__cta-btn" disabled={!isValid} onClick={createExchange}>
|
|
||||||
{t('createExchange')}
|
{t('createExchange')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import React, { useState, useCallback } from 'react'
|
import React, { useState, useCallback } from 'react'
|
||||||
import { withRouter, NavLink } from 'react-router-dom'
|
import { withRouter, NavLink } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { CSSTransitionGroup } from 'react-transition-group'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import OversizedPanel from '../../components/OversizedPanel'
|
import OversizedPanel from '../../components/OversizedPanel'
|
||||||
import Dropdown from '../../assets/images/dropdown-blue.svg'
|
import Dropdown from '../../assets/images/dropdown-blue.svg'
|
||||||
import Modal from '../../components/Modal'
|
import Modal from '../../components/Modal'
|
||||||
import { useBodyKeyDown } from '../../hooks'
|
import { useBodyKeyDown } from '../../hooks'
|
||||||
|
|
||||||
import './pool.scss'
|
|
||||||
|
|
||||||
const poolTabOrder = [
|
const poolTabOrder = [
|
||||||
{
|
{
|
||||||
path: '/add-liquidity',
|
path: '/add-liquidity',
|
||||||
@ -28,10 +26,61 @@ const poolTabOrder = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const LiquidityContainer = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 0.75rem;
|
||||||
|
width: 0.75rem;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LiquidityLabel = styled.span`
|
||||||
|
flex: 1 0 auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
const activeClassName = 'MODE'
|
||||||
|
|
||||||
|
const StyledNavLink = styled(NavLink).attrs({
|
||||||
|
activeClassName
|
||||||
|
})`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
padding: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
&.${activeClassName} {
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
border-radius: 3rem;
|
||||||
|
box-shadow: 0 0 0.5px 0.5px ${({ theme }) => theme.mercuryGray};
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const PoolModal = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 2rem 0 2rem 0;
|
||||||
|
`
|
||||||
|
|
||||||
function ModeSelector({ location: { pathname }, history }) {
|
function ModeSelector({ location: { pathname }, history }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [isShowingModal, setIsShowingModal] = useState(false)
|
const [modalIsOpen, setModalIsOpen] = useState(false)
|
||||||
|
|
||||||
const activeTabKey = poolTabOrder[poolTabOrder.findIndex(({ regex }) => pathname.match(regex))].textKey
|
const activeTabKey = poolTabOrder[poolTabOrder.findIndex(({ regex }) => pathname.match(regex))].textKey
|
||||||
|
|
||||||
@ -49,53 +98,41 @@ function ModeSelector({ location: { pathname }, history }) {
|
|||||||
navigate(-1)
|
navigate(-1)
|
||||||
}, [navigate])
|
}, [navigate])
|
||||||
|
|
||||||
useBodyKeyDown('ArrowDown', navigateRight, isShowingModal)
|
useBodyKeyDown('ArrowDown', navigateRight, modalIsOpen)
|
||||||
useBodyKeyDown('ArrowUp', navigateLeft, isShowingModal)
|
useBodyKeyDown('ArrowUp', navigateLeft, modalIsOpen)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OversizedPanel hideTop>
|
<OversizedPanel hideTop>
|
||||||
<div
|
<LiquidityContainer
|
||||||
className="pool__liquidity-container"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsShowingModal(true)
|
setModalIsOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="pool__liquidity-label">{t(activeTabKey)}</span>
|
<LiquidityLabel>{t(activeTabKey)}</LiquidityLabel>
|
||||||
<img src={Dropdown} alt="dropdown" />
|
<img src={Dropdown} alt="dropdown" />
|
||||||
</div>
|
</LiquidityContainer>
|
||||||
{isShowingModal && (
|
<Modal
|
||||||
<Modal
|
isOpen={modalIsOpen}
|
||||||
onClose={() => {
|
onDismiss={() => {
|
||||||
setIsShowingModal(false)
|
setModalIsOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
minHeight={null}
|
||||||
<CSSTransitionGroup
|
>
|
||||||
transitionName="pool-modal"
|
<PoolModal>
|
||||||
transitionAppear={true}
|
{poolTabOrder.map(({ path, textKey, regex }) => (
|
||||||
transitionLeave={true}
|
<StyledNavLink
|
||||||
transitionAppearTimeout={200}
|
key={path}
|
||||||
transitionLeaveTimeout={200}
|
to={path}
|
||||||
transitionEnterTimeout={200}
|
isActive={(_, { pathname }) => pathname.match(regex)}
|
||||||
>
|
onClick={() => {
|
||||||
<div className="pool-modal">
|
setModalIsOpen(false)
|
||||||
{poolTabOrder.map(({ path, textKey, regex }) => (
|
}}
|
||||||
<NavLink
|
>
|
||||||
key={path}
|
{t(textKey)}
|
||||||
to={path}
|
</StyledNavLink>
|
||||||
className="pool-modal__item"
|
))}
|
||||||
activeClassName="pool-modal__item--selected"
|
</PoolModal>
|
||||||
isActive={(_, { pathname }) => pathname.match(regex)}
|
</Modal>
|
||||||
onClick={() => {
|
|
||||||
setIsShowingModal(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t(textKey)}
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CSSTransitionGroup>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import classnames from 'classnames'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ReactGA from 'react-ga'
|
import ReactGA from 'react-ga'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { Button } from '../../theme'
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import ContextualInfo from '../../components/ContextualInfo'
|
import ContextualInfo from '../../components/ContextualInfo'
|
||||||
import OversizedPanel from '../../components/OversizedPanel'
|
import OversizedPanel from '../../components/OversizedPanel'
|
||||||
@ -25,6 +26,69 @@ const DEADLINE_FROM_NOW = 60 * 15
|
|||||||
// denominated in bips
|
// denominated in bips
|
||||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
||||||
|
|
||||||
|
const BlueSpan = styled.span`
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
`
|
||||||
|
|
||||||
|
const DownArrowBackground = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DownArrow = styled.img`
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.875rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const RemoveLiquidityOutput = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
min-height: 3.5rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const RemoveLiquidityOutputText = styled.div`
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
padding: 1rem 0.75rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const RemoveLiquidityOutputPlus = styled.div`
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SummaryPanel = styled.div`
|
||||||
|
${({ theme }) => theme.flexColumnNoWrap}
|
||||||
|
padding: 1rem 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRateWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 1rem 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRate = styled.span`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
color: ${({ theme }) => theme.chaliceGray};
|
||||||
|
`
|
||||||
|
|
||||||
|
const Flex = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
|
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
@ -193,7 +257,7 @@ export default function RemoveLiquidity() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const b = text => <span className="swap__highlight-text">{text}</span>
|
const b = text => <BlueSpan>{text}</BlueSpan>
|
||||||
|
|
||||||
function renderTransactionDetails() {
|
function renderTransactionDetails() {
|
||||||
ReactGA.event({
|
ReactGA.event({
|
||||||
@ -203,17 +267,17 @@ export default function RemoveLiquidity() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="pool__summary-modal__item">
|
<div>
|
||||||
{t('youAreRemoving')} {b(`${amountFormatter(ethWithdrawnMin, 18, 4)} ETH`)} {t('and')}{' '}
|
{t('youAreRemoving')} {b(`${amountFormatter(ethWithdrawnMin, 18, 4)} ETH`)} {t('and')}{' '}
|
||||||
{b(`${amountFormatter(tokenWithdrawnMin, decimals, Math.min(decimals, 4))} ${symbol}`)} {t('outPool')}
|
{b(`${amountFormatter(tokenWithdrawnMin, decimals, Math.min(decimals, 4))} ${symbol}`)} {t('outPool')}
|
||||||
</div>
|
</div>
|
||||||
<div className="pool__summary-modal__item">
|
<div>
|
||||||
{t('youWillRemove')} {b(amountFormatter(valueParsed, 18, 4))} {t('liquidityTokens')}
|
{t('youWillRemove')} {b(amountFormatter(valueParsed, 18, 4))} {t('liquidityTokens')}
|
||||||
</div>
|
</div>
|
||||||
<div className="pool__summary-modal__item">
|
<div>
|
||||||
{t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))}
|
{t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))}
|
||||||
</div>
|
</div>
|
||||||
<div className="pool__summary-modal__item">
|
<div>
|
||||||
{t('tokenWorth')} {b(amountFormatter(ETHPer, 18, 4))} ETH {t('and')}{' '}
|
{t('tokenWorth')} {b(amountFormatter(ETHPer, 18, 4))} ETH {t('and')}{' '}
|
||||||
{b(amountFormatter(tokenPer, decimals, Math.min(4, decimals)))} {symbol}
|
{b(amountFormatter(tokenPer, decimals, Math.min(4, decimals)))} {symbol}
|
||||||
</div>
|
</div>
|
||||||
@ -278,9 +342,9 @@ export default function RemoveLiquidity() {
|
|||||||
selectedTokenAddress={outputCurrency}
|
selectedTokenAddress={outputCurrency}
|
||||||
/>
|
/>
|
||||||
<OversizedPanel>
|
<OversizedPanel>
|
||||||
<div className="swap__down-arrow-background">
|
<DownArrowBackground>
|
||||||
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
|
<DownArrow src={isActive ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
|
||||||
</div>
|
</DownArrowBackground>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
title={t('output')}
|
title={t('output')}
|
||||||
@ -288,35 +352,30 @@ export default function RemoveLiquidity() {
|
|||||||
key="remove-liquidity-input"
|
key="remove-liquidity-input"
|
||||||
renderInput={() =>
|
renderInput={() =>
|
||||||
ethWithdrawnMin && tokenWithdrawnMin ? (
|
ethWithdrawnMin && tokenWithdrawnMin ? (
|
||||||
<div className="remove-liquidity__output">
|
<RemoveLiquidityOutput>
|
||||||
<div className="remove-liquidity__output-text">{`${amountFormatter(
|
<RemoveLiquidityOutputText>
|
||||||
ethWithdrawnMin,
|
{`${amountFormatter(ethWithdrawnMin, 18, 4, false)} ETH`}
|
||||||
18,
|
</RemoveLiquidityOutputText>
|
||||||
4,
|
<RemoveLiquidityOutputPlus> + </RemoveLiquidityOutputPlus>
|
||||||
false
|
<RemoveLiquidityOutputText>
|
||||||
)} ETH`}</div>
|
{`${amountFormatter(tokenWithdrawnMin, decimals, Math.min(4, decimals))} ${symbol}`}
|
||||||
<div className="remove-liquidity__output-plus"> + </div>
|
</RemoveLiquidityOutputText>
|
||||||
<div className="remove-liquidity__output-text">{`${amountFormatter(
|
</RemoveLiquidityOutput>
|
||||||
tokenWithdrawnMin,
|
|
||||||
decimals,
|
|
||||||
Math.min(4, decimals)
|
|
||||||
)} ${symbol}`}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="remove-liquidity__output" />
|
<RemoveLiquidityOutput />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
disableTokenSelect
|
disableTokenSelect
|
||||||
disableUnlock
|
disableUnlock
|
||||||
/>
|
/>
|
||||||
<OversizedPanel key="remove-liquidity-input-under" hideBottom>
|
<OversizedPanel key="remove-liquidity-input-under" hideBottom>
|
||||||
<div className="pool__summary-panel">
|
<SummaryPanel>
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<ExchangeRateWrapper>
|
||||||
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
|
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
|
||||||
{marketRate ? <span>{`1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}`}</span> : ' - '}
|
{marketRate ? <span>{`1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}`}</span> : ' - '}
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<ExchangeRateWrapper>
|
||||||
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
|
<ExchangeRate>{t('currentPoolSize')}</ExchangeRate>
|
||||||
{exchangeETHBalance && exchangeTokenBalance && decimals ? (
|
{exchangeETHBalance && exchangeTokenBalance && decimals ? (
|
||||||
<span>{`${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter(
|
<span>{`${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter(
|
||||||
exchangeTokenBalance,
|
exchangeTokenBalance,
|
||||||
@ -326,11 +385,11 @@ export default function RemoveLiquidity() {
|
|||||||
) : (
|
) : (
|
||||||
' - '
|
' - '
|
||||||
)}
|
)}
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<ExchangeRateWrapper>
|
||||||
<span className="swap__exchange-rate">
|
<ExchangeRate>
|
||||||
{t('yourPoolShare')} ({ownershipPercentageFormatted && ownershipPercentageFormatted}%)
|
{t('yourPoolShare')} ({ownershipPercentageFormatted && ownershipPercentageFormatted}%)
|
||||||
</span>
|
</ExchangeRate>
|
||||||
{ETHOwnShare && TokenOwnShare ? (
|
{ETHOwnShare && TokenOwnShare ? (
|
||||||
<span>
|
<span>
|
||||||
{`${amountFormatter(ETHOwnShare, 18, 4)} ETH + ${amountFormatter(
|
{`${amountFormatter(ETHOwnShare, 18, 4)} ETH + ${amountFormatter(
|
||||||
@ -342,21 +401,15 @@ export default function RemoveLiquidity() {
|
|||||||
) : (
|
) : (
|
||||||
' - '
|
' - '
|
||||||
)}
|
)}
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
</div>
|
</SummaryPanel>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
{renderSummary()}
|
{renderSummary()}
|
||||||
<div className="pool__cta-container">
|
<Flex>
|
||||||
<button
|
<Button disabled={!isValid} onClick={onRemoveLiquidity}>
|
||||||
className={classnames('pool__cta-btn', {
|
|
||||||
'pool__cta-btn--inactive': !isActive
|
|
||||||
})}
|
|
||||||
disabled={!isValid}
|
|
||||||
onClick={onRemoveLiquidity}
|
|
||||||
>
|
|
||||||
{t('removeLiquidity')}
|
{t('removeLiquidity')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React, { Suspense, lazy, useEffect } from 'react'
|
||||||
import ReactGA from 'react-ga'
|
import ReactGA from 'react-ga'
|
||||||
import { Switch, Route, Redirect } from 'react-router-dom'
|
import { Switch, Route, Redirect } from 'react-router-dom'
|
||||||
|
|
||||||
import ModeSelector from './ModeSelector'
|
import ModeSelector from './ModeSelector'
|
||||||
import AddLiquidity from './AddLiquidity'
|
|
||||||
import CreateExchange from './CreateExchange'
|
|
||||||
import RemoveLiquidity from './RemoveLiquidity'
|
|
||||||
|
|
||||||
import './pool.scss'
|
const AddLiquidity = lazy(() => import('./AddLiquidity'))
|
||||||
|
const RemoveLiquidity = lazy(() => import('./RemoveLiquidity'))
|
||||||
|
const CreateExchange = lazy(() => import('./CreateExchange'))
|
||||||
|
|
||||||
export default function Pool() {
|
export default function Pool() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -17,20 +16,23 @@ export default function Pool() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModeSelector />
|
<ModeSelector />
|
||||||
<Switch>
|
{/* this Suspense is for route code-splitting */}
|
||||||
<Route exact strict path="/add-liquidity" component={AddLiquidity} />
|
<Suspense fallback={null}>
|
||||||
<Route exact strict path="/remove-liquidity" component={RemoveLiquidity} />
|
<Switch>
|
||||||
<Route exact strict path="/create-exchange" component={CreateExchange} />
|
<Route exact strict path="/add-liquidity" component={AddLiquidity} />
|
||||||
<Route
|
<Route exact strict path="/remove-liquidity" component={RemoveLiquidity} />
|
||||||
path="/create-exchange/:tokenAddress"
|
<Route exact strict path="/create-exchange" component={CreateExchange} />
|
||||||
render={({ match }) => {
|
<Route
|
||||||
return (
|
path="/create-exchange/:tokenAddress"
|
||||||
<Redirect to={{ pathname: '/create-exchange', state: { tokenAddress: match.params.tokenAddress } }} />
|
render={({ match }) => {
|
||||||
)
|
return (
|
||||||
}}
|
<Redirect to={{ pathname: '/create-exchange', state: { tokenAddress: match.params.tokenAddress } }} />
|
||||||
/>
|
)
|
||||||
<Redirect to="/add-liquidity" />
|
}}
|
||||||
</Switch>
|
/>
|
||||||
|
<Redirect to="/add-liquidity" />
|
||||||
|
</Switch>
|
||||||
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,187 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.pool {
|
|
||||||
@extend %col-nowrap;
|
|
||||||
height: 100%;
|
|
||||||
background-color: $white;
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
padding: 1rem 0.75rem;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__liquidity-container {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.625rem 1rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: $royal-blue;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 0.75rem;
|
|
||||||
width: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__liquidity-label {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__summary-panel {
|
|
||||||
@extend %col-nowrap;
|
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__last-summary-text {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__exchange-rate-wrapper {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
color: $dove-gray;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.25rem 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__exchange-rate {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
color: $chalice-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__cta-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__cta-btn {
|
|
||||||
@extend %primary-button;
|
|
||||||
margin: 2rem auto 0;
|
|
||||||
|
|
||||||
&--inactive {
|
|
||||||
background: $mercury-gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__new-exchange-warning {
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border: 1px solid rgba($pizazz-orange, 0.4);
|
|
||||||
background-color: rgba($pizazz-orange, 0.1);
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__new-exchange-warning-text {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
padding-bottom: 0.3rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__summary-item {
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pool-modal {
|
|
||||||
background-color: $white;
|
|
||||||
position: relative;
|
|
||||||
bottom: 11.0625rem;
|
|
||||||
width: 100%;
|
|
||||||
height: 11.0625rem;
|
|
||||||
z-index: 2000;
|
|
||||||
border-top-left-radius: 1rem;
|
|
||||||
border-top-right-radius: 1rem;
|
|
||||||
transition: 250ms ease-in-out;
|
|
||||||
padding: 1rem 0 0.5rem;
|
|
||||||
|
|
||||||
@media only screen and (min-width: 768px) {
|
|
||||||
max-width: 560px;
|
|
||||||
position: absolute;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
top: 0;
|
|
||||||
margin-top: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
color: $dove-gray;
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
&--selected {
|
|
||||||
background-color: $white;
|
|
||||||
border-radius: 3rem;
|
|
||||||
box-shadow: 0 0 0.5px 0.5px $mercury-gray;
|
|
||||||
font-weight: 500;
|
|
||||||
color: $royal-blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pool-modal-appear {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pool-modal-appear.modal-container-appear-active {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-exchange {
|
|
||||||
&__summary-panel {
|
|
||||||
color: $dove-gray;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__summary-text {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--error {
|
|
||||||
color: $salmon-red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-liquidity {
|
|
||||||
&__output {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
min-height: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__output-text {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
padding: 1rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__output-plus {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,9 @@ import ReactGA from 'react-ga'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { Button } from '../../theme'
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import NewContextualInfo from '../../components/ContextualInfoNew'
|
import NewContextualInfo from '../../components/ContextualInfoNew'
|
||||||
import OversizedPanel from '../../components/OversizedPanel'
|
import OversizedPanel from '../../components/OversizedPanel'
|
||||||
@ -17,8 +19,6 @@ import { useTransactionAdder } from '../../contexts/Transactions'
|
|||||||
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
||||||
import { useAddressAllowance } from '../../contexts/Allowances'
|
import { useAddressAllowance } from '../../contexts/Allowances'
|
||||||
|
|
||||||
import './send.scss'
|
|
||||||
|
|
||||||
const INPUT = 0
|
const INPUT = 0
|
||||||
const OUTPUT = 1
|
const OUTPUT = 1
|
||||||
|
|
||||||
@ -36,6 +36,52 @@ const DEADLINE_FROM_NOW = 60 * 15
|
|||||||
// denominated in bips
|
// denominated in bips
|
||||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
||||||
|
|
||||||
|
const BlueSpan = styled.span`
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
`
|
||||||
|
|
||||||
|
const DownArrowBackground = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DownArrow = styled.img`
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.875rem;
|
||||||
|
cursor: ${({ clickable }) => clickable && 'pointer'};
|
||||||
|
`
|
||||||
|
|
||||||
|
const LastSummaryText = styled.div`
|
||||||
|
margin-top: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRateWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRate = styled.span`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
color: ${({ theme }) => theme.chaliceGray};
|
||||||
|
`
|
||||||
|
|
||||||
|
const Flex = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
function calculateSlippageBounds(value, token = false) {
|
function calculateSlippageBounds(value, token = false) {
|
||||||
if (value) {
|
if (value) {
|
||||||
const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
|
const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
|
||||||
@ -440,7 +486,11 @@ export default function Swap() {
|
|||||||
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
|
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
|
||||||
: undefined
|
: undefined
|
||||||
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
|
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
|
||||||
const slippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.1')) // 10%
|
const slippageWarning =
|
||||||
|
percentSlippage &&
|
||||||
|
percentSlippage.gte(ethers.utils.parseEther('.05')) &&
|
||||||
|
percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%)
|
||||||
|
const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+%
|
||||||
|
|
||||||
const isValid = exchangeRate && inputError === null && independentError === null && recipientError === null
|
const isValid = exchangeRate && inputError === null && independentError === null && recipientError === null
|
||||||
|
|
||||||
@ -455,7 +505,7 @@ export default function Swap() {
|
|||||||
action: 'Open'
|
action: 'Open'
|
||||||
})
|
})
|
||||||
|
|
||||||
const b = text => <span className="swap__highlight-text">{text}</span>
|
const b = text => <BlueSpan>{text}</BlueSpan>
|
||||||
|
|
||||||
if (independentField === INPUT) {
|
if (independentField === INPUT) {
|
||||||
return (
|
return (
|
||||||
@ -471,7 +521,7 @@ export default function Swap() {
|
|||||||
)}
|
)}
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
<div className="send__last-summary-text">
|
<LastSummaryText>
|
||||||
{b(recipient.address)} {t('willReceive')}{' '}
|
{b(recipient.address)} {t('willReceive')}{' '}
|
||||||
{b(
|
{b(
|
||||||
`${amountFormatter(
|
`${amountFormatter(
|
||||||
@ -481,10 +531,15 @@ export default function Swap() {
|
|||||||
)} ${outputSymbol}`
|
)} ${outputSymbol}`
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{t('orTransFail')}
|
{t('orTransFail')}
|
||||||
</div>
|
</LastSummaryText>
|
||||||
<div className="send__last-summary-text">
|
<LastSummaryText>
|
||||||
|
{(slippageWarning || highSlippageWarning) && (
|
||||||
|
<span role="img" aria-label="warning">
|
||||||
|
⚠️
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
||||||
</div>
|
</LastSummaryText>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -501,7 +556,7 @@ export default function Swap() {
|
|||||||
)}{' '}
|
)}{' '}
|
||||||
{t('to')} {b(recipient.address)}.
|
{t('to')} {b(recipient.address)}.
|
||||||
</div>
|
</div>
|
||||||
<div className="send__last-summary-text">
|
<LastSummaryText>
|
||||||
{t('itWillCost')}{' '}
|
{t('itWillCost')}{' '}
|
||||||
{b(
|
{b(
|
||||||
`${amountFormatter(
|
`${amountFormatter(
|
||||||
@ -511,10 +566,10 @@ export default function Swap() {
|
|||||||
)} ${inputSymbol}`
|
)} ${inputSymbol}`
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{t('orTransFail')}
|
{t('orTransFail')}
|
||||||
</div>
|
</LastSummaryText>
|
||||||
<div className="send__last-summary-text">
|
<LastSummaryText>
|
||||||
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
||||||
</div>
|
</LastSummaryText>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -540,13 +595,21 @@ export default function Swap() {
|
|||||||
isError = true
|
isError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const slippageWarningText = highSlippageWarning
|
||||||
|
? t('highSlippageWarning')
|
||||||
|
: slippageWarning
|
||||||
|
? t('slippageWarning')
|
||||||
|
: ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NewContextualInfo
|
<NewContextualInfo
|
||||||
openDetailsText={t('transactionDetails')}
|
openDetailsText={t('transactionDetails')}
|
||||||
closeDetailsText={t('hideDetails')}
|
closeDetailsText={t('hideDetails')}
|
||||||
contextualInfo={contextualInfo ? contextualInfo : slippageWarning ? t('slippageWarning') : ''}
|
contextualInfo={contextualInfo ? contextualInfo : slippageWarningText}
|
||||||
allowExpand={!!(inputCurrency && outputCurrency && inputValueParsed && outputValueParsed && recipient.address)}
|
allowExpand={!!(inputCurrency && outputCurrency && inputValueParsed && outputValueParsed && recipient.address)}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
|
slippageWarning={slippageWarning && slippageWarningText}
|
||||||
|
highSlippageWarning={highSlippageWarning && slippageWarningText}
|
||||||
renderTransactionDetails={renderTransactionDetails}
|
renderTransactionDetails={renderTransactionDetails}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -652,16 +715,16 @@ export default function Swap() {
|
|||||||
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
|
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
|
||||||
/>
|
/>
|
||||||
<OversizedPanel>
|
<OversizedPanel>
|
||||||
<div className="swap__down-arrow-background">
|
<DownArrowBackground>
|
||||||
<img
|
<DownArrow
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
|
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
|
||||||
}}
|
}}
|
||||||
className="swap__down-arrow swap__down-arrow--clickable"
|
clickable
|
||||||
alt="swap"
|
alt="swap"
|
||||||
src={isValid ? ArrowDownBlue : ArrowDownGrey}
|
src={isValid ? ArrowDownBlue : ArrowDownGrey}
|
||||||
/>
|
/>
|
||||||
</div>
|
</DownArrowBackground>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
title={t('output')}
|
title={t('output')}
|
||||||
@ -680,19 +743,18 @@ export default function Swap() {
|
|||||||
disableUnlock
|
disableUnlock
|
||||||
/>
|
/>
|
||||||
<OversizedPanel>
|
<OversizedPanel>
|
||||||
<div className="swap__down-arrow-background">
|
<DownArrowBackground>
|
||||||
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
|
<DownArrow src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
|
||||||
</div>
|
</DownArrowBackground>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
|
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
|
||||||
<OversizedPanel hideBottom>
|
<OversizedPanel hideBottom>
|
||||||
<div
|
<ExchangeRateWrapper
|
||||||
className="swap__exchange-rate-wrapper"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInverted(inverted => !inverted)
|
setInverted(inverted => !inverted)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
|
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
|
||||||
{inverted ? (
|
{inverted ? (
|
||||||
<span>
|
<span>
|
||||||
{exchangeRate
|
{exchangeRate
|
||||||
@ -706,14 +768,14 @@ export default function Swap() {
|
|||||||
: ' - '}
|
: ' - '}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
{renderSummary()}
|
{renderSummary()}
|
||||||
<div className="swap__cta-container">
|
<Flex>
|
||||||
<button className="swap__cta-btn" disabled={!isValid} onClick={onSwap}>
|
<Button disabled={!isValid} onClick={onSwap} warning={highSlippageWarning}>
|
||||||
{t('swap')}
|
{highSlippageWarning ? t('sendAnyway') : t('send')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.send {
|
|
||||||
@extend %col-nowrap;
|
|
||||||
height: 100%;
|
|
||||||
background-color: $white;
|
|
||||||
|
|
||||||
&__last-summary-text {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,9 @@ import ReactGA from 'react-ga'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { Button } from '../../theme'
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import NewContextualInfo from '../../components/ContextualInfoNew'
|
import NewContextualInfo from '../../components/ContextualInfoNew'
|
||||||
import OversizedPanel from '../../components/OversizedPanel'
|
import OversizedPanel from '../../components/OversizedPanel'
|
||||||
@ -16,8 +18,6 @@ import { useTransactionAdder } from '../../contexts/Transactions'
|
|||||||
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
||||||
import { useAddressAllowance } from '../../contexts/Allowances'
|
import { useAddressAllowance } from '../../contexts/Allowances'
|
||||||
|
|
||||||
import './swap.scss'
|
|
||||||
|
|
||||||
const INPUT = 0
|
const INPUT = 0
|
||||||
const OUTPUT = 1
|
const OUTPUT = 1
|
||||||
|
|
||||||
@ -35,6 +35,52 @@ const DEADLINE_FROM_NOW = 60 * 15
|
|||||||
// denominated in bips
|
// denominated in bips
|
||||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
||||||
|
|
||||||
|
const BlueSpan = styled.span`
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
`
|
||||||
|
|
||||||
|
const LastSummaryText = styled.div`
|
||||||
|
margin-top: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DownArrowBackground = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DownArrow = styled.img`
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.875rem;
|
||||||
|
cursor: ${({ clickable }) => clickable && 'pointer'};
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRateWrapper = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.doveGray};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExchangeRate = styled.span`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
color: ${({ theme }) => theme.chaliceGray};
|
||||||
|
`
|
||||||
|
|
||||||
|
const Flex = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
function calculateSlippageBounds(value, token = false) {
|
function calculateSlippageBounds(value, token = false) {
|
||||||
if (value) {
|
if (value) {
|
||||||
const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
|
const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
|
||||||
@ -435,7 +481,11 @@ export default function Swap() {
|
|||||||
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
|
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
|
||||||
: undefined
|
: undefined
|
||||||
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
|
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
|
||||||
const slippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.1')) // 10%
|
const slippageWarning =
|
||||||
|
percentSlippage &&
|
||||||
|
percentSlippage.gte(ethers.utils.parseEther('.05')) &&
|
||||||
|
percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%)
|
||||||
|
const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+%
|
||||||
|
|
||||||
const isValid = exchangeRate && inputError === null && independentError === null
|
const isValid = exchangeRate && inputError === null && independentError === null
|
||||||
|
|
||||||
@ -450,7 +500,7 @@ export default function Swap() {
|
|||||||
action: 'Open'
|
action: 'Open'
|
||||||
})
|
})
|
||||||
|
|
||||||
const b = text => <span className="swap__highlight-text">{text}</span>
|
const b = text => <BlueSpan>{text}</BlueSpan>
|
||||||
|
|
||||||
if (independentField === INPUT) {
|
if (independentField === INPUT) {
|
||||||
return (
|
return (
|
||||||
@ -466,7 +516,7 @@ export default function Swap() {
|
|||||||
)}
|
)}
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
<div className="send__last-summary-text">
|
<LastSummaryText>
|
||||||
{t('youWillReceive')}{' '}
|
{t('youWillReceive')}{' '}
|
||||||
{b(
|
{b(
|
||||||
`${amountFormatter(
|
`${amountFormatter(
|
||||||
@ -476,10 +526,15 @@ export default function Swap() {
|
|||||||
)} ${outputSymbol}`
|
)} ${outputSymbol}`
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{t('orTransFail')}
|
{t('orTransFail')}
|
||||||
</div>
|
</LastSummaryText>
|
||||||
<div className="send__last-summary-text">
|
<LastSummaryText>
|
||||||
|
{(slippageWarning || highSlippageWarning) && (
|
||||||
|
<span role="img" aria-label="warning">
|
||||||
|
⚠️
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
||||||
</div>
|
</LastSummaryText>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -496,7 +551,7 @@ export default function Swap() {
|
|||||||
)}
|
)}
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
<div className="send__last-summary-text">
|
<LastSummaryText>
|
||||||
{t('itWillCost')}{' '}
|
{t('itWillCost')}{' '}
|
||||||
{b(
|
{b(
|
||||||
`${amountFormatter(
|
`${amountFormatter(
|
||||||
@ -506,10 +561,10 @@ export default function Swap() {
|
|||||||
)} ${inputSymbol}`
|
)} ${inputSymbol}`
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{t('orTransFail')}
|
{t('orTransFail')}
|
||||||
</div>
|
</LastSummaryText>
|
||||||
<div className="send__last-summary-text">
|
<LastSummaryText>
|
||||||
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
||||||
</div>
|
</LastSummaryText>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -531,13 +586,21 @@ export default function Swap() {
|
|||||||
isError = true
|
isError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const slippageWarningText = highSlippageWarning
|
||||||
|
? t('highSlippageWarning')
|
||||||
|
: slippageWarning
|
||||||
|
? t('slippageWarning')
|
||||||
|
: ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NewContextualInfo
|
<NewContextualInfo
|
||||||
openDetailsText={t('transactionDetails')}
|
openDetailsText={t('transactionDetails')}
|
||||||
closeDetailsText={t('hideDetails')}
|
closeDetailsText={t('hideDetails')}
|
||||||
contextualInfo={contextualInfo ? contextualInfo : slippageWarning ? t('slippageWarning') : ''}
|
contextualInfo={contextualInfo ? contextualInfo : slippageWarningText}
|
||||||
allowExpand={!!(inputCurrency && outputCurrency && inputValueParsed && outputValueParsed)}
|
allowExpand={!!(inputCurrency && outputCurrency && inputValueParsed && outputValueParsed)}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
|
slippageWarning={slippageWarning && !contextualInfo}
|
||||||
|
highSlippageWarning={highSlippageWarning && !contextualInfo}
|
||||||
renderTransactionDetails={renderTransactionDetails}
|
renderTransactionDetails={renderTransactionDetails}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -629,16 +692,16 @@ export default function Swap() {
|
|||||||
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
|
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
|
||||||
/>
|
/>
|
||||||
<OversizedPanel>
|
<OversizedPanel>
|
||||||
<div className="swap__down-arrow-background">
|
<DownArrowBackground>
|
||||||
<img
|
<DownArrow
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
|
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
|
||||||
}}
|
}}
|
||||||
className="swap__down-arrow swap__down-arrow--clickable"
|
clickable
|
||||||
alt="swap"
|
alt="swap"
|
||||||
src={isValid ? ArrowDownBlue : ArrowDownGrey}
|
src={isValid ? ArrowDownBlue : ArrowDownGrey}
|
||||||
/>
|
/>
|
||||||
</div>
|
</DownArrowBackground>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
title={t('output')}
|
title={t('output')}
|
||||||
@ -657,13 +720,12 @@ export default function Swap() {
|
|||||||
disableUnlock
|
disableUnlock
|
||||||
/>
|
/>
|
||||||
<OversizedPanel hideBottom>
|
<OversizedPanel hideBottom>
|
||||||
<div
|
<ExchangeRateWrapper
|
||||||
className="swap__exchange-rate-wrapper"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInverted(inverted => !inverted)
|
setInverted(inverted => !inverted)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
|
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
|
||||||
{inverted ? (
|
{inverted ? (
|
||||||
<span>
|
<span>
|
||||||
{exchangeRate
|
{exchangeRate
|
||||||
@ -677,14 +739,14 @@ export default function Swap() {
|
|||||||
: ' - '}
|
: ' - '}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ExchangeRateWrapper>
|
||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
{renderSummary()}
|
{renderSummary()}
|
||||||
<div className="swap__cta-container">
|
<Flex>
|
||||||
<button className="swap__cta-btn" disabled={!isValid} onClick={onSwap}>
|
<Button disabled={!isValid} onClick={onSwap} warning={highSlippageWarning}>
|
||||||
{t('swap')}
|
{highSlippageWarning ? t('swapAnyway') : t('swap')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
@import '../../variables.scss';
|
|
||||||
|
|
||||||
.swap {
|
|
||||||
@extend %col-nowrap;
|
|
||||||
height: 100%;
|
|
||||||
background-color: $white;
|
|
||||||
|
|
||||||
&--inactive {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
padding: 1rem 0.75rem;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__down-arrow {
|
|
||||||
width: 0.625rem;
|
|
||||||
height: 0.625rem;
|
|
||||||
position: relative;
|
|
||||||
z-index: 200;
|
|
||||||
padding: 0.875rem;
|
|
||||||
|
|
||||||
&--clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__down-arrow-background {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__exchange-rate-wrapper {
|
|
||||||
@extend %row-nowrap;
|
|
||||||
align-items: center;
|
|
||||||
color: $dove-gray;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__exchange-rate {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
color: $chalice-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__highlight-text {
|
|
||||||
color: $royal-blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__cta-container {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__cta-btn {
|
|
||||||
@extend %primary-button;
|
|
||||||
margin: 1rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sub-icon {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sub-text {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-modal-appear {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-modal-appear.modal-container-appear-active {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
87
src/theme/components.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import styled, { keyframes } from 'styled-components'
|
||||||
|
import { lighten, darken } from 'polished'
|
||||||
|
|
||||||
|
export const Button = styled.button.attrs(({ warning, theme }) => ({
|
||||||
|
backgroundColor: warning ? theme.salmonRed : theme.royalBlue
|
||||||
|
}))`
|
||||||
|
padding: 1rem 2rem 1rem 2rem;
|
||||||
|
border-radius: 3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||||
|
color: ${({ theme }) => theme.white};
|
||||||
|
transition: background-color 125ms ease-in-out;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:hover,
|
||||||
|
:focus {
|
||||||
|
background-color: ${({ backgroundColor }) => lighten(0.05, backgroundColor)};
|
||||||
|
}
|
||||||
|
|
||||||
|
:active {
|
||||||
|
background-color: ${({ backgroundColor }) => darken(0.05, backgroundColor)};
|
||||||
|
}
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
background-color: ${({ theme }) => theme.mercuryGray};
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Link = styled.a.attrs({
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener noreferrer'
|
||||||
|
})`
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${({ theme }) => theme.royalBlue};
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
outline: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const BorderlessInput = styled.input`
|
||||||
|
color: ${({ theme }) => theme.mineshaftGray};
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
|
||||||
|
[type='number'] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-outer-spin-button,
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: ${({ theme }) => theme.mercuryGray};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const rotate = keyframes`
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Spinner = styled.img`
|
||||||
|
animation: 2s ${rotate} linear infinite;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
`
|
@ -1,12 +1,73 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle } from 'styled-components'
|
import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle, css } from 'styled-components'
|
||||||
|
|
||||||
|
export * from './components'
|
||||||
|
|
||||||
|
const MEDIA_WIDTHS = {
|
||||||
|
upToSmall: 600,
|
||||||
|
upToMedium: 960,
|
||||||
|
upToLarge: 1280
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaWidthTemplates = Object.keys(MEDIA_WIDTHS).reduce((accumulator, size) => {
|
||||||
|
accumulator[size] = (...args) => css`
|
||||||
|
@media (max-width: ${MEDIA_WIDTHS[size]}px) {
|
||||||
|
${css(...args)}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
return accumulator
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const mediaHeightTemplates = Object.keys(MEDIA_WIDTHS).reduce((accumulator, size) => {
|
||||||
|
accumulator[size] = (...args) => css`
|
||||||
|
@media (max-height: ${MEDIA_WIDTHS[size] / (16 / 9)}px) {
|
||||||
|
${css(...args)}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
return accumulator
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const flexColumnNoWrap = css`
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
const flexRowNoWrap = css`
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
`
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
|
white: '#FFFFFF',
|
||||||
|
black: '#000000',
|
||||||
|
// grays
|
||||||
|
concreteGray: '#FAFAFA',
|
||||||
|
mercuryGray: '#E1E1E1',
|
||||||
|
silverGray: '#C4C4C4',
|
||||||
|
chaliceGray: '#AEAEAE',
|
||||||
|
doveGray: '#737373',
|
||||||
|
mineshaftGray: '#2B2B2B',
|
||||||
|
// blues
|
||||||
|
zumthorBlue: '#EBF4FF',
|
||||||
|
malibuBlue: '#5CA2FF',
|
||||||
|
royalBlue: '#2F80ED',
|
||||||
|
// purples
|
||||||
|
wisteriaPurple: '#DC6BE5',
|
||||||
|
// reds
|
||||||
|
salmonRed: '#FF6871',
|
||||||
|
// orange
|
||||||
|
pizazzOrange: '#FF8F05',
|
||||||
|
// yellows
|
||||||
|
warningYellow: '#FFE270',
|
||||||
|
// pink
|
||||||
uniswapPink: '#DC6BE5',
|
uniswapPink: '#DC6BE5',
|
||||||
royalBlue: '#2f80ed',
|
connectedGreen: '#27AE60',
|
||||||
salmonRed: '#ff6871',
|
// media queries
|
||||||
white: '#FFF',
|
mediaWidth: mediaWidthTemplates,
|
||||||
black: '#000'
|
mediaHeight: mediaHeightTemplates,
|
||||||
|
// css snippets
|
||||||
|
flexColumnNoWrap,
|
||||||
|
flexRowNoWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ThemeProvider({ children }) {
|
export default function ThemeProvider({ children }) {
|
||||||
@ -15,60 +76,29 @@ export default function ThemeProvider({ children }) {
|
|||||||
|
|
||||||
export const GlobalStyle = createGlobalStyle`
|
export const GlobalStyle = createGlobalStyle`
|
||||||
@import url('https://rsms.me/inter/inter.css');
|
@import url('https://rsms.me/inter/inter.css');
|
||||||
|
html { font-family: 'Inter', sans-serif; }
|
||||||
|
@supports (font-variation-settings: normal) {
|
||||||
|
html { font-family: 'Inter var', sans-serif; }
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: Inter, sans-serif;
|
padding: 0;
|
||||||
font-variant: none;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
font-variant: none;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
overflow-x: hidden;
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
position: relative;
|
${({ theme }) => theme.flexColumnNoWrap}
|
||||||
display: flex;
|
justify-content: center;
|
||||||
flex-flow: column nowrap;
|
align-items: center;
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: ${props => props.theme.white};
|
|
||||||
z-index: 100;
|
|
||||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
|
||||||
@media only screen and (min-width: 768px) {
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#modal-root {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-top: 1px solid ${props => props.theme.royalBlue};
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 0.75rem;
|
|
||||||
height: 0.75rem;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
animation: spin 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
|
|
||||||
import FACTORY_ABI from '../abi/factory'
|
import FACTORY_ABI from '../constants/abis/factory'
|
||||||
import EXCHANGE_ABI from '../abi/exchange'
|
import EXCHANGE_ABI from '../constants/abis/exchange'
|
||||||
import ERC20_ABI from '../abi/erc20'
|
import ERC20_ABI from '../constants/abis/erc20'
|
||||||
import ERC20_WITH_BYTES_ABI from '../abi/erc20_bytes32'
|
import ERC20_BYTES32_ABI from '../constants/abis/erc20_bytes32'
|
||||||
import { FACTORY_ADDRESSES } from '../constants'
|
import { FACTORY_ADDRESSES } from '../constants'
|
||||||
|
|
||||||
|
import UncheckedJsonRpcSigner from './signer'
|
||||||
|
|
||||||
export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].reduce(
|
export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].reduce(
|
||||||
(accumulator, currentValue, currentIndex) => {
|
(accumulator, currentValue, currentIndex) => {
|
||||||
accumulator[currentValue] = currentIndex
|
accumulator[currentValue] = currentIndex
|
||||||
@ -23,6 +25,61 @@ export function safeAccess(object, path) {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ETHERSCAN_PREFIXES = {
|
||||||
|
1: '',
|
||||||
|
3: 'ropsten.',
|
||||||
|
4: 'rinkeby.',
|
||||||
|
5: 'goerli.',
|
||||||
|
42: 'kovan.'
|
||||||
|
}
|
||||||
|
export function getEtherscanLink(networkId, data, type) {
|
||||||
|
const prefix = `https://${ETHERSCAN_PREFIXES[networkId] || ETHERSCAN_PREFIXES[1]}etherscan.io`
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'transaction': {
|
||||||
|
return `${prefix}/tx/${data}`
|
||||||
|
}
|
||||||
|
case 'address':
|
||||||
|
default: {
|
||||||
|
return `${prefix}/address/${data}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNetworkName(networkId) {
|
||||||
|
switch (networkId) {
|
||||||
|
case 1: {
|
||||||
|
return 'the Main Ethereum Network'
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
return 'the Ropsten Test Network'
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
return 'the Rinkeby Test Network'
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
return 'the Görli Test Network'
|
||||||
|
}
|
||||||
|
case 42: {
|
||||||
|
return 'the Kovan Test Network'
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return 'the correct network'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shortenAddress(address, digits = 4) {
|
||||||
|
if (!isAddress(address)) {
|
||||||
|
throw Error(`Invalid 'address' parameter '${address}'.`)
|
||||||
|
}
|
||||||
|
return `${address.substring(0, digits + 2)}...${address.substring(42 - digits)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shortenTransactionHash(hash, digits = 4) {
|
||||||
|
return `${hash.substring(0, digits + 2)}...${hash.substring(66 - digits)}`
|
||||||
|
}
|
||||||
|
|
||||||
export function isAddress(value) {
|
export function isAddress(value) {
|
||||||
try {
|
try {
|
||||||
return ethers.utils.getAddress(value.toLowerCase())
|
return ethers.utils.getAddress(value.toLowerCase())
|
||||||
@ -38,7 +95,7 @@ export function calculateGasMargin(value, margin) {
|
|||||||
|
|
||||||
// account is optional
|
// account is optional
|
||||||
export function getProviderOrSigner(library, account) {
|
export function getProviderOrSigner(library, account) {
|
||||||
return account ? library.getSigner(account) : library
|
return account ? new UncheckedJsonRpcSigner(library.getSigner(account)) : library
|
||||||
}
|
}
|
||||||
|
|
||||||
// account is optional
|
// account is optional
|
||||||
@ -69,7 +126,7 @@ export async function getTokenName(tokenAddress, library) {
|
|||||||
return getContract(tokenAddress, ERC20_ABI, library)
|
return getContract(tokenAddress, ERC20_ABI, library)
|
||||||
.name()
|
.name()
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
getContract(tokenAddress, ERC20_WITH_BYTES_ABI, library)
|
getContract(tokenAddress, ERC20_BYTES32_ABI, library)
|
||||||
.name()
|
.name()
|
||||||
.then(bytes32 => ethers.utils.parseBytes32String(bytes32))
|
.then(bytes32 => ethers.utils.parseBytes32String(bytes32))
|
||||||
)
|
)
|
||||||
@ -88,7 +145,7 @@ export async function getTokenSymbol(tokenAddress, library) {
|
|||||||
return getContract(tokenAddress, ERC20_ABI, library)
|
return getContract(tokenAddress, ERC20_ABI, library)
|
||||||
.symbol()
|
.symbol()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
const contractBytes32 = getContract(tokenAddress, ERC20_WITH_BYTES_ABI, library)
|
const contractBytes32 = getContract(tokenAddress, ERC20_BYTES32_ABI, library)
|
||||||
return contractBytes32.symbol().then(bytes32 => ethers.utils.parseBytes32String(bytes32))
|
return contractBytes32.symbol().then(bytes32 => ethers.utils.parseBytes32String(bytes32))
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
36
src/utils/signer.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import * as ethers from 'ethers'
|
||||||
|
|
||||||
|
export default class UncheckedJsonRpcSigner extends ethers.Signer {
|
||||||
|
constructor(signer) {
|
||||||
|
super()
|
||||||
|
ethers.utils.defineReadOnly(this, 'signer', signer)
|
||||||
|
ethers.utils.defineReadOnly(this, 'provider', signer.provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddress() {
|
||||||
|
return this.signer.getAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTransaction(transaction) {
|
||||||
|
return this.signer.sendUncheckedTransaction(transaction).then(hash => {
|
||||||
|
return {
|
||||||
|
hash: hash,
|
||||||
|
nonce: null,
|
||||||
|
gasLimit: null,
|
||||||
|
gasPrice: null,
|
||||||
|
data: null,
|
||||||
|
value: null,
|
||||||
|
chainId: null,
|
||||||
|
confirmations: 0,
|
||||||
|
from: null,
|
||||||
|
wait: confirmations => {
|
||||||
|
return this.signer.provider.waitForTransaction(hash, confirmations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
signMessage(message) {
|
||||||
|
return this.signer.signMessage(message)
|
||||||
|
}
|
||||||
|
}
|
@ -1,74 +0,0 @@
|
|||||||
$white: #fff;
|
|
||||||
$black: #000;
|
|
||||||
|
|
||||||
// Gray
|
|
||||||
$concrete-gray: #fafafa;
|
|
||||||
$mercury-gray: #e1e1e1;
|
|
||||||
$silver-gray: #c4c4c4;
|
|
||||||
$chalice-gray: #aeaeae;
|
|
||||||
$dove-gray: #737373;
|
|
||||||
$mine-shaft-gray: #2b2b2b;
|
|
||||||
|
|
||||||
// Blue
|
|
||||||
$zumthor-blue: #ebf4ff;
|
|
||||||
$malibu-blue: #5ca2ff;
|
|
||||||
$royal-blue: #2f80ed;
|
|
||||||
|
|
||||||
// Purple
|
|
||||||
$wisteria-purple: #dc6be5;
|
|
||||||
|
|
||||||
// Red
|
|
||||||
$salmon-red: #ff6871;
|
|
||||||
|
|
||||||
// Orange
|
|
||||||
$pizazz-orange: #ff8f05;
|
|
||||||
|
|
||||||
%col-nowrap {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
%row-nowrap {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
%primary-button {
|
|
||||||
font-size: 1rem;
|
|
||||||
min-width: 16rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: $royal-blue;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 3rem;
|
|
||||||
color: $white;
|
|
||||||
outline: none;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
user-select: none;
|
|
||||||
transition: background-color 300ms ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($royal-blue, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: darken($royal-blue, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: $mercury-gray;
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%borderless-input {
|
|
||||||
color: $mine-shaft-gray;
|
|
||||||
font-size: 1rem;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $mercury-gray;
|
|
||||||
}
|
|
||||||
}
|
|