From b6ca269fdbdca916051ea0586f299b0ccf1f45d1 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 3 Jan 2020 13:59:26 +0100 Subject: [PATCH 01/53] wallet connect page wording (#377) * wallet connect page wording * fixed unused var, better wording? --- src/pages/ConnectWallet.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/ConnectWallet.tsx b/src/pages/ConnectWallet.tsx index de0edd0ff..b176f0408 100644 --- a/src/pages/ConnectWallet.tsx +++ b/src/pages/ConnectWallet.tsx @@ -2,7 +2,6 @@ import React from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faWallet } from '@fortawesome/free-solid-svg-icons' -import Wallet from 'components/UserWallet' import { useWalletConnection } from 'hooks/useWalletConnection' import { Redirect } from 'react-router' import { History } from 'history' @@ -31,9 +30,8 @@ const ConnectWallet: React.FC = (props: ConnectWalletProps) return ( -

Connect Wallet

-

Please connect your wallet first

- +

Wallet Disconnected

+

Please connect your wallet using the button at the top of the page 👆

) } From 844f869e183824cede79200929a1e9c04b75ca81 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 02:36:19 +0000 Subject: [PATCH 02/53] Bump @types/node from 12.12.22 to 13.1.4 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 12.12.22 to 13.1.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 182 +++++---------------------------------------------- 2 files changed, 16 insertions(+), 168 deletions(-) diff --git a/package.json b/package.json index 30a4a6aaa..91fdd8d0a 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@types/enzyme": "^3.10.3", "@types/enzyme-adapter-react-16": "^1.0.5", "@types/jest": "^24.0.18", - "@types/node": "^12.7.5", + "@types/node": "^13.1.4", "@types/qrcode.react": "^1.0.0", "@types/react-copy-to-clipboard": "^4.3.0", "@types/react-dom": "^16.9.4", diff --git a/yarn.lock b/yarn.lock index fe5737e54..a409ef621 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1324,17 +1324,17 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/node@*": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.1.tgz#6d11a8c2d58405b3db9388ab740106cbfa64c3c9" - integrity sha512-hx6zWtudh3Arsbl3cXay+JnkvVgCKzCWKv42C9J01N2T2np4h8w5X8u6Tpz5mj38kE3M9FM0Pazx8vKFFMnjLQ== +"@types/node@*", "@types/node@^13.1.4": + version "13.1.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.4.tgz#4cfd90175a200ee9b02bd6b1cd19bc349741607e" + integrity sha512-Lue/mlp2egZJoHXZr4LndxDAd7i/7SQYhV0EjWfb/a4/OZ6tuVwMCVPiwkU5nsEipxEf7hmkSU7Em5VQ8P5NGA== "@types/node@^10.12.18", "@types/node@^10.3.2": version "10.17.13" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== -"@types/node@^12.6.1", "@types/node@^12.7.5": +"@types/node@^12.6.1": version "12.12.22" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.22.tgz#b8d9eae3328b96910a373cf06ac8d3c5abe9c200" integrity sha512-r5i93jqbPWGXYXxianGATOxTelkp6ih/U0WVnvaqAvTqM+0U6J3kw6Xk6uq/dWNRkEVw/0SLcO5ORXbVNz4FMQ== @@ -1726,11 +1726,6 @@ abab@^2.0.0: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - abstract-leveldown@~2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz#1c5e8c6a5ef965ae8c35dfb3a8770c476b82c4b8" @@ -1904,19 +1899,11 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -aproba@^1.0.3, aproba@^1.1.1: +aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -3569,11 +3556,6 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -3882,7 +3864,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6. dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: +debug@^3.0.0, debug@^3.1.1, debug@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -3992,11 +3974,6 @@ deep-equal@^1.0.1, deep-equal@~1.1.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -4079,11 +4056,6 @@ delegate@^3.1.2: resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -4114,11 +4086,6 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -5755,20 +5722,6 @@ functions-have-names@^1.2.0: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ== -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" @@ -6095,11 +6048,6 @@ has-to-string-tag-x@^1.2.0: dependencies: has-symbol-support-x "^1.4.1" -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -6408,7 +6356,7 @@ husky@^3.0.5: run-node "^1.0.0" slash "^3.0.0" -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -6439,13 +6387,6 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= -ignore-walk@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" - integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== - dependencies: - minimatch "^3.0.4" - ignore@^3.3.5: version "3.3.10" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" @@ -6533,7 +6474,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: +ini@^1.3.4, ini@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== @@ -8437,15 +8378,6 @@ nearley@^2.7.10: randexp "0.4.6" semver "^5.4.1" -needle@^2.2.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" - integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -8546,22 +8478,6 @@ node-notifier@^5.4.2: shellwords "^0.1.1" which "^1.3.0" -node-pre-gyp@*: - version "0.14.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" - integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4.4.2" - node-releases@^1.1.29, node-releases@^1.1.42: version "1.1.44" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.44.tgz#cd66438a6eb875e3eb012b6a12e48d9f4326ffd7" @@ -8569,14 +8485,6 @@ node-releases@^1.1.29, node-releases@^1.1.42: dependencies: semver "^6.3.0" -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= - dependencies: - abbrev "1" - osenv "^0.1.4" - normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -8604,26 +8512,6 @@ normalize-url@^4.1.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== -npm-bundled@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" - integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== - dependencies: - npm-normalize-package-bin "^1.0.1" - -npm-normalize-package-bin@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" - integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== - -npm-packlist@^1.1.6: - version "1.4.7" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.7.tgz#9e954365a06b80b18111ea900945af4f88ed4848" - integrity sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -8631,16 +8519,6 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - nth-check@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -8887,19 +8765,11 @@ os-locale@^3.0.0, os-locale@^3.1.0: lcid "^2.0.0" mem "^4.0.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - p-cancelable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" @@ -9688,16 +9558,6 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - react-copy-to-clipboard@^5.0.1: version "5.0.2" resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz#d82a437e081e68dfca3761fbd57dbf2abdda1316" @@ -9935,7 +9795,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -10317,7 +10177,7 @@ rimraf@2.6.3: dependencies: glob "^7.1.3" -rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: +rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -10608,7 +10468,7 @@ servify@^0.1.12: request "^2.79.0" xhr "^2.3.3" -set-blocking@^2.0.0, set-blocking@~2.0.0: +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -11059,7 +10919,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -11186,11 +11046,6 @@ strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - style-loader@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.1.2.tgz#1b519c19faf548df6182b93e72ea1a4156022c2f" @@ -11340,7 +11195,7 @@ tar-stream@^1.5.2: to-buffer "^1.1.1" xtend "^4.0.0" -tar@^4.0.2, tar@^4.4.2: +tar@^4.0.2: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== @@ -12458,13 +12313,6 @@ which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" From 0bc222e8292cc3255166ff03fac1eae169e6608c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 02:36:47 +0000 Subject: [PATCH 03/53] Bump @typescript-eslint/parser from 2.13.0 to 2.14.0 Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 2.13.0 to 2.14.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v2.14.0/packages/parser) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index a409ef621..24118fdeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1462,7 +1462,7 @@ regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.13.0", "@typescript-eslint/experimental-utils@^2.5.0": +"@typescript-eslint/experimental-utils@2.13.0": version "2.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.13.0.tgz#958614faa6f77599ee2b241740e0ea402482533d" integrity sha512-+Hss3clwa6aNiC8ZjA45wEm4FutDV5HsVXPl/rDug1THq6gEtOYRGLqS3JlTk7mSnL5TbJz0LpEbzbPnKvY6sw== @@ -1471,14 +1471,23 @@ "@typescript-eslint/typescript-estree" "2.13.0" eslint-scope "^5.0.0" +"@typescript-eslint/experimental-utils@2.14.0", "@typescript-eslint/experimental-utils@^2.5.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.14.0.tgz#e9179fa3c44e00b3106b85d7b69342901fb43e3b" + integrity sha512-KcyKS7G6IWnIgl3ZpyxyBCxhkBPV+0a5Jjy2g5HxlrbG2ZLQNFeneIBVXdaBCYOVjvGmGGFKom1kgiAY75SDeQ== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.14.0" + eslint-scope "^5.0.0" + "@typescript-eslint/parser@^2.8.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.13.0.tgz#ea1ab394cf9ca17467e3da7f96eca9309f57c326" - integrity sha512-vbDeLr5QRJ1K7x5iRK8J9wuGwR9OVyd1zDAY9XFAQvAosHVjSVbDgkm328ayE6hx2QWVGhwvGaEhedcqAbfQcA== + version "2.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.14.0.tgz#30fa0523d86d74172a5e32274558404ba4262cd6" + integrity sha512-haS+8D35fUydIs+zdSf4BxpOartb/DjrZ2IxQ5sR8zyGfd77uT9ZJZYF8+I0WPhzqHmfafUBx8MYpcp8pfaoSA== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.13.0" - "@typescript-eslint/typescript-estree" "2.13.0" + "@typescript-eslint/experimental-utils" "2.14.0" + "@typescript-eslint/typescript-estree" "2.14.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@2.13.0": @@ -1494,6 +1503,19 @@ semver "^6.3.0" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@2.14.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.14.0.tgz#c67698acdc14547f095eeefe908958d93e1a648d" + integrity sha512-pnLpUcMNG7GfFFfNQbEX6f1aPa5fMnH2G9By+A1yovYI4VIOK2DzkaRuUlIkbagpAcrxQHLqovI1YWqEcXyRnA== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + tsutils "^3.17.1" + "@walletconnect/browser@^1.0.0-beta.42": version "1.0.0-beta.42" resolved "https://registry.yarnpkg.com/@walletconnect/browser/-/browser-1.0.0-beta.42.tgz#913da3af0704dd5c9b8b881c593793275cd18e56" From 2f9622daedf40c1b75c262a78b28463780526f8d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 02:37:17 +0000 Subject: [PATCH 04/53] Bump css-loader from 3.4.0 to 3.4.1 Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 3.4.0 to 3.4.1. - [Release notes](https://github.com/webpack-contrib/css-loader/releases) - [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/css-loader/compare/v3.4.0...v3.4.1) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 24118fdeb..e090b0aca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3782,9 +3782,9 @@ css-color-keywords@^1.0.0: integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= css-loader@^3.2.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.4.0.tgz#9fb263436783117a41d014e45e8eaeba54dd6670" - integrity sha512-JornYo4RAXl1Mzt0lOSVPmArzAMV3rGY2VuwtaDc732WTWjdwTaeS19nCGWMcSCf305Q396lhhDAJEWWM0SgPQ== + version "3.4.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.4.1.tgz#dfb7968aa9bffb26bd20375afdffe77d5a234b77" + integrity sha512-+ybmv7sVxxNEenQhkifQDvny/1iNQM7YooJbSfVUdQQvisyg1aKIqgGjCjoFSyVLJMp17z9rfZFQaR5HGHcMbw== dependencies: camelcase "^5.3.1" cssesc "^3.0.0" From 6c8f46c22fad7b1f5ef51ad9b2dc8fff7d731c82 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 02:37:47 +0000 Subject: [PATCH 05/53] Bump react-toastify from 5.4.1 to 5.5.0 Bumps [react-toastify](https://github.com/fkhadra/react-toastify) from 5.4.1 to 5.5.0. - [Release notes](https://github.com/fkhadra/react-toastify/releases) - [Commits](https://github.com/fkhadra/react-toastify/compare/v5.4.1...v5.5.0) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index e090b0aca..5271e1787 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9735,9 +9735,9 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0: scheduler "^0.18.0" react-toastify@^5.4.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-5.4.1.tgz#27533ca33753631016c44122934c7b6548352fcb" - integrity sha512-24EwkWrj47Id/HGjYfdcntaZpAQ3J5NX31SnGRD66hM/KvPKVJzPiDBPZ+/RZ3SvNkbNWfHpPKFWzenJjC26hg== + version "5.5.0" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-5.5.0.tgz#f55de44f6b5e3ce3b13b69e5bb4427f2c9404822" + integrity sha512-jsVme7jALIFGRyQsri/g4YTsRuaaGI70T6/ikjwZMB4mwTZaCWqj5NqxhGrRStKlJc5npXKKvKeqTiRGQl78LQ== dependencies: "@babel/runtime" "^7.4.2" classnames "^2.2.6" From e7876deb1981e53ccb42461bb295ad61d2546991 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 02:38:19 +0000 Subject: [PATCH 06/53] Bump @typescript-eslint/eslint-plugin from 2.13.0 to 2.14.0 Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 2.13.0 to 2.14.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v2.14.0/packages/eslint-plugin) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5271e1787..cbcf2a0bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1452,11 +1452,11 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^2.8.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.13.0.tgz#57e933fe16a2fc66dbac059af0d6d85d921d748e" - integrity sha512-QoiANo0MMGNa8ej/yX3BrW5dZj5d8HYcKiM2fyYUlezECqn8Xc7T/e4EUdiGinn8jhBrn+9X47E9TWaaup3u1g== + version "2.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.14.0.tgz#c74447400537d4eb7aae1e31879ab43e6c662a8a" + integrity sha512-sneOJ3Hu0m5whJiVIxGBZZZMxMJ7c0LhAJzeMJgHo+n5wFs+/6rSR/gl7crkdR2kNwfOOSdzdc0gMvatG4dX2Q== dependencies: - "@typescript-eslint/experimental-utils" "2.13.0" + "@typescript-eslint/experimental-utils" "2.14.0" eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" From eabd72d7088ab235c082a04ec43cb2519987c5d3 Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Wed, 8 Jan 2020 09:01:03 -0800 Subject: [PATCH 07/53] 329/refactor api params (#388) * Renaming transfer and transferFrom parameter for consistency * Added interface for all Erc20Api methods rather than inplace type definition * Moved txOptionalParams into params interface for Erc20Api * Added interfaces for most of DepositApi methods * Moved txOptionalParams into params interface for DepositApi * Renamed cancelOrders param senderAddress to userAddress for consistency * Added CancelOrderParams interface * Added AddTokenParams interface * We need to pass along userAddress when adding token * Added interfaces for remainder ExchangeApi methods * Moved txOptionalParams into params interface for ExchangeApi * Final touch on api interfaces * One interface per method on DepositApi * One interface per method on Erc20Api --- src/api/deposit/DepositApi.ts | 100 ++++------ src/api/deposit/DepositApiMock.ts | 77 +++----- src/api/erc20/Erc20Api.ts | 172 ++++++++---------- src/api/erc20/Erc20ApiMock.ts | 119 ++++++------ src/api/exchange/ExchangeApi.ts | 74 +++++--- src/api/exchange/ExchangeApiMock.ts | 58 +++--- .../DepositWidget/useRowActions.tsx | 15 +- .../OrdersWidget/useDeleteOrders.tsx | 2 +- src/hooks/useEnableToken.ts | 17 +- src/hooks/useOrders.ts | 2 +- src/hooks/usePlaceOrder.ts | 7 +- src/hooks/useWithdrawTokens.ts | 3 +- src/services/factories/addTokenToExchange.ts | 15 +- .../factories/getTokenFromExchange.ts | 4 +- test/api/ExchangeApi/Erc20ApiMock.test.ts | 57 +++--- test/api/ExchangeApi/ExchangeApiMock.test.ts | 46 ++--- 16 files changed, 370 insertions(+), 398 deletions(-) diff --git a/src/api/deposit/DepositApi.ts b/src/api/deposit/DepositApi.ts index bf9c040c2..3266e8c5f 100644 --- a/src/api/deposit/DepositApi.ts +++ b/src/api/deposit/DepositApi.ts @@ -9,46 +9,40 @@ import { Receipt, TxOptionalParams } from 'types' import Web3 from 'web3' import { getProviderState, Provider, ProviderState } from '@gnosis.pm/dapp-ui' +interface ReadOnlyParams { + userAddress: string + tokenAddress: string +} + +export type GetBalanceParams = ReadOnlyParams +export type GetPendingDepositParams = ReadOnlyParams +export type GetPendingWithdrawParams = ReadOnlyParams + +interface WithTxOptionalParams { + txOptionalParams?: TxOptionalParams +} + +export interface DepositParams extends ReadOnlyParams, WithTxOptionalParams { + amount: BN +} + +export type RequestWithdrawParams = DepositParams + +export type WithdrawParams = Omit + export interface DepositApi { getContractAddress(networkId: number): string | null getBatchTime(): Promise getCurrentBatchId(): Promise getSecondsRemainingInBatch(): Promise - getBalance({ userAddress, tokenAddress }: { userAddress: string; tokenAddress: string }): Promise - getPendingDeposit({ userAddress, tokenAddress }: { userAddress: string; tokenAddress: string }): Promise - getPendingWithdraw({ userAddress, tokenAddress }: { userAddress: string; tokenAddress: string }): Promise - - deposit( - { - userAddress, - tokenAddress, - amount, - }: { - userAddress: string - tokenAddress: string - amount: BN - }, - txOptionalParams?: TxOptionalParams, - ): Promise - - requestWithdraw( - { - userAddress, - tokenAddress, - amount, - }: { - userAddress: string - tokenAddress: string - amount: BN - }, - txOptionalParams?: TxOptionalParams, - ): Promise - - withdraw( - { userAddress, tokenAddress }: { userAddress: string; tokenAddress: string }, - txOptionalParams?: TxOptionalParams, - ): Promise + getBalance(params: GetBalanceParams): Promise + getPendingDeposit(params: GetPendingDepositParams): Promise + getPendingWithdraw(params: GetPendingWithdrawParams): Promise + + deposit(params: DepositParams): Promise + requestWithdraw(params: RequestWithdrawParams): Promise + withdraw(params: WithdrawParams): Promise } export interface PendingFlux { @@ -103,7 +97,7 @@ export class DepositApiImpl implements DepositApi { return +secondsRemainingInBatch } - public async getBalance({ userAddress, tokenAddress }: { userAddress: string; tokenAddress: string }): Promise { + public async getBalance({ userAddress, tokenAddress }: GetBalanceParams): Promise { if (!userAddress || !tokenAddress) return ZERO const contract = await this._getContract() @@ -112,13 +106,7 @@ export class DepositApiImpl implements DepositApi { return toBN(balance) } - public async getPendingDeposit({ - userAddress, - tokenAddress, - }: { - userAddress: string - tokenAddress: string - }): Promise { + public async getPendingDeposit({ userAddress, tokenAddress }: GetPendingDepositParams): Promise { if (!userAddress || !tokenAddress) return { amount: ZERO, batchId: 0 } const contract = await this._getContract() @@ -128,13 +116,7 @@ export class DepositApiImpl implements DepositApi { return { amount: toBN(amount), batchId: Number(batchId) } } - public async getPendingWithdraw({ - userAddress, - tokenAddress, - }: { - userAddress: string - tokenAddress: string - }): Promise { + public async getPendingWithdraw({ userAddress, tokenAddress }: GetPendingWithdrawParams): Promise { if (!userAddress || !tokenAddress) return { amount: ZERO, batchId: 0 } const contract = await this._getContract() @@ -144,10 +126,7 @@ export class DepositApiImpl implements DepositApi { return { amount: toBN(amount), batchId: Number(batchId) } } - public async deposit( - { userAddress, tokenAddress, amount }: { userAddress: string; tokenAddress: string; amount: BN }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async deposit({ userAddress, tokenAddress, amount, txOptionalParams }: DepositParams): Promise { const contract = await this._getContract() // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = contract.methods.deposit(tokenAddress, amount.toString()).send({ from: userAddress }) @@ -160,10 +139,12 @@ export class DepositApiImpl implements DepositApi { return tx } - public async requestWithdraw( - { userAddress, tokenAddress, amount }: { userAddress: string; tokenAddress: string; amount: BN }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async requestWithdraw({ + userAddress, + tokenAddress, + amount, + txOptionalParams, + }: RequestWithdrawParams): Promise { const contract = await this._getContract() // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = contract.methods.requestWithdraw(tokenAddress, amount.toString()).send({ from: userAddress }) @@ -176,10 +157,7 @@ export class DepositApiImpl implements DepositApi { return tx } - public async withdraw( - { userAddress, tokenAddress }: { userAddress: string; tokenAddress: string }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async withdraw({ userAddress, tokenAddress, txOptionalParams }: WithdrawParams): Promise { const contract = await this._getContract() const tx = contract.methods.withdraw(userAddress, tokenAddress).send({ from: userAddress }) diff --git a/src/api/deposit/DepositApiMock.ts b/src/api/deposit/DepositApiMock.ts index a1cfd66a3..6d603dfc1 100644 --- a/src/api/deposit/DepositApiMock.ts +++ b/src/api/deposit/DepositApiMock.ts @@ -5,9 +5,18 @@ import { getEpoch, log } from 'utils' import { ZERO, BATCH_TIME } from 'const' import { CONTRACT, RECEIPT, createFlux } from '../../../test/data' -import { Receipt, TxOptionalParams } from 'types' +import { Receipt } from 'types' import { waitAndSendReceipt } from 'utils/mock' -import { DepositApi, PendingFlux } from './DepositApi' +import { + DepositApi, + PendingFlux, + GetBalanceParams, + GetPendingDepositParams, + GetPendingWithdrawParams, + RequestWithdrawParams, + WithdrawParams, + DepositParams, +} from './DepositApi' import { Erc20Api } from 'api/erc20/Erc20Api' export interface BalanceState { @@ -45,7 +54,7 @@ export class DepositApiMock implements DepositApi { return BATCH_TIME - (getEpoch() % BATCH_TIME) } - public async getBalance({ userAddress, tokenAddress }: { userAddress: string; tokenAddress: string }): Promise { + public async getBalance({ userAddress, tokenAddress }: GetBalanceParams): Promise { const userBalanceStates = this._balanceStates[userAddress] if (!userBalanceStates) { return ZERO @@ -55,13 +64,7 @@ export class DepositApiMock implements DepositApi { return balanceState ? balanceState.balance : ZERO } - public async getPendingDeposit({ - userAddress, - tokenAddress, - }: { - userAddress: string - tokenAddress: string - }): Promise { + public async getPendingDeposit({ userAddress, tokenAddress }: GetPendingDepositParams): Promise { const userBalanceStates = this._balanceStates[userAddress] if (!userBalanceStates) { return createFlux() @@ -71,13 +74,7 @@ export class DepositApiMock implements DepositApi { return balanceState ? balanceState.pendingDeposits : createFlux() } - public async getPendingWithdraw({ - userAddress, - tokenAddress, - }: { - userAddress: string - tokenAddress: string - }): Promise { + public async getPendingWithdraw({ userAddress, tokenAddress }: GetPendingWithdrawParams): Promise { const userBalanceStates = this._balanceStates[userAddress] if (!userBalanceStates) { return createFlux() @@ -86,18 +83,7 @@ export class DepositApiMock implements DepositApi { return balanceState ? balanceState.pendingWithdraws : createFlux() } - public async deposit( - { - userAddress, - tokenAddress, - amount, - }: { - userAddress: string - tokenAddress: string - amount: BN - }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async deposit({ userAddress, tokenAddress, amount, txOptionalParams }: DepositParams): Promise { await waitAndSendReceipt({ txOptionalParams }) // Create the balance state if it's the first deposit @@ -113,7 +99,7 @@ export class DepositApiMock implements DepositApi { // mock transfer tokens from user's mock `wallet` await this._erc20Api.transferFrom({ - senderAddress: this.getContractAddress(), + userAddress: this.getContractAddress(), tokenAddress, fromAddress: userAddress, toAddress: this.getContractAddress(), @@ -124,18 +110,12 @@ export class DepositApiMock implements DepositApi { return RECEIPT } - public async requestWithdraw( - { - userAddress, - tokenAddress, - amount, - }: { - userAddress: string - tokenAddress: string - amount: BN - }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async requestWithdraw({ + userAddress, + tokenAddress, + amount, + txOptionalParams, + }: RequestWithdrawParams): Promise { await waitAndSendReceipt({ txOptionalParams }) const currentBatchId = await this.getCurrentBatchId() @@ -150,16 +130,7 @@ export class DepositApiMock implements DepositApi { return RECEIPT } - public async withdraw( - { - userAddress, - tokenAddress, - }: { - userAddress: string - tokenAddress: string - }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async withdraw({ userAddress, tokenAddress, txOptionalParams }: WithdrawParams): Promise { await waitAndSendReceipt({ txOptionalParams }) const currentBatchId = await this.getCurrentBatchId() @@ -179,7 +150,7 @@ export class DepositApiMock implements DepositApi { // mock transfer tokens to user's mock `wallet` await this._erc20Api.transfer({ - fromAddress: this.getContractAddress(), + userAddress: this.getContractAddress(), tokenAddress, toAddress: userAddress, amount, diff --git a/src/api/erc20/Erc20Api.ts b/src/api/erc20/Erc20Api.ts index 546dfa878..4fa09961d 100644 --- a/src/api/erc20/Erc20Api.ts +++ b/src/api/erc20/Erc20Api.ts @@ -9,64 +9,60 @@ import { toBN } from 'utils' import Web3 from 'web3' +interface BaseParams { + tokenAddress: string +} + +export type NameParams = BaseParams +export type SymbolParams = BaseParams +export type DecimalsParams = BaseParams +export type TotalSupplyParams = BaseParams + +export interface BalanceOfParams extends BaseParams { + userAddress: string +} + +export interface AllowanceParams extends BalanceOfParams { + spenderAddress: string +} + +interface WithTxOptionalParams { + txOptionalParams?: TxOptionalParams +} + +export interface ApproveParams extends AllowanceParams, WithTxOptionalParams { + amount: BN +} + +export interface TransferParams extends BalanceOfParams, WithTxOptionalParams { + toAddress: string + amount: BN +} + +export interface TransferFromParams extends TransferParams { + fromAddress: string +} + /** * Interfaces the access to ERC20 token * * See: https://theethereum.wiki/w/index.php/ERC20_Token_Standard */ export interface Erc20Api { - balanceOf({ tokenAddress, userAddress }: { tokenAddress: string; userAddress: string }): Promise - name({ tokenAddress }: { tokenAddress: string }): Promise - symbol({ tokenAddress }: { tokenAddress: string }): Promise - decimals({ tokenAddress }: { tokenAddress: string }): Promise - totalSupply({ tokenAddress }: { tokenAddress: string }): Promise + name(params: NameParams): Promise + symbol(params: SymbolParams): Promise + decimals(params: DecimalsParams): Promise + totalSupply(params: TotalSupplyParams): Promise - allowance({ - tokenAddress, - userAddress, - spenderAddress, - }: { - tokenAddress: string - userAddress: string - spenderAddress: string - }): Promise - - approve( - { - userAddress, - tokenAddress, - spenderAddress, - amount, - }: { userAddress: string; tokenAddress: string; spenderAddress: string; amount: BN }, - txOptionalParams?: TxOptionalParams, - ): Promise - - transfer( - { - fromAddress, - tokenAddress, - toAddress, - amount, - }: { fromAddress: string; tokenAddress: string; toAddress: string; amount: BN }, - txOptionalParams?: TxOptionalParams, - ): Promise - - transferFrom( - { - senderAddress, - tokenAddress, - fromAddress, - toAddress, - amount, - }: { - senderAddress: string - tokenAddress: string - fromAddress: string - toAddress: string - amount: BN - }, - txOptionalParams?: TxOptionalParams, - ): Promise + balanceOf(params: BalanceOfParams): Promise + + allowance(params: AllowanceParams): Promise + + approve(params: ApproveParams): Promise + + transfer(params: TransferParams): Promise + + transferFrom(params: TransferFromParams): Promise } /** @@ -85,7 +81,7 @@ export class Erc20ApiImpl implements Erc20Api { ;(window as any).erc20 = this._contractPrototype } - public async balanceOf({ tokenAddress, userAddress }: { tokenAddress: string; userAddress: string }): Promise { + public async balanceOf({ tokenAddress, userAddress }: BalanceOfParams): Promise { if (!userAddress || !tokenAddress) return ZERO const erc20 = this._getERC20AtAddress(tokenAddress) @@ -95,19 +91,19 @@ export class Erc20ApiImpl implements Erc20Api { return toBN(result) } - public async name({ tokenAddress }: { tokenAddress: string }): Promise { + public async name({ tokenAddress }: NameParams): Promise { const erc20 = this._getERC20AtAddress(tokenAddress) return await erc20.methods.name().call() } - public async symbol({ tokenAddress }: { tokenAddress: string }): Promise { + public async symbol({ tokenAddress }: SymbolParams): Promise { const erc20 = this._getERC20AtAddress(tokenAddress) return await erc20.methods.symbol().call() } - public async decimals({ tokenAddress }: { tokenAddress: string }): Promise { + public async decimals({ tokenAddress }: DecimalsParams): Promise { const erc20 = this._getERC20AtAddress(tokenAddress) const decimals = await erc20.methods.decimals().call() @@ -115,7 +111,7 @@ export class Erc20ApiImpl implements Erc20Api { return Number(decimals) } - public async totalSupply({ tokenAddress }: { tokenAddress: string }): Promise { + public async totalSupply({ tokenAddress }: TotalSupplyParams): Promise { const erc20 = this._getERC20AtAddress(tokenAddress) const totalSupply = await erc20.methods.totalSupply().call() @@ -123,15 +119,7 @@ export class Erc20ApiImpl implements Erc20Api { return toBN(totalSupply) } - public async allowance({ - tokenAddress, - userAddress, - spenderAddress, - }: { - tokenAddress: string - userAddress: string - spenderAddress: string - }): Promise { + public async allowance({ tokenAddress, userAddress, spenderAddress }: AllowanceParams): Promise { if (!userAddress || !tokenAddress) return ZERO const erc20 = this._getERC20AtAddress(tokenAddress) @@ -141,15 +129,13 @@ export class Erc20ApiImpl implements Erc20Api { return toBN(result) } - public async approve( - { - userAddress, - tokenAddress, - spenderAddress, - amount, - }: { userAddress: string; tokenAddress: string; spenderAddress: string; amount: BN }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async approve({ + userAddress, + tokenAddress, + spenderAddress, + amount, + txOptionalParams, + }: ApproveParams): Promise { const erc20 = this._getERC20AtAddress(tokenAddress) // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 @@ -164,20 +150,18 @@ export class Erc20ApiImpl implements Erc20Api { return tx } - public async transfer( - { - fromAddress, - tokenAddress, - toAddress, - amount, - }: { fromAddress: string; tokenAddress: string; toAddress: string; amount: BN }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async transfer({ + userAddress, + tokenAddress, + toAddress, + amount, + txOptionalParams, + }: TransferParams): Promise { const erc20 = this._getERC20AtAddress(tokenAddress) // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = erc20.methods.transfer(toAddress, amount.toString()).send({ - from: fromAddress, + from: userAddress, }) if (txOptionalParams?.onSentTransaction) { @@ -187,19 +171,17 @@ export class Erc20ApiImpl implements Erc20Api { return tx } - public async transferFrom( - { - senderAddress, - tokenAddress, - fromAddress, - toAddress, - amount, - }: { senderAddress: string; tokenAddress: string; fromAddress: string; toAddress: string; amount: BN }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async transferFrom({ + userAddress, + tokenAddress, + fromAddress, + toAddress, + amount, + txOptionalParams, + }: TransferFromParams): Promise { const erc20 = this._getERC20AtAddress(tokenAddress) - const tx = erc20.methods.transferFrom(senderAddress, toAddress, amount.toString()).send({ + const tx = erc20.methods.transferFrom(userAddress, toAddress, amount.toString()).send({ from: fromAddress, }) diff --git a/src/api/erc20/Erc20ApiMock.ts b/src/api/erc20/Erc20ApiMock.ts index c09049281..043ffd447 100644 --- a/src/api/erc20/Erc20ApiMock.ts +++ b/src/api/erc20/Erc20ApiMock.ts @@ -1,11 +1,22 @@ import BN from 'bn.js' -import { TxOptionalParams, Receipt } from 'types' +import { Receipt } from 'types' import { ZERO, ALLOWANCE_MAX_VALUE } from 'const' import { RECEIPT } from '../../../test/data' import { log, assert } from 'utils' import { waitAndSendReceipt } from 'utils/mock' -import { Erc20Api } from './Erc20Api' +import { + Erc20Api, + NameParams, + SymbolParams, + DecimalsParams, + TotalSupplyParams, + BalanceOfParams, + AllowanceParams, + ApproveParams, + TransferParams, + TransferFromParams, +} from './Erc20Api' interface Balances { [userAddress: string]: { [tokenAddress: string]: BN } @@ -41,7 +52,7 @@ export class Erc20ApiMock implements Erc20Api { this._tokens = tokens } - public async balanceOf({ tokenAddress, userAddress }: { tokenAddress: string; userAddress: string }): Promise { + public async balanceOf({ tokenAddress, userAddress }: BalanceOfParams): Promise { const userBalances = this._balances[userAddress] if (!userBalances) { return ZERO @@ -51,7 +62,7 @@ export class Erc20ApiMock implements Erc20Api { return balance ? balance : ZERO } - public async name({ tokenAddress }: { tokenAddress: string }): Promise { + public async name({ tokenAddress }: NameParams): Promise { const erc20Info = this._initTokens(tokenAddress) // Throws when token without `name` to mock contract behavior @@ -60,7 +71,7 @@ export class Erc20ApiMock implements Erc20Api { return erc20Info.name } - public async symbol({ tokenAddress }: { tokenAddress: string }): Promise { + public async symbol({ tokenAddress }: SymbolParams): Promise { const erc20Info = this._initTokens(tokenAddress) // Throws when token without `symbol` to mock contract behavior @@ -69,7 +80,7 @@ export class Erc20ApiMock implements Erc20Api { return erc20Info.symbol } - public async decimals({ tokenAddress }: { tokenAddress: string }): Promise { + public async decimals({ tokenAddress }: DecimalsParams): Promise { const erc20Info = this._initTokens(tokenAddress) // Throws when token without `decimals` to mock contract behavior @@ -78,20 +89,12 @@ export class Erc20ApiMock implements Erc20Api { return erc20Info.decimals } - public async totalSupply({ tokenAddress }: { tokenAddress: string }): Promise { + public async totalSupply({ tokenAddress }: TotalSupplyParams): Promise { log("Don't care about %s, just making TS shut up", tokenAddress) return this._totalSupply } - public async allowance({ - tokenAddress, - userAddress, - spenderAddress, - }: { - tokenAddress: string - userAddress: string - spenderAddress: string - }): Promise { + public async allowance({ tokenAddress, userAddress, spenderAddress }: AllowanceParams): Promise { const userAllowances = this._allowances[userAddress] if (!userAllowances) { return ZERO @@ -106,15 +109,13 @@ export class Erc20ApiMock implements Erc20Api { return allowance ? allowance : ZERO } - public async approve( - { - userAddress, - tokenAddress, - spenderAddress, - amount, - }: { userAddress: string; tokenAddress: string; spenderAddress: string; amount: BN }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async approve({ + userAddress, + tokenAddress, + spenderAddress, + amount, + txOptionalParams, + }: ApproveParams): Promise { await waitAndSendReceipt({ txOptionalParams }) this._initAllowances({ userAddress, tokenAddress, spenderAddress }) @@ -127,89 +128,79 @@ export class Erc20ApiMock implements Erc20Api { } /** - * Transfers from `senderAddress`. No allowance required. + * Transfers from `userAddress`. No allowance required. * - * @param senderAddress The sender of the tx + * @param userAddress The sender of the tx * @param tokenAddress The token being transferred * @param toAddress The recipient's address * @param amount The amount transferred * @param txOptionalParams Optional params */ - public async transfer( - { - fromAddress, - tokenAddress, - toAddress, - amount, - }: { fromAddress: string; tokenAddress: string; toAddress: string; amount: BN }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async transfer({ + userAddress, + tokenAddress, + toAddress, + amount, + txOptionalParams, + }: TransferParams): Promise { await waitAndSendReceipt({ txOptionalParams }) - this._initBalances({ userAddress: fromAddress, tokenAddress }) + this._initBalances({ userAddress, tokenAddress }) this._initBalances({ userAddress: toAddress, tokenAddress }) - const balance = this._balances[fromAddress][tokenAddress] + const balance = this._balances[userAddress][tokenAddress] assert(balance.gte(amount), "The user doesn't have enough balance") - this._balances[fromAddress][tokenAddress] = balance.sub(amount) + this._balances[userAddress][tokenAddress] = balance.sub(amount) this._balances[toAddress][tokenAddress] = this._balances[toAddress][tokenAddress].add(amount) log( - `[Erc20ApiMock:transfer] Transferred ${amount} of the token ${tokenAddress} from ${fromAddress} to ${toAddress}`, + `[Erc20ApiMock:transfer] Transferred ${amount} of the token ${tokenAddress} from ${userAddress} to ${toAddress}`, ) return RECEIPT } /** - * Transfers on behalf of `fromAddress` if `senderAddress` has allowance + * Transfers on behalf of `fromAddress` if `userAddress` has allowance * - * @param senderAddress The sender of the tx + * @param userAddress The sender of the tx * @param tokenAddress The token being transferred * @param fromAddress The source of the tokens * @param toAddress The recipient's address * @param amount The amount transferred * @param txOptionalParams Optional params */ - public async transferFrom( - { - senderAddress, - tokenAddress, - fromAddress, - toAddress, - amount, - }: { - senderAddress: string - tokenAddress: string - fromAddress: string - toAddress: string - amount: BN - }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async transferFrom({ + userAddress, + tokenAddress, + fromAddress, + toAddress, + amount, + txOptionalParams, + }: TransferFromParams): Promise { await waitAndSendReceipt({ txOptionalParams }) this._initBalances({ userAddress: fromAddress, tokenAddress }) this._initBalances({ userAddress: toAddress, tokenAddress }) - this._initAllowances({ userAddress: fromAddress, tokenAddress, spenderAddress: senderAddress }) + this._initAllowances({ userAddress: fromAddress, tokenAddress, spenderAddress: userAddress }) const balance = this._balances[fromAddress][tokenAddress] assert(balance.gte(amount), "The user doesn't have enough balance") assert( - this._hasAllowance({ fromAddress, tokenAddress, spenderAddress: senderAddress, amount }), + this._hasAllowance({ fromAddress, tokenAddress, spenderAddress: userAddress, amount }), 'Not allowed to perform this transfer', ) - const allowance = this._allowances[fromAddress][tokenAddress][senderAddress] - this._allowances[fromAddress][tokenAddress][senderAddress] = allowance.sub(amount) + const allowance = this._allowances[fromAddress][tokenAddress][userAddress] + this._allowances[fromAddress][tokenAddress][userAddress] = allowance.sub(amount) log( - `[Erc20ApiMock:transferFrom] Updated allowance: ${allowance} => ${this._allowances[fromAddress][tokenAddress][senderAddress]}`, + `[Erc20ApiMock:transferFrom] Updated allowance: ${allowance} => ${this._allowances[fromAddress][tokenAddress][userAddress]}`, ) this._balances[fromAddress][tokenAddress] = balance.sub(amount) this._balances[toAddress][tokenAddress] = this._balances[toAddress][tokenAddress].add(amount) log( - `[Erc20ApiMock:transferFrom] Transferred ${amount} of the token ${tokenAddress} from ${fromAddress} to ${toAddress} by the spender ${senderAddress}`, + `[Erc20ApiMock:transferFrom] Transferred ${amount} of the token ${tokenAddress} from ${fromAddress} to ${toAddress} by the spender ${userAddress}`, ) return RECEIPT diff --git a/src/api/exchange/ExchangeApi.ts b/src/api/exchange/ExchangeApi.ts index 28bc8e629..6cbbbc2f5 100644 --- a/src/api/exchange/ExchangeApi.ts +++ b/src/api/exchange/ExchangeApi.ts @@ -5,21 +5,28 @@ import { log } from 'utils' import Web3 from 'web3' import { decodeAuctionElements } from './utils/decodeAuctionElements' -export interface ExchangeApi extends DepositApi { - getOrders(userAddress: string): Promise - getNumTokens(): Promise - getFeeDenominator(): Promise - getTokenAddressById(tokenId: number): Promise // tokenAddressToIdMap - getTokenIdByAddress(tokenAddress: string): Promise - addToken(tokenAddress: string, txOptionalParams?: TxOptionalParams): Promise - placeOrder(orderParams: PlaceOrderParams, txOptionalParams?: TxOptionalParams): Promise - cancelOrders( - { senderAddress, orderIds }: { senderAddress: string; orderIds: number[] }, - txOptionalParams?: TxOptionalParams, - ): Promise +export interface GetOrdersParams { + userAddress: string +} + +export interface GetTokenAddressByIdParams { + tokenId: number +} + +export interface GetTokenIdByAddressParams { + tokenAddress: string +} + +interface WithTxOptionalParams { + txOptionalParams?: TxOptionalParams +} + +export interface AddTokenParams extends WithTxOptionalParams { + userAddress: string + tokenAddress: string } -export interface PlaceOrderParams { +export interface PlaceOrderParams extends WithTxOptionalParams { userAddress: string buyTokenId: number sellTokenId: number @@ -28,6 +35,25 @@ export interface PlaceOrderParams { sellAmount: BN } +export interface CancelOrdersParams extends WithTxOptionalParams { + userAddress: string + orderIds: number[] +} + +export interface ExchangeApi extends DepositApi { + getNumTokens(): Promise + getFeeDenominator(): Promise + + getOrders(params: GetOrdersParams): Promise + + getTokenAddressById(params: GetTokenAddressByIdParams): Promise // tokenAddressToIdMap + getTokenIdByAddress(params: GetTokenIdByAddressParams): Promise + + addToken(params: AddTokenParams): Promise + placeOrder(params: PlaceOrderParams): Promise + cancelOrders(params: CancelOrdersParams): Promise +} + export interface AuctionElement extends Order { user: string sellTokenBalance: BN @@ -50,10 +76,11 @@ export interface Order { export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { public constructor(web3: Web3) { super(web3) + // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(window as any).exchange = this._contractPrototype } - public async getOrders(userAddress: string): Promise { + public async getOrders({ userAddress }: GetOrdersParams): Promise { const contract = await this._getContract() log(`[ExchangeApiImpl] Getting Orders for account ${userAddress}`) @@ -81,20 +108,20 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { return +feeDenominator } - public async getTokenAddressById(tokenId: number): Promise { + public async getTokenAddressById({ tokenId }: GetTokenAddressByIdParams): Promise { const contract = await this._getContract() return contract.methods.tokenIdToAddressMap(tokenId).call() } - public async getTokenIdByAddress(tokenAddress: string): Promise { + public async getTokenIdByAddress({ tokenAddress }: GetTokenIdByAddressParams): Promise { const contract = await this._getContract() const tokenId = await contract.methods.tokenAddressToIdMap(tokenAddress).call() return +tokenId } - public async addToken(tokenAddress: string, txOptionalParams?: TxOptionalParams): Promise { + public async addToken({ userAddress, tokenAddress, txOptionalParams }: AddTokenParams): Promise { const contract = await this._getContract() - const tx = contract.methods.addToken(tokenAddress).send() + const tx = contract.methods.addToken(tokenAddress).send({ from: userAddress }) if (txOptionalParams && txOptionalParams.onSentTransaction) { tx.once('transactionHash', txOptionalParams.onSentTransaction) @@ -105,8 +132,8 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { return tx } - public async placeOrder(orderParams: PlaceOrderParams, txOptionalParams?: TxOptionalParams): Promise { - const { userAddress, buyTokenId, sellTokenId, validUntil, buyAmount, sellAmount } = orderParams + public async placeOrder(params: PlaceOrderParams): Promise { + const { userAddress, buyTokenId, sellTokenId, validUntil, buyAmount, sellAmount, txOptionalParams } = params const contract = await this._getContract() @@ -129,12 +156,9 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { return tx } - public async cancelOrders( - { senderAddress, orderIds }: { senderAddress: string; orderIds: number[] }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async cancelOrders({ userAddress, orderIds, txOptionalParams }: CancelOrdersParams): Promise { const contract = await this._getContract() - const tx = contract.methods.cancelOrders(orderIds).send({ from: senderAddress }) + const tx = contract.methods.cancelOrders(orderIds).send({ from: userAddress }) if (txOptionalParams && txOptionalParams.onSentTransaction) { tx.once('transactionHash', txOptionalParams.onSentTransaction) diff --git a/src/api/exchange/ExchangeApiMock.ts b/src/api/exchange/ExchangeApiMock.ts index 8486e8918..88f02a735 100644 --- a/src/api/exchange/ExchangeApiMock.ts +++ b/src/api/exchange/ExchangeApiMock.ts @@ -3,11 +3,21 @@ import BN from 'bn.js' import assert from 'assert' import { DepositApiMock, BalancesByUserAndToken } from '../deposit/DepositApiMock' -import { Receipt, TxOptionalParams } from 'types' +import { Receipt } from 'types' import { FEE_DENOMINATOR, ONE } from 'const' import { waitAndSendReceipt } from 'utils/mock' import { RECEIPT } from '../../../test/data' -import { ExchangeApi, AuctionElement, PlaceOrderParams, Order } from './ExchangeApi' +import { + ExchangeApi, + AuctionElement, + PlaceOrderParams, + Order, + CancelOrdersParams, + AddTokenParams, + GetOrdersParams, + GetTokenAddressByIdParams, + GetTokenIdByAddressParams, +} from './ExchangeApi' import { Erc20Api } from 'api/erc20/Erc20Api' export interface OrdersByUser { @@ -45,7 +55,7 @@ export class ExchangeApiMock extends DepositApiMock implements ExchangeApi { this.orders = ordersByUser } - public async getOrders(userAddress: string): Promise { + public async getOrders({ userAddress }: GetOrdersParams): Promise { this._initOrders(userAddress) return this.orders[userAddress].map((order, index) => ({ ...order, @@ -67,17 +77,17 @@ export class ExchangeApiMock extends DepositApiMock implements ExchangeApi { return FEE_DENOMINATOR } - public async getTokenAddressById(tokenId: number): Promise { + public async getTokenAddressById({ tokenId }: GetTokenAddressByIdParams): Promise { assert(typeof this.registeredTokens[tokenId] === 'string', 'Must have ID to get Address') return this.registeredTokens[tokenId] } - public async getTokenIdByAddress(tokenAddress: string): Promise { + public async getTokenIdByAddress({ tokenAddress }: GetTokenIdByAddressParams): Promise { assert(typeof this.tokenAddressToId[tokenAddress] === 'number', 'Must have Address to get ID') return this.tokenAddressToId[tokenAddress] } - public async addToken(tokenAddress: string, txOptionalParams?: TxOptionalParams): Promise { + public async addToken({ tokenAddress, txOptionalParams }: AddTokenParams): Promise { await waitAndSendReceipt({ txOptionalParams }) assert(typeof this.tokenAddressToId[tokenAddress] !== 'number', 'Token already registered') @@ -88,42 +98,34 @@ export class ExchangeApiMock extends DepositApiMock implements ExchangeApi { return RECEIPT } - public async placeOrder(orderParams: PlaceOrderParams, txOptionalParams?: TxOptionalParams): Promise { + public async placeOrder(params: PlaceOrderParams): Promise { + const { buyTokenId, sellTokenId, validUntil, txOptionalParams } = params await waitAndSendReceipt({ txOptionalParams }) - this._initOrders(orderParams.userAddress) + this._initOrders(params.userAddress) - this.orders[orderParams.userAddress].push({ - buyTokenId: orderParams.buyTokenId, - sellTokenId: orderParams.sellTokenId, + this.orders[params.userAddress].push({ + buyTokenId, + sellTokenId, validFrom: await this.getCurrentBatchId(), - validUntil: orderParams.validUntil, - priceNumerator: orderParams.buyAmount, - priceDenominator: orderParams.sellAmount, - remainingAmount: orderParams.sellAmount, + validUntil, + priceNumerator: params.buyAmount, + priceDenominator: params.sellAmount, + remainingAmount: params.sellAmount, }) return RECEIPT } - public async cancelOrders( - { - senderAddress, - orderIds, - }: { - senderAddress: string - orderIds: number[] - }, - txOptionalParams?: TxOptionalParams, - ): Promise { + public async cancelOrders({ userAddress, orderIds, txOptionalParams }: CancelOrdersParams): Promise { await waitAndSendReceipt({ txOptionalParams }) - this._initOrders(senderAddress) + this._initOrders(userAddress) const batchId = await this.getCurrentBatchId() orderIds.forEach(orderId => { - if (this.orders[senderAddress][orderId]) { - this.orders[senderAddress][orderId].validUntil = batchId - 1 + if (this.orders[userAddress][orderId]) { + this.orders[userAddress][orderId].validUntil = batchId - 1 } }) diff --git a/src/components/DepositWidget/useRowActions.tsx b/src/components/DepositWidget/useRowActions.tsx index e02cd5646..257c19c79 100644 --- a/src/components/DepositWidget/useRowActions.tsx +++ b/src/components/DepositWidget/useRowActions.tsx @@ -45,10 +45,13 @@ export const useRowActions = (params: Params): Result => { const { symbol: tokenDisplayName } = safeFilledToken(token) - const receipt = await erc20Api.approve( - { userAddress, tokenAddress, spenderAddress: contractAddress, amount: ALLOWANCE_MAX_VALUE }, + const receipt = await erc20Api.approve({ + userAddress, + tokenAddress, + spenderAddress: contractAddress, + amount: ALLOWANCE_MAX_VALUE, txOptionalParams, - ) + }) log(`The transaction has been mined: ${receipt.transactionHash}`) toast.success(`The token ${tokenDisplayName} has been enabled for trading`) @@ -72,7 +75,7 @@ export const useRowActions = (params: Params): Result => { const { symbol, decimals } = safeFilledToken(token) log(`Processing deposit of ${amount} ${symbol} from ${userAddress}`) - const receipt = await depositApi.deposit({ userAddress, tokenAddress, amount }, txOptionalParams) + const receipt = await depositApi.deposit({ userAddress, tokenAddress, amount, txOptionalParams }) log(`The transaction has been mined: ${receipt.transactionHash}`) toast.success(`Successfully deposited ${formatAmount(amount, decimals)} ${symbol}`) @@ -98,7 +101,7 @@ export const useRowActions = (params: Params): Result => { log(`Processing withdraw request of ${amount} ${symbol} from ${userAddress}`) log(`Processing withdraw request of ${amount} ${symbol} from ${userAddress}`) - const receipt = await depositApi.requestWithdraw({ userAddress, tokenAddress, amount }, txOptionalParams) + const receipt = await depositApi.requestWithdraw({ userAddress, tokenAddress, amount, txOptionalParams }) log(`The transaction has been mined: ${receipt.transactionHash}`) toast.success(`Successfully requested withdraw of ${formatAmount(amount, decimals)} ${symbol}`) @@ -123,7 +126,7 @@ export const useRowActions = (params: Params): Result => { dispatch(setHighlightAndClaimingAction(tokenAddress)) - const receipt = await depositApi.withdraw({ userAddress, tokenAddress }, txOptionalParams) + const receipt = await depositApi.withdraw({ userAddress, tokenAddress, txOptionalParams }) log(`The transaction has been mined: ${receipt.transactionHash}`) toast.success(`Withdraw of ${formatAmount(pendingWithdraw.amount, decimals)} ${symbol} completed`) diff --git a/src/components/OrdersWidget/useDeleteOrders.tsx b/src/components/OrdersWidget/useDeleteOrders.tsx index a84b46e1e..ced82f449 100644 --- a/src/components/OrdersWidget/useDeleteOrders.tsx +++ b/src/components/OrdersWidget/useDeleteOrders.tsx @@ -36,7 +36,7 @@ export function useDeleteOrders(): Result { const orderIds = extractExchangeOrderIds(uiOrderIds) - const receipt = await exchangeApi.cancelOrders({ senderAddress: userAddress, orderIds }, txOptionalParams) + const receipt = await exchangeApi.cancelOrders({ userAddress, orderIds, txOptionalParams }) log(`The transaction has been mined: ${receipt.transactionHash}`) diff --git a/src/hooks/useEnableToken.ts b/src/hooks/useEnableToken.ts index 361805c86..181470bf7 100644 --- a/src/hooks/useEnableToken.ts +++ b/src/hooks/useEnableToken.ts @@ -23,6 +23,7 @@ export const useEnableTokens = (params: Params): Result => { const { enabled: enabledInitial, address: tokenAddress } = params.tokenBalances const [enabled, setEnabled] = useSafeState(enabledInitial) const [enabling, setEnabling] = useSafeState(false) + const { txOptionalParams } = params async function enableToken(): Promise { assert(!enabled && isConnected, 'The token was already enabled and/or user is not connected') @@ -34,15 +35,13 @@ export const useEnableTokens = (params: Params): Result => { const contractAddress = networkId ? depositApi.getContractAddress(networkId) : null assert(contractAddress, 'Contract address not found. Please check wallet.') - const receipt = await erc20Api.approve( - { - userAddress, - tokenAddress, - spenderAddress: contractAddress, - amount: ALLOWANCE_MAX_VALUE, - }, - params.txOptionalParams, - ) + const receipt = await erc20Api.approve({ + userAddress, + tokenAddress, + spenderAddress: contractAddress, + amount: ALLOWANCE_MAX_VALUE, + txOptionalParams, + }) // Update the state setEnabled(true) diff --git a/src/hooks/useOrders.ts b/src/hooks/useOrders.ts index e7b86d0fe..ddd2fd9f1 100644 --- a/src/hooks/useOrders.ts +++ b/src/hooks/useOrders.ts @@ -34,7 +34,7 @@ export function useOrders(): AuctionElement[] { useEffect(() => { userAddress && exchangeApi - .getOrders(userAddress) + .getOrders({ userAddress }) .then(filterDeletedOrders) .then(setOrders) }, [setOrders, userAddress]) diff --git a/src/hooks/usePlaceOrder.ts b/src/hooks/usePlaceOrder.ts index 4eeb847c8..8bd94d124 100644 --- a/src/hooks/usePlaceOrder.ts +++ b/src/hooks/usePlaceOrder.ts @@ -43,8 +43,8 @@ export const usePlaceOrder = (): Result => { try { const [sellTokenId, buyTokenId, batchId] = await Promise.all([ - exchangeApi.getTokenIdByAddress(sellToken.address), - exchangeApi.getTokenIdByAddress(buyToken.address), + exchangeApi.getTokenIdByAddress({ tokenAddress: sellToken.address }), + exchangeApi.getTokenIdByAddress({ tokenAddress: buyToken.address }), exchangeApi.getCurrentBatchId(), ]) @@ -60,8 +60,9 @@ export const usePlaceOrder = (): Result => { validUntil, buyAmount, sellAmount, + txOptionalParams, } - const receipt = await exchangeApi.placeOrder(params, txOptionalParams) + const receipt = await exchangeApi.placeOrder(params) log(`The transaction has been mined: ${receipt.transactionHash}`) // TODO: get order id in a separate call diff --git a/src/hooks/useWithdrawTokens.ts b/src/hooks/useWithdrawTokens.ts index bd505537d..1ca80a601 100644 --- a/src/hooks/useWithdrawTokens.ts +++ b/src/hooks/useWithdrawTokens.ts @@ -18,6 +18,7 @@ export const useWithdrawTokens = (params: Params): Result => { const { userAddress, isConnected } = useWalletConnection() const { tokenBalances: { enabled, address: tokenAddress, claimable }, + txOptionalParams, } = params const [claiming, setWithdrawing] = useSafeState(false) @@ -30,7 +31,7 @@ export const useWithdrawTokens = (params: Params): Result => { setWithdrawing(true) - return depositApi.withdraw({ userAddress, tokenAddress }, params.txOptionalParams) + return depositApi.withdraw({ userAddress, tokenAddress, txOptionalParams }) } finally { setWithdrawing(false) } diff --git a/src/services/factories/addTokenToExchange.ts b/src/services/factories/addTokenToExchange.ts index 8b81bbbf1..27c5ce408 100644 --- a/src/services/factories/addTokenToExchange.ts +++ b/src/services/factories/addTokenToExchange.ts @@ -12,15 +12,22 @@ interface Params { web3: Web3 } +interface AddTokenToExchangeParams { + userAddress: string + tokenAddress: string +} + /** * Factory of addTokenToExchange * Takes as input API instances * Returns async function to add tokenAddress to exchange */ -export function addTokenToExchangeFactory(factoryParams: Params): (tokenAddress: string) => Promise { +export function addTokenToExchangeFactory( + factoryParams: Params, +): (params: AddTokenToExchangeParams) => Promise { const { exchangeApi } = factoryParams - return async (tokenAddress: string): Promise => { + return async ({ userAddress, tokenAddress }: AddTokenToExchangeParams): Promise => { const erc20Info = getErc20Info({ ...factoryParams, tokenAddress }) if (!erc20Info) { @@ -29,7 +36,7 @@ export function addTokenToExchangeFactory(factoryParams: Params): (tokenAddress: } try { - await exchangeApi.addToken(tokenAddress) + await exchangeApi.addToken({ userAddress, tokenAddress }) } catch (e) { log('Failed to add token (%s) to exchange', tokenAddress) return false @@ -38,7 +45,7 @@ export function addTokenToExchangeFactory(factoryParams: Params): (tokenAddress: // TODO: I guess we might want to return the token and leave the proxy/cache layer to deal with it. // Revisit once we add it to the interface try { - const id = exchangeApi.getTokenIdByAddress(tokenAddress) + const id = exchangeApi.getTokenIdByAddress({ tokenAddress }) const token = { ...erc20Info, diff --git a/src/services/factories/getTokenFromExchange.ts b/src/services/factories/getTokenFromExchange.ts index 0d4e0ca07..8390f36e9 100644 --- a/src/services/factories/getTokenFromExchange.ts +++ b/src/services/factories/getTokenFromExchange.ts @@ -69,7 +69,7 @@ function getTokenFromExchangeByAddressFactory( let tokenId: number try { - tokenId = await exchangeApi.getTokenIdByAddress(tokenAddress) + tokenId = await exchangeApi.getTokenIdByAddress({ tokenAddress }) } catch (e) { log('Token with address %s not registered on contract', tokenAddress, e) return null @@ -113,7 +113,7 @@ function getTokenFromExchangeByIdFactory( // Not there, get the address from the contract let tokenAddress: string try { - tokenAddress = await exchangeApi.getTokenAddressById(tokenId) + tokenAddress = await exchangeApi.getTokenAddressById({ tokenId }) } catch (e) { log('Token with id %d not registered on contract', tokenId, e) return null diff --git a/test/api/ExchangeApi/Erc20ApiMock.test.ts b/test/api/ExchangeApi/Erc20ApiMock.test.ts index c416c10cf..588db16b4 100644 --- a/test/api/ExchangeApi/Erc20ApiMock.test.ts +++ b/test/api/ExchangeApi/Erc20ApiMock.test.ts @@ -112,7 +112,7 @@ describe('Basic view functions', () => { describe('Write functions', () => { const mockFunction = jest.fn() - const optionalParams: TxOptionalParams = { + const txOptionalParams: TxOptionalParams = { onSentTransaction: mockFunction, } function resetInstance(): void { @@ -125,10 +125,13 @@ describe('Write functions', () => { describe('approve', () => { const amount = new BN('5289375492345723') it('allowance is set', async () => { - const result = await instance.approve( - { userAddress: USER_1, tokenAddress: TOKEN_1, spenderAddress: USER_2, amount }, - optionalParams, - ) + const result = await instance.approve({ + userAddress: USER_1, + tokenAddress: TOKEN_1, + spenderAddress: USER_2, + amount, + txOptionalParams, + }) expect(await instance.allowance({ tokenAddress: TOKEN_1, userAddress: USER_1, spenderAddress: USER_2 })).toBe( amount, @@ -137,10 +140,13 @@ describe('Write functions', () => { }) it('calls optional callback', async () => { - await instance.approve( - { userAddress: USER_1, tokenAddress: TOKEN_1, spenderAddress: USER_2, amount }, - optionalParams, - ) + await instance.approve({ + userAddress: USER_1, + tokenAddress: TOKEN_1, + spenderAddress: USER_2, + amount, + txOptionalParams, + }) expect(mockFunction.mock.calls.length).toBe(1) }) }) @@ -152,7 +158,7 @@ describe('Write functions', () => { const userBalance = await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2 }) const result = await instance.transfer({ - fromAddress: CONTRACT, + userAddress: CONTRACT, tokenAddress: TOKEN_1, toAddress: USER_2, amount, @@ -168,7 +174,7 @@ describe('Write functions', () => { it('does not transfer when balance is insufficient', async () => { // TODO: after hours, couldn't figure out a way to check for the AssertionError using expect().toThrow() await instance - .transfer({ fromAddress: USER_2, tokenAddress: TOKEN_1, toAddress: CONTRACT, amount }) + .transfer({ userAddress: USER_2, tokenAddress: TOKEN_1, toAddress: CONTRACT, amount }) .then(() => fail('Should not succeed')) .catch(e => { expect(e.message).toMatch(/^The user doesn't have enough balance$/) @@ -176,10 +182,13 @@ describe('Write functions', () => { }) it('calls optional callback', async () => { - await instance.transfer( - { fromAddress: CONTRACT, tokenAddress: TOKEN_1, toAddress: USER_2, amount }, - optionalParams, - ) + await instance.transfer({ + userAddress: CONTRACT, + tokenAddress: TOKEN_1, + toAddress: USER_2, + amount, + txOptionalParams, + }) expect(mockFunction.mock.calls.length).toBe(1) }) }) @@ -197,7 +206,7 @@ describe('Write functions', () => { await instance.approve({ userAddress: USER_1, tokenAddress: TOKEN_1, spenderAddress: USER_3, amount }) const result = await instance.transferFrom({ - senderAddress: USER_3, + userAddress: USER_3, tokenAddress: TOKEN_1, fromAddress: USER_1, toAddress: USER_2, @@ -216,7 +225,7 @@ describe('Write functions', () => { await instance.approve({ userAddress: USER_2, tokenAddress: TOKEN_3, spenderAddress: USER_3, amount }) await instance - .transferFrom({ senderAddress: USER_3, tokenAddress: TOKEN_3, fromAddress: USER_2, toAddress: USER_1, amount }) + .transferFrom({ userAddress: USER_3, tokenAddress: TOKEN_3, fromAddress: USER_2, toAddress: USER_1, amount }) .then(() => { fail('Should not succeed') }) @@ -227,7 +236,7 @@ describe('Write functions', () => { it('does not transfer when allowance is insufficient', async () => { await instance - .transferFrom({ senderAddress: USER_3, tokenAddress: TOKEN_3, fromAddress: USER_1, toAddress: USER_2, amount }) + .transferFrom({ userAddress: USER_3, tokenAddress: TOKEN_3, fromAddress: USER_1, toAddress: USER_2, amount }) .then(() => { fail('Should not succeed') }) @@ -238,10 +247,14 @@ describe('Write functions', () => { it('calls optional callback', async () => { await instance.approve({ userAddress: USER_1, tokenAddress: TOKEN_1, spenderAddress: USER_3, amount }) - await instance.transferFrom( - { senderAddress: USER_3, tokenAddress: TOKEN_1, fromAddress: USER_1, toAddress: USER_2, amount }, - optionalParams, - ) + await instance.transferFrom({ + userAddress: USER_3, + tokenAddress: TOKEN_1, + fromAddress: USER_1, + toAddress: USER_2, + amount, + txOptionalParams, + }) expect(mockFunction.mock.calls.length).toBe(1) }) }) diff --git a/test/api/ExchangeApi/ExchangeApiMock.test.ts b/test/api/ExchangeApi/ExchangeApiMock.test.ts index 2ec4665c0..c62f9b8f5 100644 --- a/test/api/ExchangeApi/ExchangeApiMock.test.ts +++ b/test/api/ExchangeApi/ExchangeApiMock.test.ts @@ -54,11 +54,11 @@ describe('Basic view functions', () => { describe('get orders', () => { it('returns empty when no orders placed by user', async () => { - expect(await instance.getOrders(USER_2)).toHaveLength(0) + expect(await instance.getOrders({ userAddress: USER_2 })).toHaveLength(0) }) it('returns all placed orders', async () => { - expect(await instance.getOrders(USER_1)).toHaveLength(1) + expect(await instance.getOrders({ userAddress: USER_1 })).toHaveLength(1) }) }) }) @@ -66,12 +66,12 @@ describe('Basic view functions', () => { describe('Token view methods', () => { describe('get token id by address', () => { it('returns correct id when found', async () => { - expect(await instance.getTokenIdByAddress(tokens[1])).toBe(1) + expect(await instance.getTokenIdByAddress({ tokenAddress: tokens[1] })).toBe(1) }) it('throws when id not found', async () => { try { - await instance.getTokenIdByAddress(TOKEN_4) + await instance.getTokenIdByAddress({ tokenAddress: TOKEN_4 }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/^Must have Address to get ID$/) @@ -81,12 +81,12 @@ describe('Token view methods', () => { describe('get token address by id', () => { it('returns correct address when found', async () => { - expect(await instance.getTokenAddressById(0)).toBe(tokens[0]) + expect(await instance.getTokenAddressById({ tokenId: 0 })).toBe(tokens[0]) }) it('throws when address not found', async () => { try { - await instance.getTokenAddressById(10) + await instance.getTokenAddressById({ tokenId: 10 }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/^Must have ID to get Address$/) @@ -99,16 +99,16 @@ describe('addToken', () => { it('adds token not registered', async () => { const tokenCount = await instance.getNumTokens() - await instance.addToken(TOKEN_3) + await instance.addToken({ userAddress: USER_1, tokenAddress: TOKEN_3 }) expect(await instance.getNumTokens()).toBe(tokenCount + 1) - expect(await instance.getTokenIdByAddress(TOKEN_3)).toBe(tokenCount) - expect(await instance.getTokenAddressById(tokenCount)).toBe(TOKEN_3) + expect(await instance.getTokenIdByAddress({ tokenAddress: TOKEN_3 })).toBe(tokenCount) + expect(await instance.getTokenAddressById({ tokenId: tokenCount })).toBe(TOKEN_3) }) it('throws when token already registered', async () => { try { - await instance.addToken(tokens[0]) + await instance.addToken({ userAddress: USER_1, tokenAddress: tokens[0] }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/^Token already registered$/) @@ -116,7 +116,7 @@ describe('addToken', () => { }) it('throws when MAX_TOKENS reached', async () => { try { - await instance.addToken(TOKEN_4) + await instance.addToken({ userAddress: USER_1, tokenAddress: TOKEN_4 }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/^Max tokens reached$/) @@ -149,45 +149,45 @@ describe('placeOrder', () => { params.userAddress = USER_1 const response = await instance.placeOrder(params) expect(response).toBe(RECEIPT) - const actual = (await instance.getOrders(USER_1)).pop() + const actual = (await instance.getOrders({ userAddress: USER_1 })).pop() expect(actual).toEqual({ ...expected, user: USER_1, id: '1' }) }) test('place first order', async () => { - expect((await instance.getOrders(USER_3)).length).toBe(0) + expect((await instance.getOrders({ userAddress: USER_3 })).length).toBe(0) params.userAddress = USER_2 const response = await instance.placeOrder(params) expect(response).toBe(RECEIPT) - const actual = (await instance.getOrders(USER_2)).pop() + const actual = (await instance.getOrders({ userAddress: USER_2 })).pop() expect(actual).toEqual({ ...expected, user: USER_2, id: '0' }) }) }) describe('cancelOrder', () => { test('cancel existing order', async () => { - const orderId = (await instance.getOrders(USER_1)).length - 1 + const orderId = (await instance.getOrders({ userAddress: USER_1 })).length - 1 - await instance.cancelOrders({ senderAddress: USER_1, orderIds: [orderId] }) + await instance.cancelOrders({ userAddress: USER_1, orderIds: [orderId] }) - const actual = (await instance.getOrders(USER_1))[orderId] + const actual = (await instance.getOrders({ userAddress: USER_1 }))[orderId] expect(actual.validUntil).toBe(BATCH_ID - 1) }) test('cancel non existing order does nothing', async () => { - const expected = await instance.getOrders(USER_1) + const expected = await instance.getOrders({ userAddress: USER_1 }) - await instance.cancelOrders({ senderAddress: USER_1, orderIds: [expected.length + 1] }) + await instance.cancelOrders({ userAddress: USER_1, orderIds: [expected.length + 1] }) - const actual = await instance.getOrders(USER_1) + const actual = await instance.getOrders({ userAddress: USER_1 }) expect(actual).toEqual(expected) }) test('cancel non existing order, user with no orders does nothing', async () => { - const expected = await instance.getOrders(USER_2) + const expected = await instance.getOrders({ userAddress: USER_2 }) - await instance.cancelOrders({ senderAddress: USER_2, orderIds: [expected.length + 1] }) + await instance.cancelOrders({ userAddress: USER_2, orderIds: [expected.length + 1] }) - const actual = await instance.getOrders(USER_2) + const actual = await instance.getOrders({ userAddress: USER_2 }) expect(actual).toEqual(expected) }) }) From f754efeeccb20f87db920fada4babc51cd985a0d Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Wed, 8 Jan 2020 09:12:42 -0800 Subject: [PATCH 08/53] 329/network agnostic apis (#395) * DepositApi and ExchangeApi are now network agnostic * Erc20Api is now network agnostic --- src/api/deposit/DepositApi.ts | 99 +++--- src/api/deposit/DepositApiMock.ts | 21 +- src/api/erc20/Erc20Api.ts | 52 +-- src/api/exchange/ExchangeApi.ts | 68 ++-- .../DepositWidget/useRowActions.tsx | 17 +- .../OrdersWidget/useDeleteOrders.tsx | 7 +- src/hooks/useEnableToken.ts | 2 + src/hooks/useOrders.ts | 7 +- src/hooks/usePlaceOrder.ts | 13 +- src/hooks/useTokenBalances.ts | 15 +- src/hooks/useWithdrawTokens.ts | 5 +- src/services/factories/addTokenToExchange.ts | 9 +- .../factories/getTokenFromExchange.ts | 9 +- src/services/helpers/getErc20Info.ts | 16 +- test/api/ExchangeApi/DepositApiMock.test.ts | 333 +++++++++++++----- test/api/ExchangeApi/Erc20ApiMock.test.ts | 174 ++++++--- test/api/ExchangeApi/ExchangeApiMock.test.ts | 56 +-- 17 files changed, 618 insertions(+), 285 deletions(-) diff --git a/src/api/deposit/DepositApi.ts b/src/api/deposit/DepositApi.ts index 3266e8c5f..9b3d1eddd 100644 --- a/src/api/deposit/DepositApi.ts +++ b/src/api/deposit/DepositApi.ts @@ -7,11 +7,11 @@ import { getAddressForNetwork } from './batchExchangeAddresses' import { Receipt, TxOptionalParams } from 'types' import Web3 from 'web3' -import { getProviderState, Provider, ProviderState } from '@gnosis.pm/dapp-ui' interface ReadOnlyParams { userAddress: string tokenAddress: string + networkId: number } export type GetBalanceParams = ReadOnlyParams @@ -32,9 +32,9 @@ export type WithdrawParams = Omit export interface DepositApi { getContractAddress(networkId: number): string | null - getBatchTime(): Promise - getCurrentBatchId(): Promise - getSecondsRemainingInBatch(): Promise + getBatchTime(networkId: number): Promise + getCurrentBatchId(networkId: number): Promise + getSecondsRemainingInBatch(networkId: number): Promise getBalance(params: GetBalanceParams): Promise getPendingDeposit(params: GetPendingDepositParams): Promise @@ -50,21 +50,10 @@ export interface PendingFlux { batchId: number } -const getNetworkIdFromWeb3 = (web3: Web3): null | number => { - if (!web3.currentProvider) return null - - // web3.currentProvider may be our provider wrapped in a Proxy - // depending on web3 version - // or internally created provider from url - const providerState: ProviderState | null = getProviderState((web3.currentProvider as unknown) as Provider | null) - - return providerState && providerState.chainId -} - export class DepositApiImpl implements DepositApi { protected _contractPrototype: BatchExchangeContract protected _web3: Web3 - protected static _contractsCache: { [K: string]: BatchExchangeContract } = {} + protected static _contractsCache: { [network: number]: { [address: string]: BatchExchangeContract } } = {} public constructor(web3: Web3) { this._contractPrototype = new web3.eth.Contract(batchExchangeAbi) as BatchExchangeContract @@ -79,55 +68,69 @@ export class DepositApiImpl implements DepositApi { return getAddressForNetwork(networkId) } - public async getBatchTime(): Promise { - const contract = await this._getContract() + public async getBatchTime(networkId: number): Promise { + const contract = await this._getContract(networkId) const BATCH_TIME = await contract.methods.BATCH_TIME().call() return +BATCH_TIME } - public async getCurrentBatchId(): Promise { - const contract = await this._getContract() + public async getCurrentBatchId(networkId: number): Promise { + const contract = await this._getContract(networkId) const batchId = await contract.methods.getCurrentBatchId().call() return +batchId } - public async getSecondsRemainingInBatch(): Promise { - const contract = await this._getContract() + public async getSecondsRemainingInBatch(networkId: number): Promise { + const contract = await this._getContract(networkId) const secondsRemainingInBatch = await contract.methods.getSecondsRemainingInBatch().call() return +secondsRemainingInBatch } - public async getBalance({ userAddress, tokenAddress }: GetBalanceParams): Promise { + public async getBalance({ userAddress, tokenAddress, networkId }: GetBalanceParams): Promise { if (!userAddress || !tokenAddress) return ZERO - const contract = await this._getContract() + const contract = await this._getContract(networkId) const balance = await contract.methods.getBalance(userAddress, tokenAddress).call() return toBN(balance) } - public async getPendingDeposit({ userAddress, tokenAddress }: GetPendingDepositParams): Promise { + public async getPendingDeposit({ + userAddress, + tokenAddress, + networkId, + }: GetPendingDepositParams): Promise { if (!userAddress || !tokenAddress) return { amount: ZERO, batchId: 0 } - const contract = await this._getContract() + const contract = await this._getContract(networkId) const { 0: amount, 1: batchId } = await contract.methods.getPendingDeposit(userAddress, tokenAddress).call() return { amount: toBN(amount), batchId: Number(batchId) } } - public async getPendingWithdraw({ userAddress, tokenAddress }: GetPendingWithdrawParams): Promise { + public async getPendingWithdraw({ + userAddress, + tokenAddress, + networkId, + }: GetPendingWithdrawParams): Promise { if (!userAddress || !tokenAddress) return { amount: ZERO, batchId: 0 } - const contract = await this._getContract() + const contract = await this._getContract(networkId) const { 0: amount, 1: batchId } = await contract.methods.getPendingWithdraw(userAddress, tokenAddress).call() return { amount: toBN(amount), batchId: Number(batchId) } } - public async deposit({ userAddress, tokenAddress, amount, txOptionalParams }: DepositParams): Promise { - const contract = await this._getContract() + public async deposit({ + userAddress, + tokenAddress, + networkId, + amount, + txOptionalParams, + }: DepositParams): Promise { + const contract = await this._getContract(networkId) // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = contract.methods.deposit(tokenAddress, amount.toString()).send({ from: userAddress }) @@ -142,10 +145,11 @@ export class DepositApiImpl implements DepositApi { public async requestWithdraw({ userAddress, tokenAddress, + networkId, amount, txOptionalParams, }: RequestWithdrawParams): Promise { - const contract = await this._getContract() + const contract = await this._getContract(networkId) // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = contract.methods.requestWithdraw(tokenAddress, amount.toString()).send({ from: userAddress }) @@ -157,8 +161,8 @@ export class DepositApiImpl implements DepositApi { return tx } - public async withdraw({ userAddress, tokenAddress, txOptionalParams }: WithdrawParams): Promise { - const contract = await this._getContract() + public async withdraw({ userAddress, tokenAddress, networkId, txOptionalParams }: WithdrawParams): Promise { + const contract = await this._getContract(networkId) const tx = contract.methods.withdraw(userAddress, tokenAddress).send({ from: userAddress }) if (txOptionalParams?.onSentTransaction) { @@ -171,15 +175,7 @@ export class DepositApiImpl implements DepositApi { /******************************** private methods ********************************/ - protected async _getNetworkId(): Promise { - const networkId = getNetworkIdFromWeb3(this._web3) - - return networkId === null ? this._web3.eth.net.getId() : networkId - } - - protected async _getContract(): Promise { - const networkId = await this._getNetworkId() - + protected async _getContract(networkId: number): Promise { return this._getContractForNetwork(networkId) } @@ -188,18 +184,25 @@ export class DepositApiImpl implements DepositApi { assert(address, `EpochTokenLocker was not deployed to network ${networkId}`) - // as string as assert is not detected by TS - return this._getContractAtAddress(address) + return this._getContractAtAddress(networkId, address) } - protected _getContractAtAddress(address: string): BatchExchangeContract { - const contract = DepositApiImpl._contractsCache[address] + protected _getContractAtAddress(networkId: number, address: string): BatchExchangeContract { + let contract: BatchExchangeContract | undefined = undefined + + if (DepositApiImpl._contractsCache[networkId]) { + contract = DepositApiImpl._contractsCache[networkId][address] + } else { + DepositApiImpl._contractsCache[networkId] = {} + } - if (contract) return contract + if (contract) { + return contract + } const newContract = this._contractPrototype.clone() newContract.options.address = address - return (DepositApiImpl._contractsCache[address] = newContract) + return (DepositApiImpl._contractsCache[networkId][address] = newContract) } } diff --git a/src/api/deposit/DepositApiMock.ts b/src/api/deposit/DepositApiMock.ts index 6d603dfc1..522f7d7b1 100644 --- a/src/api/deposit/DepositApiMock.ts +++ b/src/api/deposit/DepositApiMock.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import BN from 'bn.js' import assert from 'assert' @@ -38,19 +39,19 @@ export class DepositApiMock implements DepositApi { this._erc20Api = erc20Api } - public getContractAddress(): string { + public getContractAddress(_networkId = 0): string { return CONTRACT } - public async getBatchTime(): Promise { + public async getBatchTime(_networkId = 0): Promise { return BATCH_TIME } - public async getCurrentBatchId(): Promise { + public async getCurrentBatchId(_networkId = 0): Promise { return Math.floor(getEpoch() / BATCH_TIME) } - public async getSecondsRemainingInBatch(): Promise { + public async getSecondsRemainingInBatch(_networkId = 0): Promise { return BATCH_TIME - (getEpoch() % BATCH_TIME) } @@ -83,7 +84,13 @@ export class DepositApiMock implements DepositApi { return balanceState ? balanceState.pendingWithdraws : createFlux() } - public async deposit({ userAddress, tokenAddress, amount, txOptionalParams }: DepositParams): Promise { + public async deposit({ + userAddress, + tokenAddress, + amount, + networkId, + txOptionalParams, + }: DepositParams): Promise { await waitAndSendReceipt({ txOptionalParams }) // Create the balance state if it's the first deposit @@ -104,6 +111,7 @@ export class DepositApiMock implements DepositApi { fromAddress: userAddress, toAddress: this.getContractAddress(), amount, + networkId, }) log(`[DepositApiMock] Deposited ${amount.toString()} for token ${tokenAddress}. User ${userAddress}`) @@ -130,7 +138,7 @@ export class DepositApiMock implements DepositApi { return RECEIPT } - public async withdraw({ userAddress, tokenAddress, txOptionalParams }: WithdrawParams): Promise { + public async withdraw({ userAddress, tokenAddress, networkId, txOptionalParams }: WithdrawParams): Promise { await waitAndSendReceipt({ txOptionalParams }) const currentBatchId = await this.getCurrentBatchId() @@ -154,6 +162,7 @@ export class DepositApiMock implements DepositApi { tokenAddress, toAddress: userAddress, amount, + networkId, }) log(`[DepositApiMock] Withdraw ${amount.toString()} for token ${tokenAddress}. User ${userAddress}`) diff --git a/src/api/erc20/Erc20Api.ts b/src/api/erc20/Erc20Api.ts index 4fa09961d..e809a4e94 100644 --- a/src/api/erc20/Erc20Api.ts +++ b/src/api/erc20/Erc20Api.ts @@ -11,6 +11,7 @@ import Web3 from 'web3' interface BaseParams { tokenAddress: string + networkId: number } export type NameParams = BaseParams @@ -71,7 +72,7 @@ export interface Erc20Api { export class Erc20ApiImpl implements Erc20Api { private _contractPrototype: Erc20Contract - private static _contractsCache: { [K: string]: Erc20Contract } = {} + private static _contractsCache: { [network: number]: { [address: string]: Erc20Contract } } = {} public constructor(web3: Web3) { this._contractPrototype = new web3.eth.Contract(erc20Abi as AbiItem[]) as Erc20Contract @@ -81,48 +82,48 @@ export class Erc20ApiImpl implements Erc20Api { ;(window as any).erc20 = this._contractPrototype } - public async balanceOf({ tokenAddress, userAddress }: BalanceOfParams): Promise { + public async balanceOf({ networkId, tokenAddress, userAddress }: BalanceOfParams): Promise { if (!userAddress || !tokenAddress) return ZERO - const erc20 = this._getERC20AtAddress(tokenAddress) + const erc20 = this._getERC20AtAddress(networkId, tokenAddress) const result = await erc20.methods.balanceOf(userAddress).call() return toBN(result) } - public async name({ tokenAddress }: NameParams): Promise { - const erc20 = this._getERC20AtAddress(tokenAddress) + public async name({ tokenAddress, networkId }: NameParams): Promise { + const erc20 = this._getERC20AtAddress(networkId, tokenAddress) return await erc20.methods.name().call() } - public async symbol({ tokenAddress }: SymbolParams): Promise { - const erc20 = this._getERC20AtAddress(tokenAddress) + public async symbol({ tokenAddress, networkId }: SymbolParams): Promise { + const erc20 = this._getERC20AtAddress(networkId, tokenAddress) return await erc20.methods.symbol().call() } - public async decimals({ tokenAddress }: DecimalsParams): Promise { - const erc20 = this._getERC20AtAddress(tokenAddress) + public async decimals({ tokenAddress, networkId }: DecimalsParams): Promise { + const erc20 = this._getERC20AtAddress(networkId, tokenAddress) const decimals = await erc20.methods.decimals().call() return Number(decimals) } - public async totalSupply({ tokenAddress }: TotalSupplyParams): Promise { - const erc20 = this._getERC20AtAddress(tokenAddress) + public async totalSupply({ tokenAddress, networkId }: TotalSupplyParams): Promise { + const erc20 = this._getERC20AtAddress(networkId, tokenAddress) const totalSupply = await erc20.methods.totalSupply().call() return toBN(totalSupply) } - public async allowance({ tokenAddress, userAddress, spenderAddress }: AllowanceParams): Promise { + public async allowance({ networkId, tokenAddress, userAddress, spenderAddress }: AllowanceParams): Promise { if (!userAddress || !tokenAddress) return ZERO - const erc20 = this._getERC20AtAddress(tokenAddress) + const erc20 = this._getERC20AtAddress(networkId, tokenAddress) const result = await erc20.methods.allowance(userAddress, spenderAddress).call() @@ -134,9 +135,10 @@ export class Erc20ApiImpl implements Erc20Api { tokenAddress, spenderAddress, amount, + networkId, txOptionalParams, }: ApproveParams): Promise { - const erc20 = this._getERC20AtAddress(tokenAddress) + const erc20 = this._getERC20AtAddress(networkId, tokenAddress) // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = erc20.methods.approve(spenderAddress, amount.toString()).send({ @@ -155,9 +157,10 @@ export class Erc20ApiImpl implements Erc20Api { tokenAddress, toAddress, amount, + networkId, txOptionalParams, }: TransferParams): Promise { - const erc20 = this._getERC20AtAddress(tokenAddress) + const erc20 = this._getERC20AtAddress(networkId, tokenAddress) // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = erc20.methods.transfer(toAddress, amount.toString()).send({ @@ -177,9 +180,10 @@ export class Erc20ApiImpl implements Erc20Api { fromAddress, toAddress, amount, + networkId, txOptionalParams, }: TransferFromParams): Promise { - const erc20 = this._getERC20AtAddress(tokenAddress) + const erc20 = this._getERC20AtAddress(networkId, tokenAddress) const tx = erc20.methods.transferFrom(userAddress, toAddress, amount.toString()).send({ from: fromAddress, @@ -194,15 +198,23 @@ export class Erc20ApiImpl implements Erc20Api { /******************************** private methods ********************************/ - private _getERC20AtAddress(address: string): Erc20Contract { - const erc20 = Erc20ApiImpl._contractsCache[address] + private _getERC20AtAddress(networkId: number, address: string): Erc20Contract { + let erc20: Erc20Contract | undefined = undefined - if (erc20) return erc20 + if (Erc20ApiImpl._contractsCache[networkId]) { + erc20 = Erc20ApiImpl._contractsCache[networkId][address] + } else { + Erc20ApiImpl._contractsCache[networkId] = {} + } + + if (erc20) { + return erc20 + } const newERC20 = this._contractPrototype.clone() newERC20.options.address = address - return (Erc20ApiImpl._contractsCache[address] = newERC20) + return (Erc20ApiImpl._contractsCache[networkId][address] = newERC20) } } diff --git a/src/api/exchange/ExchangeApi.ts b/src/api/exchange/ExchangeApi.ts index 6cbbbc2f5..140acd506 100644 --- a/src/api/exchange/ExchangeApi.ts +++ b/src/api/exchange/ExchangeApi.ts @@ -5,15 +5,19 @@ import { log } from 'utils' import Web3 from 'web3' import { decodeAuctionElements } from './utils/decodeAuctionElements' -export interface GetOrdersParams { +interface BaseParams { + networkId: number +} + +export interface GetOrdersParams extends BaseParams { userAddress: string } -export interface GetTokenAddressByIdParams { +export interface GetTokenAddressByIdParams extends BaseParams { tokenId: number } -export interface GetTokenIdByAddressParams { +export interface GetTokenIdByAddressParams extends BaseParams { tokenAddress: string } @@ -21,12 +25,12 @@ interface WithTxOptionalParams { txOptionalParams?: TxOptionalParams } -export interface AddTokenParams extends WithTxOptionalParams { +export interface AddTokenParams extends BaseParams, WithTxOptionalParams { userAddress: string tokenAddress: string } -export interface PlaceOrderParams extends WithTxOptionalParams { +export interface PlaceOrderParams extends BaseParams, WithTxOptionalParams { userAddress: string buyTokenId: number sellTokenId: number @@ -35,14 +39,14 @@ export interface PlaceOrderParams extends WithTxOptionalParams { sellAmount: BN } -export interface CancelOrdersParams extends WithTxOptionalParams { +export interface CancelOrdersParams extends BaseParams, WithTxOptionalParams { userAddress: string orderIds: number[] } export interface ExchangeApi extends DepositApi { - getNumTokens(): Promise - getFeeDenominator(): Promise + getNumTokens(networkId: number): Promise + getFeeDenominator(networkId: number): Promise getOrders(params: GetOrdersParams): Promise @@ -80,8 +84,8 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { ;(window as any).exchange = this._contractPrototype } - public async getOrders({ userAddress }: GetOrdersParams): Promise { - const contract = await this._getContract() + public async getOrders({ userAddress, networkId }: GetOrdersParams): Promise { + const contract = await this._getContract(networkId) log(`[ExchangeApiImpl] Getting Orders for account ${userAddress}`) const encodedOrders = await contract.methods.getEncodedUserOrders(userAddress).call() @@ -92,8 +96,8 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { return decodeAuctionElements(encodedOrders) } - public async getNumTokens(): Promise { - const contract = await this._getContract() + public async getNumTokens(networkId: number): Promise { + const contract = await this._getContract(networkId) const numTokens = await contract.methods.numTokens().call() return +numTokens } @@ -102,25 +106,25 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { * Fee is 1/fee_denominator. * i.e. 1/1000 = 0.1% */ - public async getFeeDenominator(): Promise { - const contract = await this._getContract() + public async getFeeDenominator(networkId: number): Promise { + const contract = await this._getContract(networkId) const feeDenominator = await contract.methods.FEE_DENOMINATOR().call() return +feeDenominator } - public async getTokenAddressById({ tokenId }: GetTokenAddressByIdParams): Promise { - const contract = await this._getContract() + public async getTokenAddressById({ tokenId, networkId }: GetTokenAddressByIdParams): Promise { + const contract = await this._getContract(networkId) return contract.methods.tokenIdToAddressMap(tokenId).call() } - public async getTokenIdByAddress({ tokenAddress }: GetTokenIdByAddressParams): Promise { - const contract = await this._getContract() + public async getTokenIdByAddress({ tokenAddress, networkId }: GetTokenIdByAddressParams): Promise { + const contract = await this._getContract(networkId) const tokenId = await contract.methods.tokenAddressToIdMap(tokenAddress).call() return +tokenId } - public async addToken({ userAddress, tokenAddress, txOptionalParams }: AddTokenParams): Promise { - const contract = await this._getContract() + public async addToken({ userAddress, tokenAddress, networkId, txOptionalParams }: AddTokenParams): Promise { + const contract = await this._getContract(networkId) const tx = contract.methods.addToken(tokenAddress).send({ from: userAddress }) if (txOptionalParams && txOptionalParams.onSentTransaction) { @@ -133,9 +137,18 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { } public async placeOrder(params: PlaceOrderParams): Promise { - const { userAddress, buyTokenId, sellTokenId, validUntil, buyAmount, sellAmount, txOptionalParams } = params - - const contract = await this._getContract() + const { + userAddress, + buyTokenId, + sellTokenId, + validUntil, + buyAmount, + sellAmount, + networkId, + txOptionalParams, + } = params + + const contract = await this._getContract(networkId) // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = contract.methods @@ -156,8 +169,13 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { return tx } - public async cancelOrders({ userAddress, orderIds, txOptionalParams }: CancelOrdersParams): Promise { - const contract = await this._getContract() + public async cancelOrders({ + userAddress, + orderIds, + networkId, + txOptionalParams, + }: CancelOrdersParams): Promise { + const contract = await this._getContract(networkId) const tx = contract.methods.cancelOrders(orderIds).send({ from: userAddress }) if (txOptionalParams && txOptionalParams.onSentTransaction) { diff --git a/src/components/DepositWidget/useRowActions.tsx b/src/components/DepositWidget/useRowActions.tsx index 257c19c79..91f61e997 100644 --- a/src/components/DepositWidget/useRowActions.tsx +++ b/src/components/DepositWidget/useRowActions.tsx @@ -36,6 +36,7 @@ export const useRowActions = (params: Params): Result => { async function enableToken(tokenAddress: string): Promise { try { assert(userAddress, 'User address missing. Aborting.') + assert(networkId, 'NetworkId missing. Aborting.') assert(contractAddress, 'Contract address missing. Aborting.') const token = getToken('address', tokenAddress, balances) @@ -49,6 +50,7 @@ export const useRowActions = (params: Params): Result => { userAddress, tokenAddress, spenderAddress: contractAddress, + networkId, amount: ALLOWANCE_MAX_VALUE, txOptionalParams, }) @@ -66,6 +68,7 @@ export const useRowActions = (params: Params): Result => { async function depositToken(amount: BN, tokenAddress: string): Promise { try { assert(userAddress, ON_ERROR_MESSAGE) + assert(networkId, ON_ERROR_MESSAGE) const token = getToken('address', tokenAddress, balances) assert(token, 'No token') @@ -75,7 +78,7 @@ export const useRowActions = (params: Params): Result => { const { symbol, decimals } = safeFilledToken(token) log(`Processing deposit of ${amount} ${symbol} from ${userAddress}`) - const receipt = await depositApi.deposit({ userAddress, tokenAddress, amount, txOptionalParams }) + const receipt = await depositApi.deposit({ userAddress, tokenAddress, networkId, amount, txOptionalParams }) log(`The transaction has been mined: ${receipt.transactionHash}`) toast.success(`Successfully deposited ${formatAmount(amount, decimals)} ${symbol}`) @@ -90,6 +93,7 @@ export const useRowActions = (params: Params): Result => { async function requestWithdrawToken(amount: BN, tokenAddress: string): Promise { try { assert(userAddress, ON_ERROR_MESSAGE) + assert(networkId, ON_ERROR_MESSAGE) const token = getToken('address', tokenAddress, balances) assert(token, 'No token') @@ -101,7 +105,13 @@ export const useRowActions = (params: Params): Result => { log(`Processing withdraw request of ${amount} ${symbol} from ${userAddress}`) log(`Processing withdraw request of ${amount} ${symbol} from ${userAddress}`) - const receipt = await depositApi.requestWithdraw({ userAddress, tokenAddress, amount, txOptionalParams }) + const receipt = await depositApi.requestWithdraw({ + userAddress, + tokenAddress, + networkId, + amount, + txOptionalParams, + }) log(`The transaction has been mined: ${receipt.transactionHash}`) toast.success(`Successfully requested withdraw of ${formatAmount(amount, decimals)} ${symbol}`) @@ -116,6 +126,7 @@ export const useRowActions = (params: Params): Result => { async function claimToken(tokenAddress: string): Promise { try { assert(userAddress, ON_ERROR_MESSAGE) + assert(networkId, ON_ERROR_MESSAGE) const token = getToken('address', tokenAddress, balances) assert(token, 'No token') @@ -126,7 +137,7 @@ export const useRowActions = (params: Params): Result => { dispatch(setHighlightAndClaimingAction(tokenAddress)) - const receipt = await depositApi.withdraw({ userAddress, tokenAddress, txOptionalParams }) + const receipt = await depositApi.withdraw({ userAddress, tokenAddress, networkId, txOptionalParams }) log(`The transaction has been mined: ${receipt.transactionHash}`) toast.success(`Withdraw of ${formatAmount(pendingWithdraw.amount, decimals)} ${symbol} completed`) diff --git a/src/components/OrdersWidget/useDeleteOrders.tsx b/src/components/OrdersWidget/useDeleteOrders.tsx index ced82f449..e0e49f6f2 100644 --- a/src/components/OrdersWidget/useDeleteOrders.tsx +++ b/src/components/OrdersWidget/useDeleteOrders.tsx @@ -22,7 +22,7 @@ function extractExchangeOrderIds(orderIds: string[]): number[] { export function useDeleteOrders(): Result { const [deleting, setDeleting] = useSafeState(false) - const { userAddress } = useWalletConnection() + const { userAddress, networkId } = useWalletConnection() const deleteOrders = useCallback( async (uiOrderIds: string[]): Promise => { @@ -30,13 +30,14 @@ export function useDeleteOrders(): Result { try { assert(userAddress, 'User address is missing. Aborting.') + assert(networkId, 'NetworkId is missing. Aborting.') assert(uiOrderIds.length > 0, 'No orders to cancel. Aborting.') setDeleting(true) const orderIds = extractExchangeOrderIds(uiOrderIds) - const receipt = await exchangeApi.cancelOrders({ userAddress, orderIds, txOptionalParams }) + const receipt = await exchangeApi.cancelOrders({ userAddress, orderIds, networkId, txOptionalParams }) log(`The transaction has been mined: ${receipt.transactionHash}`) @@ -53,7 +54,7 @@ export function useDeleteOrders(): Result { setDeleting(false) } }, - [setDeleting, userAddress], + [networkId, setDeleting, userAddress], ) return { deleteOrders, deleting } diff --git a/src/hooks/useEnableToken.ts b/src/hooks/useEnableToken.ts index 181470bf7..e28485da7 100644 --- a/src/hooks/useEnableToken.ts +++ b/src/hooks/useEnableToken.ts @@ -28,6 +28,7 @@ export const useEnableTokens = (params: Params): Result => { async function enableToken(): Promise { assert(!enabled && isConnected, 'The token was already enabled and/or user is not connected') assert(userAddress, 'User address not found. Please check wallet.') + assert(networkId, 'NetworkId not found. Please check wallet.') setEnabling(true) @@ -40,6 +41,7 @@ export const useEnableTokens = (params: Params): Result => { tokenAddress, spenderAddress: contractAddress, amount: ALLOWANCE_MAX_VALUE, + networkId, txOptionalParams, }) diff --git a/src/hooks/useOrders.ts b/src/hooks/useOrders.ts index ddd2fd9f1..38fe8a9bb 100644 --- a/src/hooks/useOrders.ts +++ b/src/hooks/useOrders.ts @@ -28,16 +28,17 @@ function filterDeletedOrders(orders: AuctionElement[]): AuctionElement[] { } export function useOrders(): AuctionElement[] { - const { userAddress } = useWalletConnection() + const { userAddress, networkId } = useWalletConnection() const [orders, setOrders] = useSafeState([]) useEffect(() => { userAddress && + networkId && exchangeApi - .getOrders({ userAddress }) + .getOrders({ userAddress, networkId }) .then(filterDeletedOrders) .then(setOrders) - }, [setOrders, userAddress]) + }, [networkId, setOrders, userAddress]) return orders } diff --git a/src/hooks/usePlaceOrder.ts b/src/hooks/usePlaceOrder.ts index 8bd94d124..b1405f556 100644 --- a/src/hooks/usePlaceOrder.ts +++ b/src/hooks/usePlaceOrder.ts @@ -25,11 +25,11 @@ interface Result { export const usePlaceOrder = (): Result => { const [isSubmitting, setIsSubmitting] = useSafeState(false) - const { userAddress } = useWalletConnection() + const { userAddress, networkId } = useWalletConnection() const placeOrder = useCallback( async ({ buyAmount, buyToken, sellAmount, sellToken }: PlaceOrderParams): Promise => { - if (!userAddress) { + if (!userAddress || !networkId) { toast.error('Wallet is not connected!') return false } @@ -43,9 +43,9 @@ export const usePlaceOrder = (): Result => { try { const [sellTokenId, buyTokenId, batchId] = await Promise.all([ - exchangeApi.getTokenIdByAddress({ tokenAddress: sellToken.address }), - exchangeApi.getTokenIdByAddress({ tokenAddress: buyToken.address }), - exchangeApi.getCurrentBatchId(), + exchangeApi.getTokenIdByAddress({ tokenAddress: sellToken.address, networkId }), + exchangeApi.getTokenIdByAddress({ tokenAddress: buyToken.address, networkId }), + exchangeApi.getCurrentBatchId(networkId), ]) if (sellTokenId !== 0 || buyTokenId !== 0) { @@ -60,6 +60,7 @@ export const usePlaceOrder = (): Result => { validUntil, buyAmount, sellAmount, + networkId, txOptionalParams, } const receipt = await exchangeApi.placeOrder(params) @@ -93,7 +94,7 @@ export const usePlaceOrder = (): Result => { setIsSubmitting(false) } }, - [setIsSubmitting, userAddress], + [networkId, setIsSubmitting, userAddress], ) return { placeOrder, isSubmitting } diff --git a/src/hooks/useTokenBalances.ts b/src/hooks/useTokenBalances.ts index 576f84189..fcf940faf 100644 --- a/src/hooks/useTokenBalances.ts +++ b/src/hooks/useTokenBalances.ts @@ -29,6 +29,7 @@ async function fetchBalancesForToken( token: TokenDetails, userAddress: string, contractAddress: string, + networkId: number, ): Promise { const tokenAddress = token.address const [ @@ -39,12 +40,12 @@ async function fetchBalancesForToken( walletBalance, allowance, ] = await Promise.all([ - depositApi.getBalance({ userAddress, tokenAddress }), - depositApi.getPendingDeposit({ userAddress, tokenAddress }), - depositApi.getPendingWithdraw({ userAddress, tokenAddress }), - depositApi.getCurrentBatchId(), - erc20Api.balanceOf({ userAddress, tokenAddress }), - erc20Api.allowance({ userAddress, tokenAddress, spenderAddress: contractAddress }), + depositApi.getBalance({ userAddress, tokenAddress, networkId }), + depositApi.getPendingDeposit({ userAddress, tokenAddress, networkId }), + depositApi.getPendingWithdraw({ userAddress, tokenAddress, networkId }), + depositApi.getCurrentBatchId(networkId), + erc20Api.balanceOf({ userAddress, tokenAddress, networkId }), + erc20Api.allowance({ userAddress, tokenAddress, networkId, spenderAddress: contractAddress }), ]) return { @@ -72,7 +73,7 @@ async function _getBalances(walletInfo: WalletInfo): Promise[] = tokens.map(token => - fetchBalancesForToken(token, userAddress, contractAddress).catch(e => { + fetchBalancesForToken(token, userAddress, contractAddress, networkId).catch(e => { log('Error for', token, userAddress, contractAddress) log(e) return null diff --git a/src/hooks/useWithdrawTokens.ts b/src/hooks/useWithdrawTokens.ts index 1ca80a601..5e401e932 100644 --- a/src/hooks/useWithdrawTokens.ts +++ b/src/hooks/useWithdrawTokens.ts @@ -15,7 +15,7 @@ interface Result { } export const useWithdrawTokens = (params: Params): Result => { - const { userAddress, isConnected } = useWalletConnection() + const { userAddress, isConnected, networkId } = useWalletConnection() const { tokenBalances: { enabled, address: tokenAddress, claimable }, txOptionalParams, @@ -28,10 +28,11 @@ export const useWithdrawTokens = (params: Params): Result => { assert(enabled, 'Token not enabled') assert(claimable, 'Withdraw not ready') assert(isConnected, "There's no connected wallet") + assert(networkId, 'No valid networkId found') setWithdrawing(true) - return depositApi.withdraw({ userAddress, tokenAddress, txOptionalParams }) + return depositApi.withdraw({ userAddress, tokenAddress, networkId, txOptionalParams }) } finally { setWithdrawing(false) } diff --git a/src/services/factories/addTokenToExchange.ts b/src/services/factories/addTokenToExchange.ts index 27c5ce408..bd006de34 100644 --- a/src/services/factories/addTokenToExchange.ts +++ b/src/services/factories/addTokenToExchange.ts @@ -15,6 +15,7 @@ interface Params { interface AddTokenToExchangeParams { userAddress: string tokenAddress: string + networkId: number } /** @@ -27,8 +28,8 @@ export function addTokenToExchangeFactory( ): (params: AddTokenToExchangeParams) => Promise { const { exchangeApi } = factoryParams - return async ({ userAddress, tokenAddress }: AddTokenToExchangeParams): Promise => { - const erc20Info = getErc20Info({ ...factoryParams, tokenAddress }) + return async ({ userAddress, tokenAddress, networkId }: AddTokenToExchangeParams): Promise => { + const erc20Info = getErc20Info({ ...factoryParams, tokenAddress, networkId }) if (!erc20Info) { log('Address %s does not contain a valid ERC20 token', tokenAddress) @@ -36,7 +37,7 @@ export function addTokenToExchangeFactory( } try { - await exchangeApi.addToken({ userAddress, tokenAddress }) + await exchangeApi.addToken({ userAddress, tokenAddress, networkId }) } catch (e) { log('Failed to add token (%s) to exchange', tokenAddress) return false @@ -45,7 +46,7 @@ export function addTokenToExchangeFactory( // TODO: I guess we might want to return the token and leave the proxy/cache layer to deal with it. // Revisit once we add it to the interface try { - const id = exchangeApi.getTokenIdByAddress({ tokenAddress }) + const id = exchangeApi.getTokenIdByAddress({ tokenAddress, networkId }) const token = { ...erc20Info, diff --git a/src/services/factories/getTokenFromExchange.ts b/src/services/factories/getTokenFromExchange.ts index 8390f36e9..ce41ec62c 100644 --- a/src/services/factories/getTokenFromExchange.ts +++ b/src/services/factories/getTokenFromExchange.ts @@ -11,6 +11,7 @@ import { TokenList } from 'api/tokenList/TokenListApi' interface TokenFromErc20Params { tokenAddress: string tokenId: number + networkId: number erc20Api: Erc20Api web3: Web3 } @@ -69,7 +70,7 @@ function getTokenFromExchangeByAddressFactory( let tokenId: number try { - tokenId = await exchangeApi.getTokenIdByAddress({ tokenAddress }) + tokenId = await exchangeApi.getTokenIdByAddress({ tokenAddress, networkId }) } catch (e) { log('Token with address %s not registered on contract', tokenAddress, e) return null @@ -82,7 +83,7 @@ function getTokenFromExchangeByAddressFactory( } // Not there, get it from the ERC20 contract - return getTokenFromErc20({ ...factoryParams, tokenAddress, tokenId }) + return getTokenFromErc20({ ...factoryParams, tokenAddress, tokenId, networkId }) } } @@ -113,7 +114,7 @@ function getTokenFromExchangeByIdFactory( // Not there, get the address from the contract let tokenAddress: string try { - tokenAddress = await exchangeApi.getTokenAddressById({ tokenId }) + tokenAddress = await exchangeApi.getTokenAddressById({ tokenId, networkId }) } catch (e) { log('Token with id %d not registered on contract', tokenId, e) return null @@ -127,7 +128,7 @@ function getTokenFromExchangeByIdFactory( } // Not there, get it from the ERC20 contract - return getTokenFromErc20({ ...factoryParams, tokenId, tokenAddress }) + return getTokenFromErc20({ ...factoryParams, tokenId, tokenAddress, networkId }) } } diff --git a/src/services/helpers/getErc20Info.ts b/src/services/helpers/getErc20Info.ts index b6bc76ee3..f9b9d0f36 100644 --- a/src/services/helpers/getErc20Info.ts +++ b/src/services/helpers/getErc20Info.ts @@ -19,6 +19,7 @@ async function wrapPromise(promise: Promise): Promise { interface Params { tokenAddress: string + networkId: number erc20Api: Erc20Api web3: Web3 } @@ -26,7 +27,12 @@ interface Params { /** * Fetches info for an arbitrary ERC20 token from given address */ -export async function getErc20Info({ tokenAddress, erc20Api, web3 }: Params): Promise { +export async function getErc20Info({ + tokenAddress, + networkId, + erc20Api, + web3, +}: Params): Promise { // First check whether given address is a contract const code = await web3.eth.getCode(tokenAddress) if (code === '0x') { @@ -38,7 +44,7 @@ export async function getErc20Info({ tokenAddress, erc20Api, web3 }: Params): Pr try { // totalSupply is an ERC20 mandatory read only method. // if the call succeeds, we assume it's compliant - await erc20Api.totalSupply({ tokenAddress }) + await erc20Api.totalSupply({ tokenAddress, networkId }) } catch (e) { log('Address %s is not ERC20 compliant', tokenAddress, e) return null @@ -46,9 +52,9 @@ export async function getErc20Info({ tokenAddress, erc20Api, web3 }: Params): Pr // Query for optional details. Do not fail in case any is missing. const [symbol, name, decimals] = await Promise.all([ - wrapPromise(erc20Api.symbol({ tokenAddress })), - wrapPromise(erc20Api.name({ tokenAddress })), - wrapPromise(erc20Api.decimals({ tokenAddress })), + wrapPromise(erc20Api.symbol({ tokenAddress, networkId })), + wrapPromise(erc20Api.name({ tokenAddress, networkId })), + wrapPromise(erc20Api.decimals({ tokenAddress, networkId })), ]) return { address: tokenAddress, symbol, name, decimals: decimals || DEFAULT_PRECISION } } diff --git a/test/api/ExchangeApi/DepositApiMock.test.ts b/test/api/ExchangeApi/DepositApiMock.test.ts index f8bc56487..05641ec11 100644 --- a/test/api/ExchangeApi/DepositApiMock.test.ts +++ b/test/api/ExchangeApi/DepositApiMock.test.ts @@ -14,6 +14,7 @@ let instance: DepositApi let mockErc20Api: Erc20Api const ZERO_FLUX = createFlux() +const NETWORK_ID = 0 beforeAll(() => { testHelpers.mockTimes() @@ -57,17 +58,17 @@ beforeEach(() => { describe('Basic view functions', () => { test('Get batch time', async () => { - const batchTime = await instance.getBatchTime() + const batchTime = await instance.getBatchTime(NETWORK_ID) expect(batchTime).toBe(BATCH_TIME) }) test('Get current batch id (state index)', async () => { - const batchId = await instance.getCurrentBatchId() + const batchId = await instance.getCurrentBatchId(NETWORK_ID) expect(batchId).toBe(testHelpers.BATCH_ID) }) test('Get seconds remaining in batch', async () => { - const remainingSeconds = await instance.getSecondsRemainingInBatch() + const remainingSeconds = await instance.getSecondsRemainingInBatch(NETWORK_ID) expect(remainingSeconds).toBe(BATCH_TIME - testHelpers.BATCH_SECOND) }) }) @@ -77,7 +78,7 @@ describe('Get balance', () => { // GIVEN: A user that doesn't have any deposits // WHEN: We get the balance - const balance = await instance.getBalance({ userAddress: USER_2, tokenAddress: TOKEN_1 }) + const balance = await instance.getBalance({ userAddress: USER_2, tokenAddress: TOKEN_1, networkId: NETWORK_ID }) // THEN: The balance is zero expect(balance).toEqual(ZERO) @@ -87,7 +88,7 @@ describe('Get balance', () => { // GIVEN: A user that has deposits for some tokens, but not for TOKEN_3 // WHEN: We get the balance for TOKEN_3 - const balance = await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3 }) + const balance = await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID }) // THEN: The balance is zero expect(balance).toEqual(ZERO) @@ -97,7 +98,7 @@ describe('Get balance', () => { // GIVEN: A user that has balance zero for TOKEN_1 // WHEN: We get the balance for TOKEN_1 - const balance = await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1 }) + const balance = await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID }) // THEN: The balance is zero expect(balance).toEqual(ZERO) @@ -107,7 +108,7 @@ describe('Get balance', () => { // GIVEN: A user that has balance for TOKEN_2 // WHEN: We get the balance for TOKEN_2 - const balance = await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_2 }) + const balance = await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_2, networkId: NETWORK_ID }) // THEN: The balance is AMOUNT expect(balance).toEqual(AMOUNT) @@ -120,7 +121,9 @@ describe('Get pending deposit amounts', () => { // WHEN: - // THEN: there's no pending deposits nor batchId - expect(await instance.getPendingDeposit({ userAddress: USER_2, tokenAddress: TOKEN_1 })).toEqual(ZERO_FLUX) + expect( + await instance.getPendingDeposit({ userAddress: USER_2, tokenAddress: TOKEN_1, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) }) test('Unknown token', async () => { @@ -128,7 +131,9 @@ describe('Get pending deposit amounts', () => { // WHEN: - // THEN: there's no pending deposits nor batchId - expect(await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO_FLUX) + expect( + await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) }) test('No pending balance', async () => { @@ -136,7 +141,9 @@ describe('Get pending deposit amounts', () => { // WHEN: - // THEN: there's no pending deposits - expect(await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_2 })).toEqual(ZERO_FLUX) + expect( + await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_2, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) }) }) @@ -146,7 +153,9 @@ describe('Get pending withdraw amounts', () => { // WHEN: - // THEN: there's no pending withdraws nor batchId - expect(await instance.getPendingWithdraw({ userAddress: USER_2, tokenAddress: TOKEN_1 })).toEqual(ZERO_FLUX) + expect( + await instance.getPendingWithdraw({ userAddress: USER_2, tokenAddress: TOKEN_1, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) }) test('Unknown token', async () => { @@ -154,7 +163,9 @@ describe('Get pending withdraw amounts', () => { // WHEN: - // THEN: there's no pending withdraws nor batchId - expect(await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO_FLUX) + expect( + await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) }) test('No pending balance', async () => { @@ -162,74 +173,120 @@ describe('Get pending withdraw amounts', () => { // WHEN: - // THEN: there's no pending withdraws - expect(await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_2 })).toEqual(ZERO_FLUX) + expect( + await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_2, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) }) }) describe('Deposit', () => { test('Unknown token', async () => { // GIVEN: An user with no token balance for TOKEN_3 and no pending deposits - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO) - expect(await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO_FLUX) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + expect( + await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) // WHEN: Deposits AMOUNT - await instance.deposit({ userAddress: USER_1, tokenAddress: TOKEN_3, amount: AMOUNT }) + await instance.deposit({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID, amount: AMOUNT }) // THEN: There's still no balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID })).toEqual( + ZERO, + ) // THEN: There's a pending deposit of AMOUNT - const { amount } = await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_3 }) + const { amount } = await instance.getPendingDeposit({ + userAddress: USER_1, + tokenAddress: TOKEN_3, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) }) test('No balance', async () => { // GIVEN: An user with no token balance for TOKEN_1 and no pending deposits - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO) - expect(await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO_FLUX) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + expect( + await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) // WHEN: Deposits AMOUNT - await instance.deposit({ userAddress: USER_1, tokenAddress: TOKEN_1, amount: AMOUNT }) + await instance.deposit({ userAddress: USER_1, tokenAddress: TOKEN_1, amount: AMOUNT, networkId: NETWORK_ID }) // THEN: There's still no balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID })).toEqual( + ZERO, + ) // THEN: There's a pending deposit of AMOUNT - const { amount } = await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_1 }) + const { amount } = await instance.getPendingDeposit({ + userAddress: USER_1, + tokenAddress: TOKEN_1, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) }) test('Applicable pending balance', async () => { // GIVEN: An user with no balance for TOKEN_4, and applicable pending deposits - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4 })).toEqual(ZERO) - const { amount } = await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + const { amount } = await instance.getPendingDeposit({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) // WHEN: Deposits AMOUNT - await instance.deposit({ userAddress: USER_1, tokenAddress: TOKEN_4, amount: AMOUNT }) + await instance.deposit({ userAddress: USER_1, tokenAddress: TOKEN_4, amount: AMOUNT, networkId: NETWORK_ID }) // THEN: The previous pending deposit is applied - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4 })).toEqual(AMOUNT) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID })).toEqual( + AMOUNT, + ) // THEN: There's a pending deposit of AMOUNT - const { amount: amount2 } = await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + const { amount: amount2 } = await instance.getPendingDeposit({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + }) expect(amount2).toEqual(AMOUNT) }) test('Unapplicable pending balance', async () => { // GIVEN: An user with an unapplicable pending deposit for TOKEN_5 - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_5 })).toEqual(AMOUNT) - const { amount } = await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_5 }) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_5, networkId: NETWORK_ID })).toEqual( + AMOUNT, + ) + const { amount } = await instance.getPendingDeposit({ + userAddress: USER_1, + tokenAddress: TOKEN_5, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) // WHEN: Deposits AMOUNT - await instance.deposit({ userAddress: USER_1, tokenAddress: TOKEN_5, amount: AMOUNT }) + await instance.deposit({ userAddress: USER_1, tokenAddress: TOKEN_5, amount: AMOUNT, networkId: NETWORK_ID }) // THEN: There's still the same balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_5 })).toEqual(AMOUNT) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_5, networkId: NETWORK_ID })).toEqual( + AMOUNT, + ) // THEN: There's a pending deposit of AMOUNT + AMOUNT - const { amount: amount2 } = await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_5 }) + const { amount: amount2 } = await instance.getPendingDeposit({ + userAddress: USER_1, + tokenAddress: TOKEN_5, + networkId: NETWORK_ID, + }) expect(amount2).toEqual(AMOUNT.add(AMOUNT)) }) }) @@ -237,67 +294,131 @@ describe('Deposit', () => { describe('Request withdraw', () => { test('Unknown token', async () => { // GIVEN: An user with no token balance for TOKEN_3 and no pending withdraw - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO) - expect(await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO_FLUX) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + expect( + await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) // WHEN: Requesting a withdraw of AMOUNT - await instance.requestWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_3, amount: AMOUNT }) + await instance.requestWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_3, + amount: AMOUNT, + networkId: NETWORK_ID, + }) // THEN: There's still no balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID })).toEqual( + ZERO, + ) // THEN: There's a pending withdraw of AMOUNT - const { amount } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_3 }) + const { amount } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_3, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) }) test('No balance', async () => { // GIVEN: An user with no token balance for TOKEN_1 and no pending withdraw - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO) - expect(await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO_FLUX) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + expect( + await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) // WHEN: Requesting a withdraw of AMOUNT - await instance.requestWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_1, amount: AMOUNT }) + await instance.requestWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_1, + amount: AMOUNT, + networkId: NETWORK_ID, + }) // THEN: There's still no balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID })).toEqual( + ZERO, + ) // THEN: There's a pending withdraw of AMOUNT - const { amount } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_1 }) + const { amount } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_1, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) }) test('Increase previous withdraw request amount', async () => { // GIVEN: An user with no balance for TOKEN_4, and a previous pending withdraw of AMOUNT - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4 })).toEqual(ZERO) - const { amount } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + const { amount } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) // WHEN: Requesting a withdraw of 2 * AMOUNT - await instance.requestWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_4, amount: AMOUNT.mul(TWO) }) + await instance.requestWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + amount: AMOUNT.mul(TWO), + }) // THEN: There's still no balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4 })).toEqual(ZERO) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID })).toEqual( + ZERO, + ) // THEN: There's a pending withdraw of 2 * AMOUNT - const { amount: amount2 } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + const { amount: amount2 } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + }) expect(amount2).toEqual(AMOUNT.mul(TWO)) }) test('Decrease previous withdraw request amount', async () => { // GIVEN: An user with no balance for TOKEN_4, and a previous pending withdraw of AMOUNT - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4 })).toEqual(ZERO) - const { amount } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + const { amount } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) // WHEN: Requesting a withdraw of 2 * AMOUNT - await instance.requestWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_4, amount: AMOUNT.div(TWO) }) + await instance.requestWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + amount: AMOUNT.div(TWO), + }) // THEN: There's still no balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4 })).toEqual(ZERO) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID })).toEqual( + ZERO, + ) // THEN: There's a pending withdraw of 2 * AMOUNT - const { amount: amount2 } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + const { amount: amount2 } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + }) expect(amount2).toEqual(AMOUNT.div(TWO)) }) }) @@ -305,96 +426,150 @@ describe('Request withdraw', () => { describe('Withdraw', () => { test('Unknown token', async () => { // GIVEN: An user with no token balance for TOKEN_3 and no pending withdraw - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO) - expect(await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO_FLUX) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + expect( + await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) // WHEN: Withdraw AMOUNT - const withdrawPromise = instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_3 }) + const withdrawPromise = instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID }) // THEN: The withdraw fails await expect(withdrawPromise).rejects.toBeTruthy() // THEN: There's still no balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3 })).toEqual(ZERO) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID })).toEqual( + ZERO, + ) // THEN: There's still no pending withdraw - const { amount } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_3 }) + const { amount } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_3, + networkId: NETWORK_ID, + }) expect(amount).toEqual(ZERO) }) test('No balance', async () => { // GIVEN: An user with no token balance for TOKEN_1 and no pending withdraw - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO) - expect(await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO_FLUX) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + expect( + await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) // WHEN: Withdraw AMOUNT - const withdrawPromise = instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_1 }) + const withdrawPromise = instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID }) // THEN: The withdraw fails await expect(withdrawPromise).rejects.toBeTruthy() // THEN: There's still no balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID })).toEqual( + ZERO, + ) // THEN: There's still no pending withdraw - expect(await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_1 })).toEqual(ZERO_FLUX) + expect( + await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_1, networkId: NETWORK_ID }), + ).toEqual(ZERO_FLUX) }) test('Settled withdraw request, but not balance', async () => { // GIVEN: An user with no balance for TOKEN_4, and a previous pending withdraw of AMOUNT - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4 })).toEqual(ZERO) - const { amount } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID })).toEqual( + ZERO, + ) + const { amount } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) // WHEN: Withdraw AMOUNT - const withdrawPromise = instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + const withdrawPromise = instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID }) // THEN: The withdraw fails await expect(withdrawPromise).rejects.toBeTruthy() // THEN: There's still no balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4 })).toEqual(ZERO) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID })).toEqual( + ZERO, + ) // THEN: There's still a withdraw request - const { amount: amount2 } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + const { amount: amount2 } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_4, + networkId: NETWORK_ID, + }) expect(amount2).toEqual(AMOUNT) }) test('Settled withdraw request', async () => { // GIVEN: An user with AMOUNT balance for TOKEN_6, and a previous pending withdraw of AMOUNT_SMALL - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_6 })).toEqual(AMOUNT) - const { amount } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_6 }) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_6, networkId: NETWORK_ID })).toEqual( + AMOUNT, + ) + const { amount } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_6, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT_SMALL) // WHEN: Withdraw AMOUNT_SMALL - await instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_6 }) + await instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_6, networkId: NETWORK_ID }) // THEN: There remaining balance is AMOUNT - AMOUNT_SMALL - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_6 })).toEqual(AMOUNT.sub(AMOUNT_SMALL)) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_6, networkId: NETWORK_ID })).toEqual( + AMOUNT.sub(AMOUNT_SMALL), + ) // THEN: There's no pending withdraw anymore - const { amount: amount2 } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_6 }) + const { amount: amount2 } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_6, + networkId: NETWORK_ID, + }) expect(amount2).toEqual(ZERO) }) test('Unsettled withdraw request', async () => { // GIVEN: An user with a non applicable withdraw request on TOKEN_5 - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_5 })).toEqual(AMOUNT) - const { amount, batchId } = await instance.getPendingDeposit({ userAddress: USER_1, tokenAddress: TOKEN_5 }) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_5, networkId: NETWORK_ID })).toEqual( + AMOUNT, + ) + const { amount, batchId } = await instance.getPendingDeposit({ + userAddress: USER_1, + tokenAddress: TOKEN_5, + networkId: NETWORK_ID, + }) expect(amount).toEqual(AMOUNT) - expect(await instance.getCurrentBatchId()).toBeGreaterThanOrEqual(batchId) + expect(await instance.getCurrentBatchId(NETWORK_ID)).toBeGreaterThanOrEqual(batchId) // WHEN: Withdraw AMOUNT - const withdrawPromise = instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_5 }) + const withdrawPromise = instance.withdraw({ userAddress: USER_1, tokenAddress: TOKEN_5, networkId: NETWORK_ID }) // THEN: The withdraw fails await expect(withdrawPromise).rejects.toBeTruthy() // THEN: There's still the same balance - expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_5 })).toEqual(AMOUNT) + expect(await instance.getBalance({ userAddress: USER_1, tokenAddress: TOKEN_5, networkId: NETWORK_ID })).toEqual( + AMOUNT, + ) // THEN: There's still the same withdraw request - const { amount: amount2 } = await instance.getPendingWithdraw({ userAddress: USER_1, tokenAddress: TOKEN_5 }) + const { amount: amount2 } = await instance.getPendingWithdraw({ + userAddress: USER_1, + tokenAddress: TOKEN_5, + networkId: NETWORK_ID, + }) expect(amount2).toEqual(AMOUNT) }) }) diff --git a/test/api/ExchangeApi/Erc20ApiMock.test.ts b/test/api/ExchangeApi/Erc20ApiMock.test.ts index 588db16b4..5ceb120d5 100644 --- a/test/api/ExchangeApi/Erc20ApiMock.test.ts +++ b/test/api/ExchangeApi/Erc20ApiMock.test.ts @@ -20,60 +20,84 @@ import { ZERO, ALLOWANCE_MAX_VALUE } from 'const' import BN from 'bn.js' import { clone } from '../../testHelpers' +const NETWORK_ID = 0 + let instance: Erc20Api = new Erc20ApiMock({ balances: BALANCES, allowances: ALLOWANCES, tokens: unregisteredTokens }) describe('Basic view functions', () => { describe('balanceOf', () => { it('returns balance', async () => { const token1Balance = BALANCES[USER_1][TOKEN_1] - expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_1 })).toBe(token1Balance) + expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_1, networkId: NETWORK_ID })).toBe( + token1Balance, + ) }) it('returns 0 when not found', async () => { - expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2 })).toBe(ZERO) + expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2, networkId: NETWORK_ID })).toBe(ZERO) }) }) describe('allowance', () => { it('returns allowance', async () => { const allowance = ALLOWANCES[USER_1][TOKEN_6][CONTRACT] - expect(await instance.allowance({ tokenAddress: TOKEN_6, userAddress: USER_1, spenderAddress: CONTRACT })).toBe( - allowance, - ) + expect( + await instance.allowance({ + tokenAddress: TOKEN_6, + userAddress: USER_1, + spenderAddress: CONTRACT, + networkId: NETWORK_ID, + }), + ).toBe(allowance) }) it('user without allowance set returns 0', async () => { - expect(await instance.allowance({ tokenAddress: TOKEN_1, userAddress: USER_2, spenderAddress: CONTRACT })).toBe( - ZERO, - ) + expect( + await instance.allowance({ + tokenAddress: TOKEN_1, + userAddress: USER_2, + spenderAddress: CONTRACT, + networkId: NETWORK_ID, + }), + ).toBe(ZERO) }) it('token without allowance set returns 0', async () => { - expect(await instance.allowance({ tokenAddress: TOKEN_8, userAddress: USER_1, spenderAddress: CONTRACT })).toBe( - ZERO, - ) + expect( + await instance.allowance({ + tokenAddress: TOKEN_8, + userAddress: USER_1, + spenderAddress: CONTRACT, + networkId: NETWORK_ID, + }), + ).toBe(ZERO) }) it('spender allowance 0 returns 0', async () => { - expect(await instance.allowance({ tokenAddress: TOKEN_1, userAddress: USER_1, spenderAddress: CONTRACT })).toBe( - ZERO, - ) + expect( + await instance.allowance({ + tokenAddress: TOKEN_1, + userAddress: USER_1, + spenderAddress: CONTRACT, + networkId: NETWORK_ID, + }), + ).toBe(ZERO) }) }) describe('totalSupply', () => { it('returns totalSupply', async () => { - expect(await instance.totalSupply({ tokenAddress: TOKEN_1 })).toBe(ALLOWANCE_MAX_VALUE) + expect(await instance.totalSupply({ tokenAddress: TOKEN_1, networkId: NETWORK_ID })).toBe(ALLOWANCE_MAX_VALUE) }) }) describe('name', () => { it('returns name', async () => { - expect(await instance.name({ tokenAddress: FEE_TOKEN })).toBe('Fee token') + expect(await instance.name({ tokenAddress: FEE_TOKEN, networkId: NETWORK_ID })).toBe('Fee token') }) it("throws when there's no name", async () => { try { - await instance.name({ tokenAddress: TOKEN_1 }) + await instance.name({ tokenAddress: TOKEN_1, networkId: NETWORK_ID }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/token does not implement 'name'/) @@ -83,11 +107,11 @@ describe('Basic view functions', () => { describe('symbol', () => { it('returns symbol', async () => { - expect(await instance.symbol({ tokenAddress: FEE_TOKEN })).toBe('FEET') + expect(await instance.symbol({ tokenAddress: FEE_TOKEN, networkId: NETWORK_ID })).toBe('FEET') }) it("throws when there's no symbol", async () => { try { - await instance.symbol({ tokenAddress: TOKEN_1 }) + await instance.symbol({ tokenAddress: TOKEN_1, networkId: NETWORK_ID }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/token does not implement 'symbol'/) @@ -97,11 +121,11 @@ describe('Basic view functions', () => { describe('decimals', () => { it('returns decimals', async () => { - expect(await instance.decimals({ tokenAddress: FEE_TOKEN })).toBe(18) + expect(await instance.decimals({ tokenAddress: FEE_TOKEN, networkId: NETWORK_ID })).toBe(18) }) it("throws when there's no decimals", async () => { try { - await instance.decimals({ tokenAddress: TOKEN_1 }) + await instance.decimals({ tokenAddress: TOKEN_1, networkId: NETWORK_ID }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/token does not implement 'decimals'/) @@ -131,11 +155,17 @@ describe('Write functions', () => { spenderAddress: USER_2, amount, txOptionalParams, + networkId: NETWORK_ID, }) - expect(await instance.allowance({ tokenAddress: TOKEN_1, userAddress: USER_1, spenderAddress: USER_2 })).toBe( - amount, - ) + expect( + await instance.allowance({ + tokenAddress: TOKEN_1, + userAddress: USER_1, + spenderAddress: USER_2, + networkId: NETWORK_ID, + }), + ).toBe(amount) expect(result).toBe(RECEIPT) }) @@ -146,6 +176,7 @@ describe('Write functions', () => { spenderAddress: USER_2, amount, txOptionalParams, + networkId: NETWORK_ID, }) expect(mockFunction.mock.calls.length).toBe(1) }) @@ -154,27 +185,38 @@ describe('Write functions', () => { describe('transfer', () => { const amount = new BN('987542934752394') it('transfers', async () => { - const contractBalance = await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: CONTRACT }) - const userBalance = await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2 }) + const contractBalance = await instance.balanceOf({ + tokenAddress: TOKEN_1, + userAddress: CONTRACT, + networkId: NETWORK_ID, + }) + const userBalance = await instance.balanceOf({ + tokenAddress: TOKEN_1, + userAddress: USER_2, + networkId: NETWORK_ID, + }) const result = await instance.transfer({ userAddress: CONTRACT, tokenAddress: TOKEN_1, toAddress: USER_2, amount, + networkId: NETWORK_ID, }) - expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: CONTRACT })).toEqual( + expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: CONTRACT, networkId: NETWORK_ID })).toEqual( contractBalance.sub(amount), ) - expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2 })).toEqual(userBalance.add(amount)) + expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2, networkId: NETWORK_ID })).toEqual( + userBalance.add(amount), + ) expect(result).toBe(RECEIPT) }) it('does not transfer when balance is insufficient', async () => { // TODO: after hours, couldn't figure out a way to check for the AssertionError using expect().toThrow() await instance - .transfer({ userAddress: USER_2, tokenAddress: TOKEN_1, toAddress: CONTRACT, amount }) + .transfer({ userAddress: USER_2, tokenAddress: TOKEN_1, toAddress: CONTRACT, amount, networkId: NETWORK_ID }) .then(() => fail('Should not succeed')) .catch(e => { expect(e.message).toMatch(/^The user doesn't have enough balance$/) @@ -188,6 +230,7 @@ describe('Write functions', () => { toAddress: USER_2, amount, txOptionalParams, + networkId: NETWORK_ID, }) expect(mockFunction.mock.calls.length).toBe(1) }) @@ -196,14 +239,20 @@ describe('Write functions', () => { const amount = new BN('78565893578') it('transfers and allowance is deduced', async () => { - const expectedUser1Balance = (await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_1 })).sub( - amount, - ) - const expectedUser2Balance = (await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2 })).add( - amount, - ) + const expectedUser1Balance = ( + await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_1, networkId: NETWORK_ID }) + ).sub(amount) + const expectedUser2Balance = ( + await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2, networkId: NETWORK_ID }) + ).add(amount) - await instance.approve({ userAddress: USER_1, tokenAddress: TOKEN_1, spenderAddress: USER_3, amount }) + await instance.approve({ + userAddress: USER_1, + tokenAddress: TOKEN_1, + spenderAddress: USER_3, + amount, + networkId: NETWORK_ID, + }) const result = await instance.transferFrom({ userAddress: USER_3, @@ -211,21 +260,46 @@ describe('Write functions', () => { fromAddress: USER_1, toAddress: USER_2, amount, + networkId: NETWORK_ID, }) - expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_1 })).toEqual(expectedUser1Balance) - expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2 })).toEqual(expectedUser2Balance) + expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_1, networkId: NETWORK_ID })).toEqual( + expectedUser1Balance, + ) + expect(await instance.balanceOf({ tokenAddress: TOKEN_1, userAddress: USER_2, networkId: NETWORK_ID })).toEqual( + expectedUser2Balance, + ) expect( - (await instance.allowance({ tokenAddress: TOKEN_1, userAddress: USER_1, spenderAddress: USER_3 })).toString(), + ( + await instance.allowance({ + tokenAddress: TOKEN_1, + userAddress: USER_1, + spenderAddress: USER_3, + networkId: NETWORK_ID, + }) + ).toString(), ).toEqual(ZERO.toString()) expect(result).toBe(RECEIPT) }) it('does not transfer when balance is insufficient', async () => { - await instance.approve({ userAddress: USER_2, tokenAddress: TOKEN_3, spenderAddress: USER_3, amount }) + await instance.approve({ + userAddress: USER_2, + tokenAddress: TOKEN_3, + spenderAddress: USER_3, + amount, + networkId: NETWORK_ID, + }) await instance - .transferFrom({ userAddress: USER_3, tokenAddress: TOKEN_3, fromAddress: USER_2, toAddress: USER_1, amount }) + .transferFrom({ + userAddress: USER_3, + tokenAddress: TOKEN_3, + fromAddress: USER_2, + toAddress: USER_1, + amount, + networkId: NETWORK_ID, + }) .then(() => { fail('Should not succeed') }) @@ -236,7 +310,14 @@ describe('Write functions', () => { it('does not transfer when allowance is insufficient', async () => { await instance - .transferFrom({ userAddress: USER_3, tokenAddress: TOKEN_3, fromAddress: USER_1, toAddress: USER_2, amount }) + .transferFrom({ + userAddress: USER_3, + tokenAddress: TOKEN_3, + fromAddress: USER_1, + toAddress: USER_2, + amount, + networkId: NETWORK_ID, + }) .then(() => { fail('Should not succeed') }) @@ -246,7 +327,13 @@ describe('Write functions', () => { }) it('calls optional callback', async () => { - await instance.approve({ userAddress: USER_1, tokenAddress: TOKEN_1, spenderAddress: USER_3, amount }) + await instance.approve({ + userAddress: USER_1, + tokenAddress: TOKEN_1, + spenderAddress: USER_3, + amount, + networkId: NETWORK_ID, + }) await instance.transferFrom({ userAddress: USER_3, tokenAddress: TOKEN_1, @@ -254,6 +341,7 @@ describe('Write functions', () => { toAddress: USER_2, amount, txOptionalParams, + networkId: NETWORK_ID, }) expect(mockFunction.mock.calls.length).toBe(1) }) diff --git a/test/api/ExchangeApi/ExchangeApiMock.test.ts b/test/api/ExchangeApi/ExchangeApiMock.test.ts index c62f9b8f5..b6cb3a22f 100644 --- a/test/api/ExchangeApi/ExchangeApiMock.test.ts +++ b/test/api/ExchangeApi/ExchangeApiMock.test.ts @@ -15,6 +15,7 @@ jest.mock('api/erc20/Erc20ApiMock') let instance: ExchangeApi let mockErc20Api: Erc20Api const tokens = [FEE_TOKEN, TOKEN_1, TOKEN_2] +const NETWORK_ID = 0 beforeAll(() => { testHelpers.mockTimes() @@ -45,20 +46,20 @@ beforeEach(() => { describe('Basic view functions', () => { test('fee denominator', async () => { - expect(await instance.getFeeDenominator()).toBe(FEE_DENOMINATOR) + expect(await instance.getFeeDenominator(NETWORK_ID)).toBe(FEE_DENOMINATOR) }) test('num tokens', async () => { - expect(await instance.getNumTokens()).toBe(tokens.length) + expect(await instance.getNumTokens(NETWORK_ID)).toBe(tokens.length) }) describe('get orders', () => { it('returns empty when no orders placed by user', async () => { - expect(await instance.getOrders({ userAddress: USER_2 })).toHaveLength(0) + expect(await instance.getOrders({ userAddress: USER_2, networkId: NETWORK_ID })).toHaveLength(0) }) it('returns all placed orders', async () => { - expect(await instance.getOrders({ userAddress: USER_1 })).toHaveLength(1) + expect(await instance.getOrders({ userAddress: USER_1, networkId: NETWORK_ID })).toHaveLength(1) }) }) }) @@ -66,12 +67,12 @@ describe('Basic view functions', () => { describe('Token view methods', () => { describe('get token id by address', () => { it('returns correct id when found', async () => { - expect(await instance.getTokenIdByAddress({ tokenAddress: tokens[1] })).toBe(1) + expect(await instance.getTokenIdByAddress({ tokenAddress: tokens[1], networkId: NETWORK_ID })).toBe(1) }) it('throws when id not found', async () => { try { - await instance.getTokenIdByAddress({ tokenAddress: TOKEN_4 }) + await instance.getTokenIdByAddress({ tokenAddress: TOKEN_4, networkId: NETWORK_ID }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/^Must have Address to get ID$/) @@ -81,12 +82,12 @@ describe('Token view methods', () => { describe('get token address by id', () => { it('returns correct address when found', async () => { - expect(await instance.getTokenAddressById({ tokenId: 0 })).toBe(tokens[0]) + expect(await instance.getTokenAddressById({ tokenId: 0, networkId: NETWORK_ID })).toBe(tokens[0]) }) it('throws when address not found', async () => { try { - await instance.getTokenAddressById({ tokenId: 10 }) + await instance.getTokenAddressById({ tokenId: 10, networkId: NETWORK_ID }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/^Must have ID to get Address$/) @@ -97,18 +98,18 @@ describe('Token view methods', () => { describe('addToken', () => { it('adds token not registered', async () => { - const tokenCount = await instance.getNumTokens() + const tokenCount = await instance.getNumTokens(NETWORK_ID) - await instance.addToken({ userAddress: USER_1, tokenAddress: TOKEN_3 }) + await instance.addToken({ userAddress: USER_1, tokenAddress: TOKEN_3, networkId: NETWORK_ID }) - expect(await instance.getNumTokens()).toBe(tokenCount + 1) - expect(await instance.getTokenIdByAddress({ tokenAddress: TOKEN_3 })).toBe(tokenCount) - expect(await instance.getTokenAddressById({ tokenId: tokenCount })).toBe(TOKEN_3) + expect(await instance.getNumTokens(NETWORK_ID)).toBe(tokenCount + 1) + expect(await instance.getTokenIdByAddress({ tokenAddress: TOKEN_3, networkId: NETWORK_ID })).toBe(tokenCount) + expect(await instance.getTokenAddressById({ tokenId: tokenCount, networkId: NETWORK_ID })).toBe(TOKEN_3) }) it('throws when token already registered', async () => { try { - await instance.addToken({ userAddress: USER_1, tokenAddress: tokens[0] }) + await instance.addToken({ userAddress: USER_1, tokenAddress: tokens[0], networkId: NETWORK_ID }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/^Token already registered$/) @@ -116,7 +117,7 @@ describe('addToken', () => { }) it('throws when MAX_TOKENS reached', async () => { try { - await instance.addToken({ userAddress: USER_1, tokenAddress: TOKEN_4 }) + await instance.addToken({ userAddress: USER_1, tokenAddress: TOKEN_4, networkId: NETWORK_ID }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/^Max tokens reached$/) @@ -143,51 +144,52 @@ describe('placeOrder', () => { validUntil: expected.validUntil, buyAmount: expected.priceNumerator, sellAmount: expected.priceDenominator, + networkId: NETWORK_ID, } test('place order not first', async () => { params.userAddress = USER_1 const response = await instance.placeOrder(params) expect(response).toBe(RECEIPT) - const actual = (await instance.getOrders({ userAddress: USER_1 })).pop() + const actual = (await instance.getOrders({ userAddress: USER_1, networkId: NETWORK_ID })).pop() expect(actual).toEqual({ ...expected, user: USER_1, id: '1' }) }) test('place first order', async () => { - expect((await instance.getOrders({ userAddress: USER_3 })).length).toBe(0) + expect((await instance.getOrders({ userAddress: USER_3, networkId: NETWORK_ID })).length).toBe(0) params.userAddress = USER_2 const response = await instance.placeOrder(params) expect(response).toBe(RECEIPT) - const actual = (await instance.getOrders({ userAddress: USER_2 })).pop() + const actual = (await instance.getOrders({ userAddress: USER_2, networkId: NETWORK_ID })).pop() expect(actual).toEqual({ ...expected, user: USER_2, id: '0' }) }) }) describe('cancelOrder', () => { test('cancel existing order', async () => { - const orderId = (await instance.getOrders({ userAddress: USER_1 })).length - 1 + const orderId = (await instance.getOrders({ userAddress: USER_1, networkId: NETWORK_ID })).length - 1 - await instance.cancelOrders({ userAddress: USER_1, orderIds: [orderId] }) + await instance.cancelOrders({ userAddress: USER_1, orderIds: [orderId], networkId: NETWORK_ID }) - const actual = (await instance.getOrders({ userAddress: USER_1 }))[orderId] + const actual = (await instance.getOrders({ userAddress: USER_1, networkId: NETWORK_ID }))[orderId] expect(actual.validUntil).toBe(BATCH_ID - 1) }) test('cancel non existing order does nothing', async () => { - const expected = await instance.getOrders({ userAddress: USER_1 }) + const expected = await instance.getOrders({ userAddress: USER_1, networkId: NETWORK_ID }) - await instance.cancelOrders({ userAddress: USER_1, orderIds: [expected.length + 1] }) + await instance.cancelOrders({ userAddress: USER_1, orderIds: [expected.length + 1], networkId: NETWORK_ID }) - const actual = await instance.getOrders({ userAddress: USER_1 }) + const actual = await instance.getOrders({ userAddress: USER_1, networkId: NETWORK_ID }) expect(actual).toEqual(expected) }) test('cancel non existing order, user with no orders does nothing', async () => { - const expected = await instance.getOrders({ userAddress: USER_2 }) + const expected = await instance.getOrders({ userAddress: USER_2, networkId: NETWORK_ID }) - await instance.cancelOrders({ userAddress: USER_2, orderIds: [expected.length + 1] }) + await instance.cancelOrders({ userAddress: USER_2, orderIds: [expected.length + 1], networkId: NETWORK_ID }) - const actual = await instance.getOrders({ userAddress: USER_2 }) + const actual = await instance.getOrders({ userAddress: USER_2, networkId: NETWORK_ID }) expect(actual).toEqual(expected) }) }) From e97d15c9c796cb208e62f851104cb05bd800bd20 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 9 Jan 2020 12:48:05 +0100 Subject: [PATCH 09/53] Refactor/orders page (#399) * styled order rows like wallet page * orderRow try 1 * lock * responsive css edit 2 1. make best fit from current HTML strcture * checkbox -> button when responsive * order image row added * better image row css * hide delete row in responsive * open/close cards * hide full order-details * ts error * removed pending prop * minor CSS margin edits --- src/components/OrdersWidget/OrderRow.tsx | 308 +++++++++++++++++++---- src/components/OrdersWidget/index.tsx | 106 ++++---- 2 files changed, 307 insertions(+), 107 deletions(-) diff --git a/src/components/OrdersWidget/OrderRow.tsx b/src/components/OrdersWidget/OrderRow.tsx index ab612ea2d..7529f5221 100644 --- a/src/components/OrdersWidget/OrderRow.tsx +++ b/src/components/OrdersWidget/OrderRow.tsx @@ -2,11 +2,19 @@ import React, { useMemo, useEffect } from 'react' import BigNumber from 'bignumber.js' import styled from 'styled-components' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faExclamationTriangle, faSpinner } from '@fortawesome/free-solid-svg-icons' +import { + faExclamationTriangle, + faSpinner, + faExchangeAlt, + faChevronUp, + faChevronDown, + faTrashAlt, +} from '@fortawesome/free-solid-svg-icons' import { toast } from 'react-toastify' import Highlight from 'components/Highlight' import { EtherscanLink } from 'components/EtherscanLink' +// import TokenImg from 'components/TokenImg' import { getTokenFromExchangeById } from 'services' import useSafeState from 'hooks/useSafeState' @@ -21,35 +29,177 @@ import { isOrderActive, } from 'utils' import { onErrorFactory } from 'utils/onError' -import { MIN_UNLIMITED_SELL_ORDER } from 'const' +import { MIN_UNLIMITED_SELL_ORDER, RESPONSIVE_SIZES } from 'const' import { AuctionElement } from 'api/exchange/ExchangeApi' +import TokenImg from 'components/TokenImg' + +export const OrderRowWrapper = styled.div<{ $open?: boolean }>` + display: grid; + grid-template-columns: 5rem minmax(13.625rem, 1fr) repeat(2, minmax(6.2rem, 0.6fr)) 6.5rem; + align-items: center; + justify-content: center; + background: var(--color-background-pageWrapper); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); + margin: 0.3rem 0; + min-height: 4rem; + text-align: center; + z-index: 1; + transition: all 0.2s ease-in-out; + + > div { + margin: 0 0.85rem; + } -const OrderRowWrapper = styled.div` - .container { - display: grid; - position: relative; + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + grid-template-columns: none; + grid-template-rows: auto; + + align-items: center; + justify-content: stretch; + padding: 0 0.7rem; + + &.selected { + > div { + border-bottom: 0.0625rem solid #ffffff40; + } + } + + > div { + display: flex; + flex-flow: row; + align-items: center; + border-bottom: 0.0625rem solid #00000024; + + margin: 0; + padding: 0.7rem; + + &:first-child { + grid-row-start: 6; + width: 100%; + + > img { + order: 2; + margin-right: -0.5rem; + } + } + + &.order-image-row, + &.cardOpener { + display: initial; + } + + &.order-image-row { + > div { + display: flex; + align-items: center; + justify-content: space-evenly; + max-width: 72%; + margin: auto; + + > div { + display: inherit; + justify-content: inherit; + align-items: center; + > * { + margin: 0 0.3rem; + } + } + } + } + + > .order-details { + display: none; + // grid-template-columns: min-content minmax(2.2rem, max-content); + } + + > .order-details-responsive { + display: flex; + } + + &.checked { + grid-template-columns: 0.5fr 1fr; + + > button { + display: flex; + } + > input { + display: none; + } + } + + &:not(:nth-child(2)):not(:last-child)::before { + content: attr(data-label); + margin-right: auto; + font-weight: bold; + text-transform: uppercase; + font-size: 0.7rem; + } + + ${(props): string | false => + !props.$open && + ` + &:not(:nth-child(2)):not(:nth-child(3)):not(:last-child) { + display: none; + } + `} + } + } + + .cardOpener { + cursor: pointer; + } + + .order-image-row, + .cardOpener { + display: none; + } + + .checked { + > button { + display: none; + justify-content: center; + align-items: center; + + margin: 0 0 0 auto; + > * { + margin: 0 0.5rem; + } + } + } + + .order-details-responsive { + display: none; } .order-details { - grid-template-columns: 6em 3em 5em; - grid-template-rows: repeat(2, 1fr); - justify-self: start; + display: grid; + grid-template-columns: max-content max-content; + grid-gap: 0 1rem; + text-align: left; + justify-content: space-evenly; + + .order-details-subgrid { + display: grid; + grid-template-columns: min-content minmax(5.6rem, max-content); + grid-gap: 0 0.5rem; + justify-content: space-between; + } } .sub-columns { - gap: 0.5em; + display: flex; + flex-flow: row wrap; + justify-content: center; + align-items: center; div:first-child { justify-self: end; } - } - .two-columns { - grid-template-columns: repeat(2, 1fr); - } - - .three-columns { - grid-template-columns: minmax(4em, 60%) minmax(3em, 30%) minmax(1em, 10%); + > *:not(:last-child) { + margin: 0 0.3rem; + } } .pendingCell { @@ -66,13 +216,9 @@ const OrderRowWrapper = styled.div` } ` -const PendingLink: React.FC> = ({ pending, transactionHash }) => { - if (!pending) { - return null - } - +const PendingLink: React.FC> = ({ transactionHash }) => { return ( -
+
{transactionHash && view} />}
@@ -83,13 +229,16 @@ const DeleteOrder: React.FC> = ({ isMarkedForDeletion, toggleMarkedForDeletion, pending, disabled }) => ( -
+
+
) @@ -116,6 +265,26 @@ interface OrderDetailsProps extends Pick { sellToken: TokenDetails } +const OrderImage: React.FC> = ({ buyToken, sellToken }) => { + return ( +
+
+ {/* e.g SELL DAI <-> BUY TUSD */} +
+ {' '} + {displayTokenSymbolOrLink(sellToken)} +
+ {/* Switcher icon */} + +
+ {displayTokenSymbolOrLink(buyToken)}{' '} + +
+
+
+ ) +} + const OrderDetails: React.FC = ({ buyToken, sellToken, order, pending }) => { const price = useMemo( () => @@ -125,25 +294,27 @@ const OrderDetails: React.FC = ({ buyToken, sellToken, order, ), [buyToken, order.priceDenominator, order.priceNumerator, sellToken], ) - return ( -
-
Sell
-
- 1 -
-
- {displayTokenSymbolOrLink(sellToken)} -
- -
- for at least -
-
- {price} +
+
+
Sell
+
+ 1 {displayTokenSymbolOrLink(sellToken)} +
+ +
+ for at least +
+
+ {price}{' '} + {displayTokenSymbolOrLink(buyToken)} +
-
- {displayTokenSymbolOrLink(buyToken)} +
+
+ {price}{' '} + {displayTokenSymbolOrLink(buyToken)} +
) @@ -161,7 +332,7 @@ const UnfilledAmount: React.FC = ({ sellToken, order, pendi const unlimited = order.priceDenominator.gt(MIN_UNLIMITED_SELL_ORDER) return ( -
+
{unlimited ? ( no limit ) : ( @@ -188,10 +359,14 @@ const AccountBalance: React.FC = ({ sellToken, order, isOve const isActive = isOrderActive(order, new Date()) return ( -
+
{accountBalance}
{displayTokenSymbolOrLink(sellToken)} -
{isOverBalance && isActive && }
+ {isOverBalance && isActive && ( +
+ +
+ )}
) } @@ -204,7 +379,15 @@ const Expires: React.FC> = ({ order, pending }) return { isNeverExpires, expiresOn } }, [order.validUntil]) - return
{isNeverExpires ? Never : expiresOn}
+ return ( +
+ {isNeverExpires ? ( + Never + ) : ( + {expiresOn} + )} +
+ ) } async function fetchToken( @@ -227,6 +410,19 @@ async function fetchToken( } } +interface ResponsiveRowSizeTogglerProps { + handleOpen: () => void + openStatus: boolean +} + +const ResponsiveRowSizeToggler: React.FC = ({ handleOpen, openStatus }) => { + return ( +
+ +
+ ) +} + interface Props { order: AuctionElement isOverBalance: boolean @@ -255,6 +451,7 @@ const OrderRow: React.FC = props => { // Fetching buy and sell tokens const [sellToken, setSellToken] = useSafeState(null) const [buyToken, setBuyToken] = useSafeState(null) + const [openCard, setOpenCard] = useSafeState(true) useEffect(() => { fetchToken(order.buyTokenId, order.id, networkId, setBuyToken).catch(onError) @@ -264,18 +461,23 @@ const OrderRow: React.FC = props => { return ( <> {sellToken && buyToken && ( - - - + + {pending ? ( + + ) : ( + + )} + + setOpenCard(!openCard)} openStatus={openCard} /> )} diff --git a/src/components/OrdersWidget/index.tsx b/src/components/OrdersWidget/index.tsx index 7842e9f21..2fb38b58a 100644 --- a/src/components/OrdersWidget/index.tsx +++ b/src/components/OrdersWidget/index.tsx @@ -14,8 +14,9 @@ import { isOrderActive } from 'utils' import Widget from 'components/Layout/Widget' import Highlight from 'components/Highlight' -import OrderRow from './OrderRow' +import OrderRow, { OrderRowWrapper } from './OrderRow' import { useDeleteOrders } from './useDeleteOrders' +import { RESPONSIVE_SIZES } from 'const' const OrdersWrapper = styled(Widget)` > a { @@ -86,9 +87,6 @@ const CreateButtons = styled.div` ` const OrdersForm = styled.div` - margin-top: 2em; - margin-left: 2em; - .infoContainer { display: grid; grid-template-columns: repeat(2, 1fr); @@ -96,6 +94,10 @@ const OrdersForm = styled.div` margin: 1em 0; + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + grid-template-columns: 2fr 1fr; + } + .warning { justify-self: end; } @@ -118,53 +120,17 @@ const OrdersForm = styled.div` } .ordersContainer { - // negative left margin to better position "hidden" elements - margin: 2em 0 2em -3em; - display: grid; - // 6 columns: - // loading indicator | select checkbox | order details | unfilled | available | expires - grid-template-columns: 3em 4em minmax(20em, 1.5fr) repeat(3, 1fr); - grid-row-gap: 1em; - place-items: center; - } - - .headerRow { - // make the contents of this div behave as part of the parent - // grid container - display: contents; - - text-transform: uppercase; - font-weight: bold; - font-size: 0.75em; - - .title { - // create a divider line only bellow titled columns - border-bottom: 0.125rem solid #ededed; - // push the border all the way to the bottom and extend it - place-self: stretch; - - // align that text! - display: flex; - align-items: center; - justify-content: center; - text-align: center; - } - - > * { - // more space for the divider line - padding-bottom: 0.5em; - } - } - - .orderRow { - display: contents; } .checked { // pull checkbox to the left to make divider line be further away justify-self: left; - grid-column-start: 2; + display: grid; + grid-template-columns: 1fr 1fr; + justify-content: center; + align-items: center; + gap: 0 0.6rem; } .deleteContainer { @@ -175,6 +141,10 @@ const OrdersForm = styled.div` .hidden { visibility: hidden; } + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + display: none; + } } .noOrders { @@ -189,6 +159,37 @@ const OrdersForm = styled.div` } ` +const OrdersHeader = styled(OrderRowWrapper)` + background: transparent; + box-shadow: none; + + text-transform: uppercase; + font-weight: bold; + font-size: 0.75em; + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + display: none; + } + + .title { + // create a divider line only bellow titled columns + border-bottom: 0.125rem solid #ededed; + // push the border all the way to the bottom and extend it + place-self: stretch; + + // align that text! + display: flex; + align-items: center; + justify-content: center; + text-align: center; + } + + > * { + // more space for the divider line + padding-bottom: 0.5em; + } +` + interface ShowOrdersButtonProps { type: 'active' | 'expired' isActive: boolean @@ -332,7 +333,8 @@ const OrdersWidget: React.FC = () => { {shownOrdersCount ? (
-
+ {/* GRID HEADER */} +
{ checked={orders.length === markedForDeletion.size} disabled={deleting} /> + All
Order details
-
- Unfilled
amount -
-
- Account
- balance -
+
Unfilled amount
+
Account balance
Expires
-
+ {orders.map(order => ( Date: Thu, 9 Jan 2020 18:18:30 +0100 Subject: [PATCH 10/53] Add more decimals to order details (#397) --- src/components/TradeWidget/OrderDetails.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/TradeWidget/OrderDetails.tsx b/src/components/TradeWidget/OrderDetails.tsx index a988f8291..f8a0f61eb 100644 --- a/src/components/TradeWidget/OrderDetails.tsx +++ b/src/components/TradeWidget/OrderDetails.tsx @@ -4,6 +4,8 @@ import styled from 'styled-components' import { FEE_PERCENTAGE } from 'const' import Highlight from 'components/Highlight' +const DECIMALS_FOR_PRICE = 4 + const Wrapper = styled.dl` margin: 2em 0 0 0; font-size: 0.8em; @@ -18,8 +20,7 @@ const Wrapper = styled.dl` } ` -// TODO: move to utils? -function calculatePrice(sellAmount: number, receiveAmount: number): number { +function _calculatePrice(sellAmount: number, receiveAmount: number): number { return sellAmount > 0 ? receiveAmount / sellAmount : 0 } @@ -38,9 +39,7 @@ const OrderDetails: React.FC = ({ sellAmount, sellTokenName, receiveAmoun return null } - function _calculatePrice(): string { - return calculatePrice(sellAmountNumber, receiveAmountNumber).toFixed(2) - } + const price = _calculatePrice(sellAmountNumber, receiveAmountNumber).toFixed(DECIMALS_FOR_PRICE) return ( @@ -52,7 +51,7 @@ const OrderDetails: React.FC = ({ sellAmount, sellTokenName, receiveAmoun {' '} at a price{' '} - 1 {sellTokenName} = {_calculatePrice()} {receiveTokenName} + 1 {sellTokenName} = {price} {receiveTokenName} {' '} or better.
Your order might be partially filled. From 5755666aff4c7e09e73fcc9a885642cbfd033c65 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 9 Jan 2020 18:40:27 +0100 Subject: [PATCH 11/53] Decode test (#371) * Update to last version of dex-js * Use RC3 of dex-js * Add decode test * Encode order --- test/api/ExchangeApi/decodeOrders.test.ts | 181 ++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 test/api/ExchangeApi/decodeOrders.test.ts diff --git a/test/api/ExchangeApi/decodeOrders.test.ts b/test/api/ExchangeApi/decodeOrders.test.ts new file mode 100644 index 000000000..78861dd6b --- /dev/null +++ b/test/api/ExchangeApi/decodeOrders.test.ts @@ -0,0 +1,181 @@ +import { decodeAuctionElements } from 'api/exchange/utils/decodeAuctionElements' +import BN from 'bn.js' + +function _transformBnToString(order: { + sellTokenBalance: BN + priceNumerator: BN + priceDenominator: BN + remainingAmount: BN +}): { + sellTokenBalance: string + priceNumerator: string + priceDenominator: string + remainingAmount: string +} { + return { + ...order, + sellTokenBalance: order.sellTokenBalance.toString(), + priceNumerator: order.priceNumerator.toString(), + priceDenominator: order.priceDenominator.toString(), + remainingAmount: order.remainingAmount.toString(), + } +} + +function _encodeOrder(order: { + user: number + sellTokenBalance: number + buyTokenId: number + sellTokenId: number + validFrom: number + validUntil: number + priceNumerator: number + priceDenominator: number + remainingAmount: number +}): string { + const { + user, + sellTokenBalance, + buyTokenId, + sellTokenId, + validFrom, + validUntil, + priceNumerator, + priceDenominator, + remainingAmount, + } = order + return ( + user.toString(16).padStart(40, '0') + + sellTokenBalance.toString(16).padStart(64, '0') + + buyTokenId.toString(16).padStart(4, '0') + + sellTokenId.toString(16).padStart(4, '0') + + validFrom.toString(16).padStart(8, '0') + + validUntil.toString(16).padStart(8, '0') + + priceNumerator.toString(16).padStart(32, '0') + + priceDenominator.toString(16).padStart(32, '0') + + remainingAmount.toString(16).padStart(32, '0') + ) +} + +describe('Decode orders', () => { + test('Test no orders', async () => { + // GIVEN: An empty encoded order + const encodedOrders = '0x' + + // WHEN: decoding + const orders = decodeAuctionElements(encodedOrders) + + // THEN: The decoded orders are an empty array + expect(orders).toEqual([]) + }) + + test('Test single order', async () => { + // GIVEN: An encode order + const user = 1 + const sellTokenBalance = 0 + const buyTokenId = 4 + const sellTokenId = 7 + const validFrom = 5259300 + const validUntil = 5259306 + const priceNumerator = 1500000 + const priceDenominator = 1000000000000000000 + const remainingAmount = 1000000000000000000 + const encodedOrder = _encodeOrder({ + user, + sellTokenBalance, + buyTokenId, + sellTokenId, + validFrom, + validUntil, + priceNumerator, + priceDenominator, + remainingAmount, + }) + const encodedOrders = '0x' + encodedOrder + + // WHEN: decoding + const orders = decodeAuctionElements(encodedOrders).map(_transformBnToString) + + // THEN: The decoded oder matches the order + expect(orders).toEqual([ + { + user: '0x' + user.toString(16).padStart(40, '0'), + sellTokenBalance: '0', + id: '0', + buyTokenId, + sellTokenId, + validFrom, + validUntil, + priceNumerator: priceNumerator.toString(), + priceDenominator: priceDenominator.toString(), + remainingAmount: remainingAmount.toString(), + }, + ]) + }) + + test('Test empty orders', async () => { + // GIVEN: An 2 empty encoded orders + const user = 0 + const sellTokenBalance = 0 + const buyTokenId = 0 + const sellTokenId = 0 + const validFrom = 0 + const validUntil = 0 + const priceNumerator = 0 + const priceDenominator = 0 + const remainingAmount = 0 + const encodedOrder1 = _encodeOrder({ + user, + sellTokenBalance, + buyTokenId, + sellTokenId, + validFrom, + validUntil, + priceNumerator, + priceDenominator, + remainingAmount, + }) + const encodedOrder2 = _encodeOrder({ + user, + sellTokenBalance, + buyTokenId, + sellTokenId, + validFrom, + validUntil, + priceNumerator, + priceDenominator, + remainingAmount, + }) + const encodedOrders = '0x' + encodedOrder1 + encodedOrder2 + + // WHEN: when decoding + const orders = decodeAuctionElements(encodedOrders).map(_transformBnToString) + + // THEN: The decoded orders matches the empty orders + expect(orders).toEqual([ + { + user: '0x' + user.toString(16).padStart(40, '0'), + sellTokenBalance: '0', + id: '0', + buyTokenId, + sellTokenId, + validFrom, + validUntil, + priceNumerator: '0', + priceDenominator: '0', + remainingAmount: '0', + }, + { + user: '0x' + user.toString(16).padStart(40, '0'), + sellTokenBalance: '0', + id: '1', + buyTokenId, + sellTokenId, + validFrom, + validUntil, + priceNumerator: '0', + priceDenominator: '0', + remainingAmount: '0', + }, + ]) + }) +}) From a45fc4d9a2180e14696ec7974760b6ec0e8e18e6 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 9 Jan 2020 18:43:52 +0100 Subject: [PATCH 12/53] Calculate price (#398) * Add more decimals to order details * Price formatting * Improve price formatting * Reuse the format price function * Do not return an infinity price --- src/components/OrdersWidget/OrderRow.tsx | 32 +++++++++++---------- src/components/TradeWidget/OrderDetails.tsx | 24 +++++++++------- src/utils/format.ts | 19 ++++++++++++ yarn.lock | 22 -------------- 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/src/components/OrdersWidget/OrderRow.tsx b/src/components/OrdersWidget/OrderRow.tsx index 7529f5221..448599a36 100644 --- a/src/components/OrdersWidget/OrderRow.tsx +++ b/src/components/OrdersWidget/OrderRow.tsx @@ -27,6 +27,7 @@ import { isBatchIdFarInTheFuture, formatDateFromBatchId, isOrderActive, + formatPrice, } from 'utils' import { onErrorFactory } from 'utils/onError' import { MIN_UNLIMITED_SELL_ORDER, RESPONSIVE_SIZES } from 'const' @@ -250,14 +251,16 @@ function displayTokenSymbolOrLink(token: TokenDetails): React.ReactNode | string return displayName } -function calculatePrice(_numerator?: string | null, _denominator?: string | null): string { - if (!_numerator || !_denominator) { - return 'N/A' +function calculatePrice(numeratorString?: string | null, denominatorString?: string | null): string { + let price + if (numeratorString && denominatorString) { + const numerator = new BigNumber(numeratorString) + const denominator = new BigNumber(denominatorString) + + price = formatPrice(numerator, denominator) } - const numerator = new BigNumber(_numerator) - const denominator = new BigNumber(_denominator) - const price = numerator.dividedBy(denominator) - return price.toFixed(2) + + return price || 'N/A' } interface OrderDetailsProps extends Pick { @@ -286,14 +289,13 @@ const OrderImage: React.FC> = } const OrderDetails: React.FC = ({ buyToken, sellToken, order, pending }) => { - const price = useMemo( - () => - calculatePrice( - formatAmountFull(order.priceNumerator, buyToken.decimals, false), - formatAmountFull(order.priceDenominator, sellToken.decimals, false), - ), - [buyToken, order.priceDenominator, order.priceNumerator, sellToken], - ) + const price = useMemo(() => { + const numeratorString = formatAmountFull(order.priceNumerator, buyToken.decimals, false) + const denominatorString = formatAmountFull(order.priceDenominator, sellToken.decimals, false) + + return calculatePrice(numeratorString, denominatorString) + }, [buyToken, order.priceDenominator, order.priceNumerator, sellToken]) + return (
diff --git a/src/components/TradeWidget/OrderDetails.tsx b/src/components/TradeWidget/OrderDetails.tsx index f8a0f61eb..18f83f5ac 100644 --- a/src/components/TradeWidget/OrderDetails.tsx +++ b/src/components/TradeWidget/OrderDetails.tsx @@ -1,8 +1,10 @@ import React from 'react' import styled from 'styled-components' +import BigNumber from 'bignumber.js' import { FEE_PERCENTAGE } from 'const' import Highlight from 'components/Highlight' +import { formatPrice } from 'utils' const DECIMALS_FOR_PRICE = 4 @@ -20,10 +22,6 @@ const Wrapper = styled.dl` } ` -function _calculatePrice(sellAmount: number, receiveAmount: number): number { - return sellAmount > 0 ? receiveAmount / sellAmount : 0 -} - interface Props { sellAmount: string sellTokenName: string @@ -31,23 +29,27 @@ interface Props { receiveTokenName: string } -const OrderDetails: React.FC = ({ sellAmount, sellTokenName, receiveAmount, receiveTokenName }) => { - const sellAmountNumber = Number(sellAmount) - const receiveAmountNumber = Number(receiveAmount) +const OrderDetails: React.FC = ({ + sellAmount: sellAmountString, + sellTokenName, + receiveAmount: receiveAmountString, + receiveTokenName, +}) => { + const sellAmount = new BigNumber(sellAmountString) + const receiveAmount = new BigNumber(receiveAmountString) - if (!(sellAmountNumber > 0 && receiveAmountNumber > 0)) { + const price = formatPrice(sellAmount, receiveAmount) + if (!price) { return null } - const price = _calculatePrice(sellAmountNumber, receiveAmountNumber).toFixed(DECIMALS_FOR_PRICE) - return (
Order details:
Sell up to{' '} - {sellAmount} {sellTokenName} + {sellAmountString} {sellTokenName} {' '} at a price{' '} diff --git a/src/utils/format.ts b/src/utils/format.ts index 8764a0066..30aa96763 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -1,6 +1,7 @@ import BN from 'bn.js' import { TEN, DEFAULT_PRECISION } from 'const' import { TokenDetails } from 'types' +import BigNumber from 'bignumber.js' const DEFAULT_DECIMALS = 4 const ELLIPSIS = '...' @@ -140,3 +141,21 @@ export function safeFilledToken(token: T): T { symbol: token.symbol || token.name || abbreviateString(token.address, 6, 4), } } + +export function calculatePriceBigNumber(numerator?: BigNumber, denominator?: BigNumber): BigNumber | null { + if (!numerator || !denominator || denominator.isZero()) { + return null + } + + return numerator.dividedBy(denominator) +} + +export function formatPrice( + numerator?: BigNumber, + denominator?: BigNumber, + decimals = DEFAULT_DECIMALS, +): string | null { + const price = calculatePriceBigNumber(numerator, denominator) + + return price ? price.toFixed(decimals) : null +} diff --git a/yarn.lock b/yarn.lock index cbcf2a0bb..f6ea63367 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1462,15 +1462,6 @@ regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.13.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.13.0.tgz#958614faa6f77599ee2b241740e0ea402482533d" - integrity sha512-+Hss3clwa6aNiC8ZjA45wEm4FutDV5HsVXPl/rDug1THq6gEtOYRGLqS3JlTk7mSnL5TbJz0LpEbzbPnKvY6sw== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.13.0" - eslint-scope "^5.0.0" - "@typescript-eslint/experimental-utils@2.14.0", "@typescript-eslint/experimental-utils@^2.5.0": version "2.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.14.0.tgz#e9179fa3c44e00b3106b85d7b69342901fb43e3b" @@ -1490,19 +1481,6 @@ "@typescript-eslint/typescript-estree" "2.14.0" eslint-visitor-keys "^1.1.0" -"@typescript-eslint/typescript-estree@2.13.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.13.0.tgz#a2e746867da772c857c13853219fced10d2566bc" - integrity sha512-t21Mg5cc8T3ADEUGwDisHLIubgXKjuNRbkpzDMLb7/JMmgCe/gHM9FaaujokLey+gwTuLF5ndSQ7/EfQqrQx4g== - dependencies: - debug "^4.1.1" - eslint-visitor-keys "^1.1.0" - glob "^7.1.6" - is-glob "^4.0.1" - lodash.unescape "4.0.1" - semver "^6.3.0" - tsutils "^3.17.1" - "@typescript-eslint/typescript-estree@2.14.0": version "2.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.14.0.tgz#c67698acdc14547f095eeefe908958d93e1a648d" From ca699815773a6cbeca7a6111c1e51a88c435a192 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 10 Jan 2020 20:13:40 +0300 Subject: [PATCH 13/53] Fix initial web3 check (#408) * check injected provider is valid * fix build --- src/components/TradeWidget/OrderDetails.tsx | 2 -- src/utils/autoconnect.ts | 8 +------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/TradeWidget/OrderDetails.tsx b/src/components/TradeWidget/OrderDetails.tsx index 18f83f5ac..98302b7c0 100644 --- a/src/components/TradeWidget/OrderDetails.tsx +++ b/src/components/TradeWidget/OrderDetails.tsx @@ -6,8 +6,6 @@ import { FEE_PERCENTAGE } from 'const' import Highlight from 'components/Highlight' import { formatPrice } from 'utils' -const DECIMALS_FOR_PRICE = 4 - const Wrapper = styled.dl` margin: 2em 0 0 0; font-size: 0.8em; diff --git a/src/utils/autoconnect.ts b/src/utils/autoconnect.ts index 18371a050..17b8e4b2d 100644 --- a/src/utils/autoconnect.ts +++ b/src/utils/autoconnect.ts @@ -47,7 +47,6 @@ const getWCIfConnected = async (): Promise => { export const getLastProvider = (): Promise => { const lastProviderName = localStorage.getItem(STORAGE_KEY_LAST_PROVIDER) - console.log('lastProvider', lastProviderName) try { // last provider was WalletConnect @@ -56,14 +55,9 @@ export const getLastProvider = (): Promise => { if (lastProviderName === 'WalletConnect') return getWCIfConnected() const injectedProviderName = Web3Connect.getInjectedProviderName() - console.log('injectedProvider', injectedProviderName) // last provider is the current injected provider // and it's still injected - if (injectedProviderName === lastProviderName) { - // if (injectedProvider === 'MetaMask') { - // const provider = window.ethereum || window.web3?.currentProvider - // provider._metamask?.isUnlocked() - // } + if (injectedProviderName && injectedProviderName === lastProviderName) { return Web3Connect.ConnectToInjected() } } catch (error) { From 80c0026731cd102e1b5051920227309112407c1b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 13 Jan 2020 13:40:09 +0300 Subject: [PATCH 14/53] =?UTF-8?q?=F0=9F=8C=98=20dark=20mode=20=F0=9F=8C=92?= =?UTF-8?q?=20(#372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add ThemeToggler component * dumb dark mode switch * Dark colors / Footer behaviour (#373) * use more global colors * spearate colors for dark/light modes * fix color for EtherImage * better contrasting * better contrasting pt.2 + header underline * theme toggler tweaks 1. removed absolute positioning 2. added Theme label 3. styling for fit * movement of themetoggler to (now) fixed footer * global input text colour fix * tokenSelector dark mode colours * dark mode scrollbars because why not * web3connect modal css * footer - open/close * ThemeToggler 1. smaller font (75%) 2. auto checks users clock between 8 and 17 for auto light or dark * optional fixed footer prop * reverted auto and fixed footer * input colours * input border colours: warn/error Co-authored-by: David Co-authored-by: David --- src/components/DepositWidget/Styled.tsx | 12 +- src/components/Layout/Footer.tsx | 49 ++++---- .../Layout/Header/Navigation.styled.ts | 6 + src/components/Layout/index.tsx | 2 +- src/components/LegalBanner.tsx | 4 +- src/components/ThemeToggler.tsx | 75 ++++++++++++ src/components/TokenSelector.tsx | 30 ++++- src/components/TradeWidget/TokenRow.tsx | 6 +- .../UserWallet/UserWallet.styled.ts | 14 ++- src/components/UserWallet/WalletComponent.tsx | 2 +- src/styles/global.ts | 35 +++++- src/styles/variables.ts | 108 ++++++++++++++---- 12 files changed, 281 insertions(+), 62 deletions(-) create mode 100644 src/components/ThemeToggler.tsx diff --git a/src/components/DepositWidget/Styled.tsx b/src/components/DepositWidget/Styled.tsx index e5152e289..8b64e3dd8 100644 --- a/src/components/DepositWidget/Styled.tsx +++ b/src/components/DepositWidget/Styled.tsx @@ -70,8 +70,14 @@ export const InnerWrapper = styled.div` text-align: right; } > input { + background-color: #fff; + color: #000; margin: 0; width: 100%; + + &:disabled { + background-color: #6f6f6f; + } } } @@ -131,7 +137,7 @@ export const DepositWidgetWrapper = styled.section` grid-template-columns: var(--grid-row-size-walletPage); > div { - color: #000000; + color: var(--color-text-deposit-primary); line-height: 1.5; font-size: 0.8em; text-transform: uppercase; @@ -171,6 +177,7 @@ export const RowTokenDiv = styled.div` justify-content: center; background: var(--color-background-pageWrapper); + border: 0.125rem solid transparent; border-radius: var(--border-radius); box-shadow: var(--box-shadow); margin: 0.3rem 0; @@ -180,6 +187,7 @@ export const RowTokenDiv = styled.div` &:hover { background: var(--color-background-selected); + border: 0.125rem solid var(--color-border); } > div { @@ -220,7 +228,7 @@ export const RowTokenDiv = styled.div` &.selected { background-color: var(--color-button-disabled); - color: var(--color-background-pageWrapper); + color: #fff; } &.loading { diff --git a/src/components/Layout/Footer.tsx b/src/components/Layout/Footer.tsx index 960f065b0..4a57dab01 100644 --- a/src/components/Layout/Footer.tsx +++ b/src/components/Layout/Footer.tsx @@ -1,37 +1,38 @@ import React from 'react' import styled from 'styled-components' import { Link } from 'react-router-dom' +import ThemeToggler from 'components/ThemeToggler' const Wrapper = styled.footer` - align-self: flex-end; + position: relative; + display: flex; flex-flow: row wrap; - justify-content: center; align-items: center; - padding 1.3em; + justify-content: center; + padding: 1.3rem; text-align: center; - position: relative; + + background: var(--color-background); color: var(--color-text-secondary); font-size: 0.85rem; - ul { + > div { + margin: 0.2rem auto; + flex: 1 1 15rem; + } + + > .footerLinks { display: flex; - list-style-type: none; + flex-flow: row wrap; justify-content: center; - white-space: nowrap; - } + align-items: center; - li { - margin: 0 1rem; - a { - color: var(--color-text-secondary); + > div { + margin: 0 0.5rem; } } - ul, .version { - margin-left: auto; - } - .version { font-size: 0.85em; a { @@ -42,14 +43,18 @@ const Wrapper = styled.footer` const Footer: React.FC = () => ( -
    -
  • + {/* DARK/LIGHT MODE TOGGLER */} + + {/* LINKS */} +
    +
    About dFusion -
  • -
  • +
+
Source code - - +
+
+ {/* VERSION */}
Web{' '} diff --git a/src/components/Layout/Header/Navigation.styled.ts b/src/components/Layout/Header/Navigation.styled.ts index 9553fa12d..169ce5937 100644 --- a/src/components/Layout/Header/Navigation.styled.ts +++ b/src/components/Layout/Header/Navigation.styled.ts @@ -1,4 +1,5 @@ import styled from 'styled-components' +import { RESPONSIVE_SIZES } from 'const' export const NavLinksWrapper = styled.div<{ $open?: boolean; $responsive: boolean }>` display: flex; @@ -10,6 +11,7 @@ export const NavLinksWrapper = styled.div<{ $open?: boolean; $responsive: boolea white-space: nowrap; a { + border: 0.125rem solid transparent; color: var(--color-text-secondary); font-weight: bolder; padding: 0.35em; @@ -23,6 +25,10 @@ export const NavLinksWrapper = styled.div<{ $open?: boolean; $responsive: boolea &.active { color: var(--color-text-primary); + + @media only screen and (min-width: ${RESPONSIVE_SIZES.TABLET}em) { + border-bottom: 0.125rem solid var(--color-text-primary); + } } } diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 414565608..5eb3d2e53 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -12,7 +12,7 @@ const Wrapper = styled.div` width: 100%; display: grid; - grid-template-rows: 3.125rem 0.2fr auto; + grid-template-rows: 3.125rem 0.2fr auto min-content; main { flex: 1; diff --git a/src/components/LegalBanner.tsx b/src/components/LegalBanner.tsx index 69d725045..6db5dec32 100644 --- a/src/components/LegalBanner.tsx +++ b/src/components/LegalBanner.tsx @@ -10,8 +10,8 @@ const Wrapper = styled.div` align-items: center; justify-content: center; - background-color: #272727; - color: wheat; + background-color: var(--color-background-banner); + color: var(--color-text-banner); text-align: center; font-size: 0.65rem; diff --git a/src/components/ThemeToggler.tsx b/src/components/ThemeToggler.tsx new file mode 100644 index 000000000..60166465d --- /dev/null +++ b/src/components/ThemeToggler.tsx @@ -0,0 +1,75 @@ +import React, { useEffect } from 'react' +import styled from 'styled-components' +import useSafeState from 'hooks/useSafeState' + +const FUSE_APP_THEME = 'FUSE_APP_THEME' + +const TogglerWrapper = styled.div` + font-size: 70%; +` + +const ToggleLabel = styled.label<{ selected: boolean }>` + color: ${(props): string => (props.selected ? 'var(--color-text-primary)' : 'var(--color-text-secondary)')}; + cursor: pointer; + font-weight: ${(props): string => (props.selected ? 'bolder' : 'normal')}; + + padding: 0.125rem 0.5rem; + text-transform: uppercase; + + transition: all 0.2s ease-in-out; + + &:nth-child(2) { + border-left: 0.0625rem solid var(--color-text-primary); + border-right: 0.0625rem solid var(--color-text-primary); + } + + &:hover { + color: var(--color-text-primary); + } + + > input { + display: none; + } +` + +const toggleValues = ['auto', 'light', 'dark'] +const toggleValue2class = { + light: 'light-theme', + dark: 'dark-theme', +} +const themeClasses = Object.values(toggleValue2class) + +const ThemeToggler: React.FC = () => { + const startTheme = localStorage.getItem(FUSE_APP_THEME) || 'auto' + const [active, setActive] = useSafeState(startTheme) + + useEffect(() => { + const className = toggleValue2class[active] + + document.body.classList.remove(...themeClasses) + if (className) { + document.body.classList.add(className) + localStorage.setItem(FUSE_APP_THEME, active) + } + }, [active]) + + return ( + + Theme:{' '} + {toggleValues.map(value => ( + + setActive(value)} + /> + {value} + + ))} + + ) +} + +export default ThemeToggler diff --git a/src/components/TokenSelector.tsx b/src/components/TokenSelector.tsx index 2a5959f29..24f200421 100644 --- a/src/components/TokenSelector.tsx +++ b/src/components/TokenSelector.tsx @@ -65,9 +65,33 @@ function formatOptionLabel( } const customSelectStyles = { - control: (provided: CSSProperties): CSSProperties => ({ ...provided, border: 'none' }), - menu: (provided: CSSProperties): CSSProperties => ({ ...provided, minWidth: '300px' }), - valueContainer: (provided: CSSProperties): CSSProperties => ({ ...provided, minWidth: '4.5em' }), + control: (provided: CSSProperties): CSSProperties => ({ + ...provided, + border: 'none', + background: 'var(--color-background-pageWrapper)', + }), + menu: (provided: CSSProperties): CSSProperties => ({ + ...provided, + minWidth: '300px', + background: 'var(--color-background-pageWrapper)', + color: 'var(--color-text-primary)', + }), + option: (provided: CSSProperties): CSSProperties & { '&:hover': CSSProperties } => ({ + ...provided, + background: 'none', + cursor: 'pointer', + '&:hover': { + background: 'var(--color-background)', + }, + }), + valueContainer: (provided: CSSProperties): CSSProperties => ({ + ...provided, + minWidth: '4.5em', + }), + singleValue: (provided: CSSProperties): CSSProperties => ({ + ...provided, + color: 'var(--color-text-primary)', + }), } interface Props { diff --git a/src/components/TradeWidget/TokenRow.tsx b/src/components/TradeWidget/TokenRow.tsx index 572d465fb..0b99db417 100644 --- a/src/components/TradeWidget/TokenRow.tsx +++ b/src/components/TradeWidget/TokenRow.tsx @@ -37,11 +37,13 @@ const InputBox = styled.div` width: 100%; &.error { - box-shadow: 0 0 0.1875rem #cc0000; + // box-shadow: 0 0 0.1875rem #cc0000; + border-color: #ff0000a3; } &.warning { - box-shadow: 0 0 0.1875rem #ff7500; + // box-shadow: 0 0 0.1875rem #ff7500; + border-color: orange; } &:disabled { diff --git a/src/components/UserWallet/UserWallet.styled.ts b/src/components/UserWallet/UserWallet.styled.ts index fe99e54e0..a66b35a82 100644 --- a/src/components/UserWallet/UserWallet.styled.ts +++ b/src/components/UserWallet/UserWallet.styled.ts @@ -17,7 +17,7 @@ export const UserWalletWrapper = styled.div<{ $walletOpen: boolean }>` ` export const UserWalletItem = styled.div<{ $padding?: string; $wordWrap?: string }>` - color: #000; + color: var(--color-text-primary); display: flex; flex-flow: row nowrap; align-items: center; @@ -45,6 +45,16 @@ export const UserWalletToggler = styled(UserWalletItem)` ` export const EtherImage = styled.img` max-width: 5%; + + @media (prefers-color-scheme: dark) { + body:not(.light-theme) & { + filter: invert(100%); + } + } + + body.dark-theme & { + filter: invert(100%); + } ` export const CopyDiv = styled.div` @@ -67,7 +77,7 @@ export const UserWalletSlideWrapper = styled.div` ` export const NetworkTitle = styled.div<{ $color?: string; $fontSize?: string }>` - color: ${({ color = '#000' }): string => color}; + color: ${({ color = 'var(--color-text-primary)' }): string => color}; font-size: ${({ $fontSize = '1rem' }): string => $fontSize}; font-weight: bolder; ` diff --git a/src/components/UserWallet/WalletComponent.tsx b/src/components/UserWallet/WalletComponent.tsx index 334f26fe3..0375d5bce 100644 --- a/src/components/UserWallet/WalletComponent.tsx +++ b/src/components/UserWallet/WalletComponent.tsx @@ -159,7 +159,7 @@ const UserWallet: React.FC = (props: UserWalletProps) => { ) : ( // Address and copy button <> - {userAddress} + {userAddress} div > div > div:nth-child(2) { + background: transparent; + grid-gap: 0.4rem; + div { + background: var(--color-background); + border-radius: var(--border-radius); + color: var(--color-text-primary); + } + } + html, body { min-height: 100vh; min-width: 320px; @@ -35,6 +48,23 @@ const GlobalStyles = createGlobalStyle` color: var(--color-text-secondary); } } + + body::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + background-color: var(--color-background); + } + + body::-webkit-scrollbar { + width: 12px; + background-color: var(--color-background); + } + + body::-webkit-scrollbar-thumb { + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: var(--color-background-pageWrapper); + } + h1, h2, h3 { margin: 0; margin: 0.5em 0; @@ -123,8 +153,9 @@ const GlobalStyles = createGlobalStyle` input { background-color: var(--color-background-pageWrapper); - border: none; + border: 0.11rem solid transparent; border-radius: var(--border-radius); + color: var(--color-text-primary); outline: none; font-family: inherit; @@ -138,7 +169,7 @@ const GlobalStyles = createGlobalStyle` transition: all 0.2s ease-in-out; &:focus { - border: 0.11rem solid var(--color-text-primary); + border-color: var(--color-text-primary); } &:disabled { background-color: #e0e0e0; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 1e53a6575..22738bad9 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -1,33 +1,81 @@ import { css } from 'styled-components' +const LightColors = ` + // Background + --color-background-lighter: #f7f7f7; + --color-background: #eee; + --color-background-pageWrapper: #fff; + --color-background-actionCards: #bbfdbb; + --color-background-highlighted: #fcfde0; + --color-background-selected: ##d9d9d9; + --color-background-selected-darker: #b6b6b6; + --color-background-selected-dark: #bfbfbf; + + // Borders + --color-border: transparent; + + // Text + // --color-text-primary: #3d414c; + --color-text-primary: #000; + --color-text-secondary: #8c8c8c; + + // Buttons + --color-button-primary: #000; + --color-button-success: #5ca95c; + --color-button-disabled: #666; + --color-button-danger: #e55353; + + // Components + --color-background-banner: #272727; + --color-text-banner: wheat; + --color-text-wallet: #000; + --color-text-deposit-header: #000000; + + // Shadow + --shadow-color: #00000047; + +` + +const DarkColors = ` + // Background + --color-background-lighter: #f7f7f7; + --color-background: #2e2e2e; + --color-background-pageWrapper: #181a1b; + --color-background-actionCards: #bbfdbb; + --color-background-highlighted: #3f4104; + --color-background-selected: ##d9d9d9; + --color-background-selected-darker: #b6b6b6; + --color-background-selected-dark: #2a2d2f; + + // Borders + --color-border: #262626; + + // Text + --color-text-primary: #e8e6e3; + --color-text-secondar: #545454; + + // Buttons + --color-button-primary: #e8e6e3; + --color-button-success: #91c591; + --color-button-disabled: #3d4043; + --color-button-danger: #9c1818; + + // Components + --color-background-banner: #252729; + --color-text-banner: wheat; + + // Shadow + --shadow-color: #00000047; + +` + const variables = css` - :root { + :root, + body.light-theme { // ------------------------------ // COLOURS //----------------------------- - // Background - --color-background-lighter: #f7f7f7; - --color-background: #eee; - --color-background-pageWrapper: #fff; - --color-background-actionCards: #bbfdbb; - --color-background-highlighted: #fcfde0; - --color-background-selected: ##d9d9d9; - --color-background-selected-darker: #b6b6b6; - --color-background-selected-dark: #bfbfbf; - - // Text - // --color-text-primary: #3d414c; - --color-text-primary: #000; - --color-text-secondary: #8c8c8c; - - // Buttons - --color-button-primary: #000; - --color-button-success: #5ca95c; - --color-button-disabled: #666; - --color-button-danger: #e55353; - - // Cards - --color-cards-primary: #f8f8ff; + ${LightColors} // ------------------------------ // BORDERS @@ -37,13 +85,23 @@ const variables = css` // ------------------------------ // BOX-SHADOW // ------------------------------ - --box-shadow: 0.0625rem 0.125rem 0.125rem -0.0625rem #00000047; + --box-shadow: 0.0625rem 0.125rem 0.125rem -0.0625rem var(--shadow-color); //------------------------------- // GRID // ------------------------------ --grid-row-size-walletPage: minmax(10.9375rem, 1.1fr) repeat(3, 1fr) minmax(10.3125rem, 1fr); } + + @media (prefers-color-scheme: dark) { + :root { + ${DarkColors} + } + } + + body.dark-theme { + ${DarkColors} + } ` export default variables From 92ffe912cdc8cd0f3a9925a80e049193c8325ee6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2020 11:40:27 +0100 Subject: [PATCH 15/53] Bump @types/bn.js from 4.11.5 to 4.11.6 (#411) Bumps [@types/bn.js](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bn.js) from 4.11.5 to 4.11.6. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bn.js) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index f6ea63367..90d37de44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1222,9 +1222,9 @@ bignumber.js "*" "@types/bn.js@^4.11.3", "@types/bn.js@^4.11.4", "@types/bn.js@^4.11.5": - version "4.11.5" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.5.tgz#40e36197433f78f807524ec623afcf0169ac81dc" - integrity sha512-AEAZcIZga0JgVMHNtl1CprA/hXX7/wPt79AgR4XqaDt7jyj3QWYw6LPoOiznPtugDmlubUnAahMs2PFxGcQrng== + version "4.11.6" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c" + integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg== dependencies: "@types/node" "*" From d1d778ec1fe0e9613f4828c5cdea72c267fe781d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2020 11:40:40 +0100 Subject: [PATCH 16/53] Bump @babel/plugin-transform-runtime from 7.7.6 to 7.8.0 (#412) Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel) from 7.7.6 to 7.8.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/master/CHANGELOG.md) - [Commits](https://github.com/babel/babel/compare/v7.7.6...v7.8.0) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index 90d37de44..593d7310a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -138,12 +138,12 @@ dependencies: "@babel/types" "^7.7.4" -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.7.4": - version "7.7.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.7.4.tgz#e5a92529f8888bf319a6376abfbd1cebc491ad91" - integrity sha512-dGcrX6K9l8258WFjyDLJwuVKxR4XZfU0/vTUgOQYWEnRD8mgr+p4d6fCUMq/ys0h4CCt/S5JhbvtyErjWouAUQ== +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.7.4", "@babel/helper-module-imports@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.0.tgz#076edda55d8cd39c099981b785ce53f4303b967e" + integrity sha512-ylY9J6ZxEcjmJaJ4P6aVs/fZdrZVctCGnxxfYXwCnSMapqd544zT8lWK2qI/vBPjE5gS0o2jILnH+AkpsPauEQ== dependencies: - "@babel/types" "^7.7.4" + "@babel/types" "^7.8.0" "@babel/helper-module-transforms@^7.7.4", "@babel/helper-module-transforms@^7.7.5": version "7.7.5" @@ -164,10 +164,10 @@ dependencies: "@babel/types" "^7.7.4" -"@babel/helper-plugin-utils@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" - integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.0.tgz#59ec882d43c21c544ccb51decaecb306b34a8231" + integrity sha512-+hAlRGdf8fHQAyNnDBqTHQhwdLURLdrCROoWaEQYiQhk2sV9Rhs+GoFZZfMJExTq9HG8o2NX3uN2G90bFtmFdA== "@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4": version "7.5.5" @@ -583,12 +583,12 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-runtime@^7.5.5", "@babel/plugin-transform-runtime@^7.6.0": - version "7.7.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.6.tgz#4f2b548c88922fb98ec1c242afd4733ee3e12f61" - integrity sha512-tajQY+YmXR7JjTwRvwL4HePqoL3DYxpYXIHKVvrOIvJmeHe2y1w4tz5qz9ObUDC9m76rCzIMPyn4eERuwA4a4A== + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.8.0.tgz#394080e30a66cc17f63940543f60343a3b2c1d52" + integrity sha512-7AvNIKcpzx/0L9FYIJ3Rs231P3pwWrZ4XW3xt56Cejy1q0Ix+PbgCE17r12jUY8ygl5p4QhfANC0ZqLd8bz8aA== dependencies: - "@babel/helper-module-imports" "^7.7.4" - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-module-imports" "^7.8.0" + "@babel/helper-plugin-utils" "^7.8.0" resolve "^1.8.1" semver "^5.5.1" @@ -756,6 +756,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.0.tgz#1a2039a028057a2c888b668d94c98e61ea906e7f" + integrity sha512-1RF84ehyx9HH09dMMwGWl3UTWlVoCPtqqJPjGuC4JzMe1ZIVDJ2DT8mv3cPv/A7veLD6sgR7vi95lJqm+ZayIg== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" From 8d6c5c5beee304e2e5bd4439ec12d8121ac14e98 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2020 17:16:28 +0100 Subject: [PATCH 17/53] Bump @babel/plugin-proposal-class-properties from 7.7.4 to 7.8.0 (#413) Bumps [@babel/plugin-proposal-class-properties](https://github.com/babel/babel) from 7.7.4 to 7.8.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/master/CHANGELOG.md) - [Commits](https://github.com/babel/babel/compare/v7.7.4...v7.8.0) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 132 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 15 deletions(-) diff --git a/yarn.lock b/yarn.lock index 593d7310a..b5566ee18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,6 +9,13 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/code-frame@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.0.tgz#8c98d4ac29d6f80f28127b1bc50970a72086c5ac" + integrity sha512-AN2IR/wCUYsM+PdErq6Bp3RFTXl8W0p9Nmymm7zkpsCmh+r/YYcckaCGpU8Q/mEKmST19kkGRaG42A/jxOWwBA== + dependencies: + "@babel/highlight" "^7.8.0" + "@babel/core@^7.1.0", "@babel/core@^7.6.0": version "7.7.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.7.tgz#ee155d2e12300bcc0cff6a8ad46f2af5063803e9" @@ -39,6 +46,16 @@ lodash "^4.17.13" source-map "^0.5.0" +"@babel/generator@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.0.tgz#40a1244677be58ffdc5cd01e22634cd1d5b29edf" + integrity sha512-2Lp2e02CV2C7j/H4n4D9YvsvdhPVVg9GDIamr6Tu4tU35mL3mzOrzl1lZ8ZJtysfZXh+y+AGORc2rPS7yHxBUg== + dependencies: + "@babel/types" "^7.8.0" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.7.4.tgz#bb3faf1e74b74bd547e867e48f551fa6b098b6ce" @@ -71,17 +88,17 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" -"@babel/helper-create-class-features-plugin@^7.7.4": - version "7.7.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.7.4.tgz#fce60939fd50618610942320a8d951b3b639da2d" - integrity sha512-l+OnKACG4uiDHQ/aJT8dwpR+LhCJALxL0mJ6nzjB25e5IPwqV1VOsY7ah6UB1DG+VOXAIMtuC54rFJGiHkxjgA== +"@babel/helper-create-class-features-plugin@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.0.tgz#b3ddf557ed4656e0d296c3b0f3fcd381ea8de72c" + integrity sha512-ctCvqYBTlwEl2uF4hCxE0cd/sSw71Zfag0jKa39y4HDLh0BQ4PVBX1384Ye8GqrEZ69xgLp9fwPbv3GgIDDF2Q== dependencies: - "@babel/helper-function-name" "^7.7.4" - "@babel/helper-member-expression-to-functions" "^7.7.4" - "@babel/helper-optimise-call-expression" "^7.7.4" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.7.4" - "@babel/helper-split-export-declaration" "^7.7.4" + "@babel/helper-function-name" "^7.8.0" + "@babel/helper-member-expression-to-functions" "^7.8.0" + "@babel/helper-optimise-call-expression" "^7.8.0" + "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-replace-supers" "^7.8.0" + "@babel/helper-split-export-declaration" "^7.8.0" "@babel/helper-create-regexp-features-plugin@^7.7.4": version "7.7.4" @@ -117,6 +134,15 @@ "@babel/template" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/helper-function-name@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.0.tgz#dde5cf0d6b15c21817a67dd66fe7350348e023bf" + integrity sha512-x9psucuU0Xalw+0Vpr2FYJMLB7/KnPSLZhlkUyOGbYAWRDfmtZBrguYpJYiaNCRV7vGkYjO/gF6/J6yMvdWTDw== + dependencies: + "@babel/helper-get-function-arity" "^7.8.0" + "@babel/template" "^7.8.0" + "@babel/types" "^7.8.0" + "@babel/helper-get-function-arity@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0" @@ -124,6 +150,13 @@ dependencies: "@babel/types" "^7.7.4" +"@babel/helper-get-function-arity@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.0.tgz#90977f61d76d2225d1ae0208def7df22ea92792e" + integrity sha512-eUP5grliToMapQiTaYS2AAO/WwaCG7cuJztR1v/a1aPzUzUeGt+AaI9OvLATc/AfFkF8SLJ10d5ugGt/AQ9d6w== + dependencies: + "@babel/types" "^7.8.0" + "@babel/helper-hoist-variables@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.7.4.tgz#612384e3d823fdfaaf9fce31550fe5d4db0f3d12" @@ -138,6 +171,13 @@ dependencies: "@babel/types" "^7.7.4" +"@babel/helper-member-expression-to-functions@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.0.tgz#50d0ed445d2da11beb60e2dbc2c428254bd5a4ae" + integrity sha512-0m1QabGrdXuoxX/g+KOAGndoHwskC70WweqHRQyCsaO67KOEELYh4ECcGw6ZGKjDKa5Y7SW4Qbhw6ly4Fah/jQ== + dependencies: + "@babel/types" "^7.8.0" + "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.7.4", "@babel/helper-module-imports@^7.8.0": version "7.8.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.0.tgz#076edda55d8cd39c099981b785ce53f4303b967e" @@ -164,6 +204,13 @@ dependencies: "@babel/types" "^7.7.4" +"@babel/helper-optimise-call-expression@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.0.tgz#3df62773cf210db9ed34c2bb39fece5acd1e1733" + integrity sha512-aiJt1m+K57y0n10fTw+QXcCXzmpkG+o+NoQmAZqlZPstkTE0PZT+Z27QSd/6Gf00nuXJQO4NiJ0/YagSW5kC2A== + dependencies: + "@babel/types" "^7.8.0" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0": version "7.8.0" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.0.tgz#59ec882d43c21c544ccb51decaecb306b34a8231" @@ -197,6 +244,16 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/helper-replace-supers@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.0.tgz#d83cb117edb820eebe9ae6c970a8ad5eac09d19f" + integrity sha512-R2CyorW4tcO3YzdkClLpt6MS84G+tPkOi0MmiCn1bvYVnmDpdl9R15XOi3NQW2mhOAEeBnuQ4g1Bh7pT2sX8fg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.8.0" + "@babel/helper-optimise-call-expression" "^7.8.0" + "@babel/traverse" "^7.8.0" + "@babel/types" "^7.8.0" + "@babel/helper-simple-access@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.7.4.tgz#a169a0adb1b5f418cfc19f22586b2ebf58a9a294" @@ -212,6 +269,13 @@ dependencies: "@babel/types" "^7.7.4" +"@babel/helper-split-export-declaration@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.0.tgz#ed10cb03b07454c0d40735fad4e9c9711e739588" + integrity sha512-YhYFhH4T6DlbT6CPtVgLfC1Jp2gbCawU/ml7WJvUpBg01bCrXSzTYMZZXbbIGjq/kHmK8YUATxTppcRGzj31pA== + dependencies: + "@babel/types" "^7.8.0" + "@babel/helper-wrap-function@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.7.4.tgz#37ab7fed5150e22d9d7266e830072c0cdd8baace" @@ -240,11 +304,25 @@ esutils "^2.0.2" js-tokens "^4.0.0" +"@babel/highlight@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.0.tgz#4cc003dc10359919e2e3a1d9459150942913dd1a" + integrity sha512-OsdTJbHlPtIk2mmtwXItYrdmalJ8T0zpVzNAbKSkHshuywj7zb29Y09McV/jQsQunc/nEyHiPV2oy9llYMLqxw== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.7.4", "@babel/parser@^7.7.7": version "7.7.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.7.tgz#1b886595419cf92d811316d5b715a53ff38b4937" integrity sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw== +"@babel/parser@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.0.tgz#54682775f1fb25dd29a93a02315aab29a6a292bb" + integrity sha512-VVtsnUYbd1+2A2vOVhm4P2qNXQE8L/W859GpUHfUcdhX8d3pEKThZuIr6fztocWx9HbK+00/CR0tXnhAggJ4CA== + "@babel/plugin-proposal-async-generator-functions@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.7.4.tgz#0351c5ac0a9e927845fffd5b82af476947b7ce6d" @@ -255,12 +333,12 @@ "@babel/plugin-syntax-async-generators" "^7.7.4" "@babel/plugin-proposal-class-properties@^7.5.5": - version "7.7.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.4.tgz#2f964f0cb18b948450362742e33e15211e77c2ba" - integrity sha512-EcuXeV4Hv1X3+Q1TsuOmyyxeTRiSqurGJ26+I/FW1WbymmRRapVORm6x1Zl3iDIHyRxEs+VXWp6qnlcfcJSbbw== + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.0.tgz#bb3325d9166c80db8f2e15fc0bb6d61d7300e373" + integrity sha512-eVGj5NauhKCwABQjKIYncMQh9HtFsBrIcdsxImbTdUIaGnjymsVsBGmDQaDuPL/WCjYn6vPL4d+yvI6zy+VkrQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.7.4" - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-create-class-features-plugin" "^7.8.0" + "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-proposal-dynamic-import@^7.7.4": version "7.7.4" @@ -732,6 +810,15 @@ "@babel/parser" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/template@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.0.tgz#a32f57ad3be89c0fa69ae87b53b4826844dc6330" + integrity sha512-0NNMDsY2t3ltAVVK1WHNiaePo3tXPUeJpCX4I3xSKFoEl852wJHG8mrgHVADf8Lz1y+8al9cF7cSSfzSnFSYiw== + dependencies: + "@babel/code-frame" "^7.8.0" + "@babel/parser" "^7.8.0" + "@babel/types" "^7.8.0" + "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558" @@ -747,6 +834,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.0.tgz#d85266fdcff553c10e57b672604b36383a127c1f" + integrity sha512-d/6sPXFLGlJHZO/zWDtgFaKyalCOHLedzxpVJn6el1cw+f2TZa7xZEszeXdOw6EUemqRFBAn106BWBvtSck9Qw== + dependencies: + "@babel/code-frame" "^7.8.0" + "@babel/generator" "^7.8.0" + "@babel/helper-function-name" "^7.8.0" + "@babel/helper-split-export-declaration" "^7.8.0" + "@babel/parser" "^7.8.0" + "@babel/types" "^7.8.0" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" From d81c0cec6a8d73f21e4c4a4ede411c2ac2a9acb7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2020 17:17:00 +0100 Subject: [PATCH 18/53] Bump eslint-plugin-import from 2.19.1 to 2.20.0 (#414) Bumps [eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import) from 2.19.1 to 2.20.0. - [Release notes](https://github.com/benmosher/eslint-plugin-import/releases) - [Changelog](https://github.com/benmosher/eslint-plugin-import/blob/master/CHANGELOG.md) - [Commits](https://github.com/benmosher/eslint-plugin-import/compare/v2.19.1...v2.20.0) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b5566ee18..e60db93c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4749,9 +4749,9 @@ eslint-plugin-eslint-plugin@^2.1.0: integrity sha512-kT3A/ZJftt28gbl/Cv04qezb/NQ1dwYIbi8lyf806XMxkus7DvOVCLIfTXMrorp322Pnoez7+zabXH29tADIDg== eslint-plugin-import@^2.18.2: - version "2.19.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.19.1.tgz#5654e10b7839d064dd0d46cd1b88ec2133a11448" - integrity sha512-x68131aKoCZlCae7rDXKSAQmbT5DQuManyXo2sK6fJJ0aK5CWAkv6A6HJZGgqC8IhjQxYPgo6/IY4Oz8AFsbBw== + version "2.20.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.0.tgz#d749a7263fb6c29980def8e960d380a6aa6aecaa" + integrity sha512-NK42oA0mUc8Ngn4kONOPsPB1XhbUvNHqF+g307dPV28aknPoiNnKLFd9em4nkswwepdF5ouieqv5Th/63U7YJQ== dependencies: array-includes "^3.0.3" array.prototype.flat "^1.2.1" From 306d41d79ab484098951a49ae6b6f9ba31850072 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2020 16:18:49 +0000 Subject: [PATCH 19/53] Bump @babel/plugin-syntax-dynamic-import from 7.7.4 to 7.8.0 Bumps [@babel/plugin-syntax-dynamic-import](https://github.com/babel/babel) from 7.7.4 to 7.8.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/master/CHANGELOG.md) - [Commits](https://github.com/babel/babel/compare/v7.7.4...v7.8.0) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index e60db93c7..3dfe8ba27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -388,11 +388,11 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-dynamic-import@^7.2.0", "@babel/plugin-syntax-dynamic-import@^7.7.4": - version "7.7.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.7.4.tgz#29ca3b4415abfe4a5ec381e903862ad1a54c3aec" - integrity sha512-jHQW0vbRGvwQNgyVxwDh4yuXu4bH1f5/EICJLAhl1SblLs2CDhrsmCk+v5XLdE9wxtAFRyxx+P//Iw+a5L/tTg== + version "7.8.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.0.tgz#3a6c1cd36af923db602df83c5aa72e08bb14353a" + integrity sha512-Mx2RzpCHJaBfmFdA2abXDKRHVJdzJ6R0Wqwb6TxCgM7NRR5wcC4cyiAsRL7Ga+lwG8GG1cKvb+4ENjic8y15jA== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-json-strings@^7.7.4": version "7.7.4" From 4388c623415e021b1abfb144c59992e77888d491 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 14 Jan 2020 17:16:43 +0300 Subject: [PATCH 20/53] Gas station (#409) * keep track of chain update * fetch gasPrice from station and cache on current chain update state * use fetchGasPrice when sending transactions * inject fetchGasPrice as a dependency --- src/api/deposit/DepositApi.ts | 22 +++++++++-- src/api/erc20/Erc20Api.ts | 13 ++++++- src/api/exchange/ExchangeApi.ts | 12 +++--- src/api/gasStation.ts | 65 +++++++++++++++++++++++++++++++++ src/api/index.ts | 28 ++++++++------ src/api/wallet/WalletApi.ts | 20 +++++++++- src/api/wallet/WalletApiMock.ts | 6 +++ 7 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 src/api/gasStation.ts diff --git a/src/api/deposit/DepositApi.ts b/src/api/deposit/DepositApi.ts index 9b3d1eddd..f40e5b7ee 100644 --- a/src/api/deposit/DepositApi.ts +++ b/src/api/deposit/DepositApi.ts @@ -50,15 +50,23 @@ export interface PendingFlux { batchId: number } +export interface InjectedDependencies { + fetchGasPrice(): Promise +} + export class DepositApiImpl implements DepositApi { protected _contractPrototype: BatchExchangeContract protected _web3: Web3 protected static _contractsCache: { [network: number]: { [address: string]: BatchExchangeContract } } = {} - public constructor(web3: Web3) { + protected fetchGasPrice: InjectedDependencies['fetchGasPrice'] + + public constructor(web3: Web3, injectedDependencies: InjectedDependencies) { this._contractPrototype = new web3.eth.Contract(batchExchangeAbi) as BatchExchangeContract this._web3 = web3 + Object.assign(this, injectedDependencies) + // TODO remove later // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(window as any).epoch = this._contractPrototype @@ -132,7 +140,9 @@ export class DepositApiImpl implements DepositApi { }: DepositParams): Promise { const contract = await this._getContract(networkId) // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 - const tx = contract.methods.deposit(tokenAddress, amount.toString()).send({ from: userAddress }) + const tx = contract.methods + .deposit(tokenAddress, amount.toString()) + .send({ from: userAddress, gasPrice: await this.fetchGasPrice() }) if (txOptionalParams && txOptionalParams.onSentTransaction) { tx.once('transactionHash', txOptionalParams.onSentTransaction) @@ -151,7 +161,9 @@ export class DepositApiImpl implements DepositApi { }: RequestWithdrawParams): Promise { const contract = await this._getContract(networkId) // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 - const tx = contract.methods.requestWithdraw(tokenAddress, amount.toString()).send({ from: userAddress }) + const tx = contract.methods + .requestWithdraw(tokenAddress, amount.toString()) + .send({ from: userAddress, gasPrice: await this.fetchGasPrice() }) if (txOptionalParams?.onSentTransaction) { tx.once('transactionHash', txOptionalParams.onSentTransaction) @@ -163,7 +175,9 @@ export class DepositApiImpl implements DepositApi { public async withdraw({ userAddress, tokenAddress, networkId, txOptionalParams }: WithdrawParams): Promise { const contract = await this._getContract(networkId) - const tx = contract.methods.withdraw(userAddress, tokenAddress).send({ from: userAddress }) + const tx = contract.methods + .withdraw(userAddress, tokenAddress) + .send({ from: userAddress, gasPrice: await this.fetchGasPrice() }) if (txOptionalParams?.onSentTransaction) { tx.once('transactionHash', txOptionalParams.onSentTransaction) diff --git a/src/api/erc20/Erc20Api.ts b/src/api/erc20/Erc20Api.ts index e809a4e94..061e8c537 100644 --- a/src/api/erc20/Erc20Api.ts +++ b/src/api/erc20/Erc20Api.ts @@ -66,6 +66,10 @@ export interface Erc20Api { transferFrom(params: TransferFromParams): Promise } +export interface InjectedDependencies { + fetchGasPrice(): Promise +} + /** * Basic implementation of ERC20 API */ @@ -74,9 +78,13 @@ export class Erc20ApiImpl implements Erc20Api { private static _contractsCache: { [network: number]: { [address: string]: Erc20Contract } } = {} - public constructor(web3: Web3) { + private fetchGasPrice: InjectedDependencies['fetchGasPrice'] + + public constructor(web3: Web3, injectedDependencies: InjectedDependencies) { this._contractPrototype = new web3.eth.Contract(erc20Abi as AbiItem[]) as Erc20Contract + Object.assign(this, injectedDependencies) + // TODO remove later // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(window as any).erc20 = this._contractPrototype @@ -143,6 +151,7 @@ export class Erc20ApiImpl implements Erc20Api { // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = erc20.methods.approve(spenderAddress, amount.toString()).send({ from: userAddress, + gasPrice: await this.fetchGasPrice(), }) if (txOptionalParams?.onSentTransaction) { @@ -165,6 +174,7 @@ export class Erc20ApiImpl implements Erc20Api { // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = erc20.methods.transfer(toAddress, amount.toString()).send({ from: userAddress, + gasPrice: await this.fetchGasPrice(), }) if (txOptionalParams?.onSentTransaction) { @@ -187,6 +197,7 @@ export class Erc20ApiImpl implements Erc20Api { const tx = erc20.methods.transferFrom(userAddress, toAddress, amount.toString()).send({ from: fromAddress, + gasPrice: await this.fetchGasPrice(), }) if (txOptionalParams?.onSentTransaction) { diff --git a/src/api/exchange/ExchangeApi.ts b/src/api/exchange/ExchangeApi.ts index 140acd506..b23850782 100644 --- a/src/api/exchange/ExchangeApi.ts +++ b/src/api/exchange/ExchangeApi.ts @@ -1,5 +1,5 @@ import BN from 'bn.js' -import { DepositApiImpl, DepositApi } from 'api/deposit/DepositApi' +import { DepositApiImpl, DepositApi, InjectedDependencies } from 'api/deposit/DepositApi' import { Receipt, TxOptionalParams } from 'types' import { log } from 'utils' import Web3 from 'web3' @@ -78,8 +78,8 @@ export interface Order { * Basic implementation of Stable Coin Converter API */ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { - public constructor(web3: Web3) { - super(web3) + public constructor(web3: Web3, injectedDependencies: InjectedDependencies) { + super(web3, injectedDependencies) // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(window as any).exchange = this._contractPrototype } @@ -125,7 +125,7 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { public async addToken({ userAddress, tokenAddress, networkId, txOptionalParams }: AddTokenParams): Promise { const contract = await this._getContract(networkId) - const tx = contract.methods.addToken(tokenAddress).send({ from: userAddress }) + const tx = contract.methods.addToken(tokenAddress).send({ from: userAddress, gasPrice: await this.fetchGasPrice() }) if (txOptionalParams && txOptionalParams.onSentTransaction) { tx.once('transactionHash', txOptionalParams.onSentTransaction) @@ -153,7 +153,7 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { // TODO: Remove temporal fix for web3. See https://github.com/gnosis/dex-react/issues/231 const tx = contract.methods .placeOrder(buyTokenId, sellTokenId, validUntil, buyAmount.toString(), sellAmount.toString()) - .send({ from: userAddress }) + .send({ from: userAddress, gasPrice: await this.fetchGasPrice() }) if (txOptionalParams && txOptionalParams.onSentTransaction) { tx.once('transactionHash', txOptionalParams.onSentTransaction) @@ -176,7 +176,7 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { txOptionalParams, }: CancelOrdersParams): Promise { const contract = await this._getContract(networkId) - const tx = contract.methods.cancelOrders(orderIds).send({ from: userAddress }) + const tx = contract.methods.cancelOrders(orderIds).send({ from: userAddress, gasPrice: await this.fetchGasPrice() }) if (txOptionalParams && txOptionalParams.onSentTransaction) { tx.once('transactionHash', txOptionalParams.onSentTransaction) diff --git a/src/api/gasStation.ts b/src/api/gasStation.ts new file mode 100644 index 000000000..29c273e70 --- /dev/null +++ b/src/api/gasStation.ts @@ -0,0 +1,65 @@ +import { WalletApi } from './wallet/WalletApi' + +const GAS_STATIONS = { + 1: 'https://safe-relay.gnosis.pm/api/v1/gas-station/', + 4: 'https://safe-relay.staging.gnosisdev.com/api/v1/gas-station/', +} + +const GAS_PRICE_LEVEL: Exclude = 'standard' + +let cacheKey = '' +let cachedGasPrice: undefined | string + +const constructKey = ({ blockNumber, chainId }: GasPriceCacheDeps): string => chainId + '@' + blockNumber + +interface GasPriceCacheDeps { + blockNumber: number | null + chainId: number +} + +interface GasStationResponse { + lastUpdate: string + lowest: string + safeLow: string + standard: string + fast: string + fastest: string +} + +const fetchGasPriceFactory = (walletApi: WalletApi) => async (): Promise => { + const { blockchainState } = walletApi + + if (!blockchainState) return undefined + + const { chainId, blockHeader } = blockchainState + + // only fetch new gasPrice when chainId or blockNumber changed + const key = constructKey({ chainId, blockNumber: blockHeader && blockHeader.number }) + if (key === cacheKey) { + console.log('gasPrice from cache', cachedGasPrice) + return cachedGasPrice + } + + const gasStationURL = GAS_STATIONS[chainId] + + if (!gasStationURL) return undefined + + try { + const response = await fetch(gasStationURL) + const json: GasStationResponse = await response.json() + + const gasPrice = json[GAS_PRICE_LEVEL] + if (gasPrice) { + cacheKey = key + cachedGasPrice = gasPrice + console.log('new gasPrice', gasPrice) + + return gasPrice + } + } catch (error) { + console.error('Error fetching gasPrice from', gasStationURL, error) + } + return undefined +} + +export default fetchGasPriceFactory diff --git a/src/api/index.ts b/src/api/index.ts index cdf67a659..d08fa0f68 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -4,9 +4,9 @@ import { WalletApiImpl, WalletApi } from './wallet/WalletApi' import { TokenListApiImpl, TokenList } from './tokenList/TokenListApi' import { TokenListApiMock } from './tokenList/TokenListApiMock' import { Erc20ApiMock } from './erc20/Erc20ApiMock' -import { Erc20ApiImpl, Erc20Api } from './erc20/Erc20Api' +import { Erc20ApiImpl, Erc20Api, InjectedDependencies as Erc20ApiDependencies } from './erc20/Erc20Api' import { DepositApiMock } from './deposit/DepositApiMock' -import { DepositApiImpl, DepositApi } from './deposit/DepositApi' +import { DepositApiImpl, DepositApi, InjectedDependencies as DepositApiDependencies } from './deposit/DepositApi' import { ExchangeApiImpl, ExchangeApi } from './exchange/ExchangeApi' import { ExchangeApiMock } from './exchange/ExchangeApiMock' import { @@ -21,6 +21,7 @@ import { } from '../../test/data' import Web3 from 'web3' import { INITIAL_INFURA_ENDPOINT } from 'const' +import fetchGasPriceFactory from './gasStation' // TODO connect to mainnet if we need AUTOCONNECT at all export const getDefaultProvider = (): string | null => @@ -48,29 +49,29 @@ function createWalletApi(web3: Web3): WalletApi { return walletApi } -function createErc20Api(web3: Web3): Erc20Api { +function createErc20Api(web3: Web3, injectedDependencies: Erc20ApiDependencies): Erc20Api { let erc20Api if (process.env.MOCK_ERC20 === 'true') { erc20Api = new Erc20ApiMock({ balances: erc20Balances, allowances: erc20Allowances, tokens: unregisteredTokens }) } else { - erc20Api = new Erc20ApiImpl(web3) + erc20Api = new Erc20ApiImpl(web3, injectedDependencies) } window['erc20Api'] = erc20Api // register for convenience return erc20Api } -function createDepositApi(erc20Api: Erc20Api, web3: Web3): DepositApi { +function createDepositApi(erc20Api: Erc20Api, web3: Web3, injectedDependencies: DepositApiDependencies): DepositApi { let depositApi if (process.env.MOCK_DEPOSIT === 'true') { depositApi = new DepositApiMock(exchangeBalanceStates, erc20Api) } else { - depositApi = new DepositApiImpl(web3) + depositApi = new DepositApiImpl(web3, injectedDependencies) } window['depositApi'] = depositApi // register for convenience return depositApi } -function createExchangeApi(erc20Api: Erc20Api, web3: Web3): ExchangeApi { +function createExchangeApi(erc20Api: Erc20Api, web3: Web3, injectedDependencies: DepositApiDependencies): ExchangeApi { let exchangeApi if (process.env.MOCK_EXCHANGE === 'true') { const tokens = [FEE_TOKEN, ...tokenList.map(token => token.address), TOKEN_8] @@ -81,7 +82,7 @@ function createExchangeApi(erc20Api: Erc20Api, web3: Web3): ExchangeApi { ordersByUser: exchangeOrders, }) } else { - exchangeApi = new ExchangeApiImpl(web3) + exchangeApi = new ExchangeApiImpl(web3, injectedDependencies) } window['exchangeApi'] = exchangeApi return exchangeApi @@ -104,7 +105,12 @@ function createTokenListApi(): TokenList { // Build APIs export const web3: Web3 = createWeb3Api() export const walletApi: WalletApi = createWalletApi(web3) -export const erc20Api: Erc20Api = createErc20Api(web3) -export const depositApi: DepositApi = createDepositApi(erc20Api, web3) -export const exchangeApi: ExchangeApi = createExchangeApi(erc20Api, web3) + +const injectedDependencies = { + fetchGasPrice: fetchGasPriceFactory(walletApi), +} + +export const erc20Api: Erc20Api = createErc20Api(web3, injectedDependencies) +export const depositApi: DepositApi = createDepositApi(erc20Api, web3, injectedDependencies) +export const exchangeApi: ExchangeApi = createExchangeApi(erc20Api, web3, injectedDependencies) export const tokenListApi: TokenList = createTokenListApi() diff --git a/src/api/wallet/WalletApi.ts b/src/api/wallet/WalletApi.ts index 1e3e57db8..4d1fcb1d3 100644 --- a/src/api/wallet/WalletApi.ts +++ b/src/api/wallet/WalletApi.ts @@ -37,6 +37,7 @@ export interface WalletApi { addOnChangeWalletInfo(callback: (walletInfo: WalletInfo) => void, trigger?: boolean): Command removeOnChangeWalletInfo(callback: (walletInfo: WalletInfo) => void): void getProviderInfo(): ProviderInfo + blockchainState: BlockchainUpdatePrompt } export interface WalletInfo { @@ -160,6 +161,8 @@ export class WalletApiImpl implements WalletApi { private _provider: Provider | null private _web3: Web3 + public blockchainState: BlockchainUpdatePrompt + private _unsubscribe: Command = () => { // Empty comment to indicate this is on purpose: https://github.com/eslint/eslint/commit/c1c4f4d } @@ -191,7 +194,18 @@ export class WalletApiImpl implements WalletApi { // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(window as any).web3c = this._web3 - await this._notifyListeners() + const { + accounts: [account], + chainId, + } = getProviderState(provider) + + this.blockchainState = { + account, + chainId, + blockHeader: null, + } + + await this._notifyListeners(this.blockchainState) const subscriptions = createSubscriptions(provider) @@ -279,7 +293,9 @@ export class WalletApiImpl implements WalletApi { /* **************** Private Functions **************** */ - private async _notifyListeners(): Promise { + private async _notifyListeners(blockchainUpdate?: BlockchainUpdatePrompt): Promise { + if (blockchainUpdate) this.blockchainState = blockchainUpdate + await Promise.resolve() const walletInfo: WalletInfo = this.getWalletInfo() this._listeners.forEach(listener => listener(walletInfo)) diff --git a/src/api/wallet/WalletApiMock.ts b/src/api/wallet/WalletApiMock.ts index c493b21d6..c150eaa00 100644 --- a/src/api/wallet/WalletApiMock.ts +++ b/src/api/wallet/WalletApiMock.ts @@ -18,6 +18,12 @@ export class WalletApiMock implements WalletApi { private _balance: BN private _listeners: ((walletInfo: WalletInfo) => void)[] + public blockchainState = { + account: USER_1, + chainId: 1, + blockHeader: null, + } + public constructor() { this._connected = process.env.AUTOCONNECT === 'true' this._user = USER_1 From a2218732c63ff7dd6b2fe6f7f080f3725c90d114 Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Wed, 15 Jan 2020 08:00:14 -0800 Subject: [PATCH 21/53] 329/cache layer (#405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added first attempt at a generic Cache proxy for apis * Added Erc20ApiProxy * No need for constructor * Updating docstring * iSmall refactor on dumb hash calculation * Using Node-cache for cache storage * Caching some Erc20Api methods forever * Accepting a custom hash function * Making cache layer able to cache both sync and async methods * Adding another approach for caching * Fixing eslint any disables * Passing return type to get parameter and small refactor * Chose second approach for caching * Renaming variable expiration to ttl * Adding proxies for Deposit and Exchange APIs * Fixing import path for CacheMixin * Falsy values are valid function returns. Only when `undefined` there's no hit * 🤦 * Using utils log function to avoid logging during tests * Passing all parameters down to hash function * Properly caching all parameter types * Small refactorings on CacheMixin: - Remove type P for parameters as it's no needed - Returned function accepts a list of any as paramenter (...p:any[]) - Moving bind of fnToCache to parent method - Extracted a variable with bound hash method on hash method for recursive binding --- package.json | 1 + src/api/deposit/DepositApiProxy.ts | 18 ++++ src/api/erc20/Erc20ApiProxy.ts | 24 +++++ src/api/exchange/ExchangeApiProxy.ts | 22 ++++ src/api/index.ts | 15 +-- src/api/proxy/CacheMixin.ts | 126 ++++++++++++++++++++++ src/api/proxy/index.ts | 1 + test/api/proxy/CacheProxy.test.ts | 154 +++++++++++++++++++++++++++ yarn.lock | 9 +- 9 files changed, 363 insertions(+), 7 deletions(-) create mode 100644 src/api/deposit/DepositApiProxy.ts create mode 100644 src/api/erc20/Erc20ApiProxy.ts create mode 100644 src/api/exchange/ExchangeApiProxy.ts create mode 100644 src/api/proxy/CacheMixin.ts create mode 100644 src/api/proxy/index.ts create mode 100644 test/api/proxy/CacheProxy.test.ts diff --git a/package.json b/package.json index 91fdd8d0a..e4ddea570 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "combine-reducers": "^1.0.0", "date-fns": "^2.8.1", "modali": "^1.2.0", + "node-cache": "^5.1.0", "polished": "^3.4.1", "qrcode.react": "^1.0.0", "react": "^16.8.6", diff --git a/src/api/deposit/DepositApiProxy.ts b/src/api/deposit/DepositApiProxy.ts new file mode 100644 index 000000000..c39d015a4 --- /dev/null +++ b/src/api/deposit/DepositApiProxy.ts @@ -0,0 +1,18 @@ +import Web3 from 'web3' + +import { CacheMixin } from 'api/proxy' +import { InjectedDependencies } from 'api/erc20/Erc20Api' + +import { DepositApiImpl, DepositApi } from './DepositApi' + +export class DepositApiProxy extends DepositApiImpl { + private cache: CacheMixin + + public constructor(web3: Web3, injectedDependencies: InjectedDependencies) { + super(web3, injectedDependencies) + + this.cache = new CacheMixin() + + this.cache.injectCache(this, [{ method: 'getContractAddress' }]) + } +} diff --git a/src/api/erc20/Erc20ApiProxy.ts b/src/api/erc20/Erc20ApiProxy.ts new file mode 100644 index 000000000..74719b579 --- /dev/null +++ b/src/api/erc20/Erc20ApiProxy.ts @@ -0,0 +1,24 @@ +import Web3 from 'web3' + +import { CacheMixin } from 'api/proxy' + +import Erc20ApiImpl, { Erc20Api, InjectedDependencies } from './Erc20Api' + +export class Erc20ApiProxy extends Erc20ApiImpl { + private cache: CacheMixin + + public constructor(web3: Web3, injectedDependencies: InjectedDependencies) { + super(web3, injectedDependencies) + + // Potentially, we could have a global cache obj OR + // we could keep a local instance like this that allows more granular cache settings, such as default timeouts, etc + this.cache = new CacheMixin() + + this.cache.injectCache(this, [ + { method: 'name' }, + { method: 'symbol' }, + { method: 'decimals' }, + { method: 'totalSupply' }, + ]) + } +} diff --git a/src/api/exchange/ExchangeApiProxy.ts b/src/api/exchange/ExchangeApiProxy.ts new file mode 100644 index 000000000..5d0917ced --- /dev/null +++ b/src/api/exchange/ExchangeApiProxy.ts @@ -0,0 +1,22 @@ +import Web3 from 'web3' + +import { CacheMixin } from 'api/proxy' +import { InjectedDependencies } from 'api/erc20/Erc20Api' + +import ExchangeApiImpl, { ExchangeApi } from './ExchangeApi' + +export class ExchangeApiProxy extends ExchangeApiImpl { + private cache: CacheMixin + + public constructor(web3: Web3, injectedDependencies: InjectedDependencies) { + super(web3, injectedDependencies) + + this.cache = new CacheMixin() + + this.cache.injectCache(this, [ + { method: 'getFeeDenominator' }, + { method: 'getTokenAddressById' }, + { method: 'getTokenIdByAddress' }, + ]) + } +} diff --git a/src/api/index.ts b/src/api/index.ts index d08fa0f68..9cb8d7692 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,12 +3,15 @@ import { WalletApiMock } from './wallet/WalletApiMock' import { WalletApiImpl, WalletApi } from './wallet/WalletApi' import { TokenListApiImpl, TokenList } from './tokenList/TokenListApi' import { TokenListApiMock } from './tokenList/TokenListApiMock' +import { Erc20Api, InjectedDependencies as Erc20ApiDependencies } from './erc20/Erc20Api' import { Erc20ApiMock } from './erc20/Erc20ApiMock' -import { Erc20ApiImpl, Erc20Api, InjectedDependencies as Erc20ApiDependencies } from './erc20/Erc20Api' +import { Erc20ApiProxy } from './erc20/Erc20ApiProxy' +import { DepositApi, InjectedDependencies as DepositApiDependencies } from './deposit/DepositApi' import { DepositApiMock } from './deposit/DepositApiMock' -import { DepositApiImpl, DepositApi, InjectedDependencies as DepositApiDependencies } from './deposit/DepositApi' -import { ExchangeApiImpl, ExchangeApi } from './exchange/ExchangeApi' +import { DepositApiProxy } from './deposit/DepositApiProxy' +import { ExchangeApi } from './exchange/ExchangeApi' import { ExchangeApiMock } from './exchange/ExchangeApiMock' +import { ExchangeApiProxy } from './exchange/ExchangeApiProxy' import { tokenList, exchangeBalanceStates, @@ -54,7 +57,7 @@ function createErc20Api(web3: Web3, injectedDependencies: Erc20ApiDependencies): if (process.env.MOCK_ERC20 === 'true') { erc20Api = new Erc20ApiMock({ balances: erc20Balances, allowances: erc20Allowances, tokens: unregisteredTokens }) } else { - erc20Api = new Erc20ApiImpl(web3, injectedDependencies) + erc20Api = new Erc20ApiProxy(web3, injectedDependencies) } window['erc20Api'] = erc20Api // register for convenience return erc20Api @@ -65,7 +68,7 @@ function createDepositApi(erc20Api: Erc20Api, web3: Web3, injectedDependencies: if (process.env.MOCK_DEPOSIT === 'true') { depositApi = new DepositApiMock(exchangeBalanceStates, erc20Api) } else { - depositApi = new DepositApiImpl(web3, injectedDependencies) + depositApi = new DepositApiProxy(web3, injectedDependencies) } window['depositApi'] = depositApi // register for convenience return depositApi @@ -82,7 +85,7 @@ function createExchangeApi(erc20Api: Erc20Api, web3: Web3, injectedDependencies: ordersByUser: exchangeOrders, }) } else { - exchangeApi = new ExchangeApiImpl(web3, injectedDependencies) + exchangeApi = new ExchangeApiProxy(web3, injectedDependencies) } window['exchangeApi'] = exchangeApi return exchangeApi diff --git a/src/api/proxy/CacheMixin.ts b/src/api/proxy/CacheMixin.ts new file mode 100644 index 000000000..3ed584d7b --- /dev/null +++ b/src/api/proxy/CacheMixin.ts @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import NodeCache from 'node-cache' + +import { log } from 'utils' + +interface CacheOptions { + method: keyof T + ttl?: number + hashFn?: (...params: any[]) => string +} + +interface CacheMethodParams extends CacheOptions { + fnToCache: (...params: any[]) => R +} + +export class CacheMixin { + private cache: NodeCache + + public constructor() { + this.cache = new NodeCache({ useClones: false }) + } + + /** + * Injects into provided `instance` a version with cache according to the list `toCache`. + * Injected methods will obey `ttl` if provided, otherwise global cache config. + * + * @param instance Class instance to cache + * @param toCache List of configs for methods to be cached + */ + public injectCache(instance: T, toCache: CacheOptions[]): void { + toCache.forEach(cacheConfig => { + const { method } = cacheConfig + const methodName = method.toString() + + const fnToCache = instance[methodName].bind(instance) + + const params: CacheMethodParams> = { + ...cacheConfig, + fnToCache, + } + + instance[methodName] = this.cacheMethod(params) + }) + } + + /** + * HOF that returns a new function caching the return value of provided `fnToCache` + */ + private cacheMethod({ fnToCache, method, ttl, hashFn }: CacheMethodParams): (...params: any[]) => R { + return (...params: any[]): R => { + const hash = hashFn ? hashFn(method, params) : this.hashParams(method.toString(), params) + + let value = this.get(hash) + + if (value) { + // cache hit + return value + } + + // call original fn + value = fnToCache(...params) + + // save it for next round + this.store(hash, value, ttl) + + return value as R + } + } + + private get(hash: string): R | undefined { + const obj = this.cache.get(hash) + + if (obj === undefined) { + return + } + + // TODO: remove when done testing + log(`cache hit for ${hash}`) + return obj + } + + private store(hash: string, obj: R, ttl?: number): void { + if (ttl) { + // with TTL + this.cache.set(hash, obj, ttl) + } else { + // based on default config + this.cache.set(hash, obj) + } + } + + /** + * Dumb hash function that simply glues together paramName:paramValue + * Assumes all values being hashed can be converted to string + * Sorts parameters for determinism + * + * TODO: replace this with an actual hash function once testing is done + * + * @param params The params we want to hash + * + */ + private hashParams(methodName: string, params: any): string { + return `${methodName}>>${this.hash(params)}` + } + + private hash(obj: any): string { + // primitive type + if (typeof obj !== 'object') { + return obj.toString() + } + // local this.hash with correct `this` reference + const hash = this.hash.bind(this) + // array + if (Array.isArray(obj)) { + return obj + .sort() + .map(hash) + .join('|') + } + // obj + return Object.keys(obj) + .sort() + .map(key => `${key}:${hash(obj[key])}`) + .join('|') + } +} diff --git a/src/api/proxy/index.ts b/src/api/proxy/index.ts new file mode 100644 index 000000000..b9ec27af9 --- /dev/null +++ b/src/api/proxy/index.ts @@ -0,0 +1 @@ +export * from './CacheMixin' diff --git a/test/api/proxy/CacheProxy.test.ts b/test/api/proxy/CacheProxy.test.ts new file mode 100644 index 000000000..b99a5c293 --- /dev/null +++ b/test/api/proxy/CacheProxy.test.ts @@ -0,0 +1,154 @@ +import { CacheMixin } from 'api/proxy' + +interface TestApi { + echo(params: Params): T + cachedMethod(params: Params): Promise + syncCachedMethod(params: Params): T + customHashFn(params: Params): Promise + nonCachedMethod(params: Params): Promise + flatParam(param: number): number + multiFlatParams(p1: number, p2: string): string +} + +interface Params { + p: T +} + +class TestApiImpl implements TestApi { + // 'public' to be able to spy on + public echo({ p }: Params): T { + return p + } + + public async cachedMethod(params: Params): Promise { + return this.echo(params) + } + public syncCachedMethod(params: Params): T { + return this.echo(params) + } + public async customHashFn(params: Params): Promise { + return this.echo(params) + } + public async nonCachedMethod(params: Params): Promise { + return this.echo(params) + } + public flatParam(p: number): number { + return this.echo({ p }) + } + public multiFlatParams(p1: number, p2: string): string { + return this.echo({ p: `${p1}${p2}` }) + } +} + +function hashFn(..._params: any[]): string { + return 'always the same lol' +} + +class TestApiProxyV2 extends TestApiImpl { + private cache: CacheMixin + + public constructor() { + super() + + this.cache = new CacheMixin() + + this.cache.injectCache(this, [ + { method: 'cachedMethod', ttl: 10 }, + { method: 'syncCachedMethod' }, + { method: 'customHashFn', hashFn }, + { method: 'flatParam' }, + { method: 'multiFlatParams' }, + ]) + } +} + +let instance: TestApi + +beforeEach(() => { + instance = new TestApiProxyV2() +}) + +const p = 'parameter' + +describe('With cache', () => { + it('calls original api when parameters are different', async () => { + const spy = jest.spyOn(instance, 'echo') + + const firstValue = await instance.cachedMethod({ p }) + const secondValue = await instance.cachedMethod({ p: 'different value' }) + expect(spy).toHaveBeenCalledTimes(2) + expect(firstValue).not.toEqual(secondValue) + }) + + it('finds cache on second invocation with same parameters', async () => { + const spy = jest.spyOn(instance, 'echo') + + const firstValue = await instance.cachedMethod({ p }) + const secondValue = await instance.cachedMethod({ p }) + + expect(spy).toHaveBeenCalledTimes(1) + expect(firstValue).toEqual(secondValue) + }) + + it('caching works as well for sync methods', () => { + const spy = jest.spyOn(instance, 'echo') + + const firstValue = instance.syncCachedMethod({ p }) + const secondValue = instance.syncCachedMethod({ p }) + + expect(spy).toHaveBeenCalledTimes(1) + expect(firstValue).toEqual(secondValue) + }) + + it('uses custom hash function', async () => { + const spy = jest.spyOn(instance, 'echo') + + const firstValue = await instance.customHashFn({ p }) + const secondValue = await instance.customHashFn({ p: 'something else' }) + + // the custom hash function provided always hash to the same key, so doesn't matter the params, + // the return should always be the result of the first invocation + expect(spy).toHaveBeenCalledTimes(1) + expect(firstValue).toEqual(secondValue) + expect(secondValue).toBe(p) + }) + + it('is able to hash function with flat parameter', () => { + const spy = jest.spyOn(instance, 'echo') + + const firstValue = instance.flatParam(1) + const secondValue = instance.flatParam(2) + + expect(spy).toHaveBeenCalledTimes(2) + expect(firstValue).not.toEqual(secondValue) + + expect(instance.flatParam(1)).toEqual(firstValue) + expect(spy).toHaveBeenCalledTimes(2) + }) + + it('is able to hash function with multiple flat parameters', () => { + const spy = jest.spyOn(instance, 'echo') + + const firstValue = instance.multiFlatParams(1, p) + const secondValue = instance.multiFlatParams(1, 'a') + + expect(spy).toHaveBeenCalledTimes(2) + expect(firstValue).not.toEqual(secondValue) + + expect(instance.multiFlatParams(1, p)).toEqual(firstValue) + expect(spy).toHaveBeenCalledTimes(2) + + expect(instance.multiFlatParams(1, 'a')).toEqual(secondValue) + expect(spy).toHaveBeenCalledTimes(2) + }) +}) + +describe('Without cache', () => { + it('calls original api multiple times when no cache is set', async () => { + const spy = jest.spyOn(instance, 'echo') + + expect(await instance.nonCachedMethod({ p })).toMatch(p) + expect(await instance.nonCachedMethod({ p })).toMatch(p) + expect(spy).toHaveBeenCalledTimes(2) + }) +}) diff --git a/yarn.lock b/yarn.lock index 3dfe8ba27..f0a09e92d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3528,7 +3528,7 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" -clone@^2.0.0, clone@^2.1.1: +clone@2.x, clone@^2.0.0, clone@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= @@ -8521,6 +8521,13 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" +node-cache@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.0.tgz#266786c28dcec0fd34385ee29c383e6d6f1aa5de" + integrity sha512-gFQwYdoOztBuPlwg6DKQEf50G+gkK69aqLnw4djkmlHCzeVrLJfwvg9xl4RCAGviTIMUVoqcyoZ/V/wPEu/VVg== + dependencies: + clone "2.x" + node-fetch@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" From b1be6737385c9002848f84b15da7325e9b5807b2 Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Thu, 16 Jan 2020 12:46:01 +0100 Subject: [PATCH 22/53] fauct doc moved --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5ecb9e4d..34dbb8683 100644 --- a/README.md +++ b/README.md @@ -94,4 +94,4 @@ If you use Visual Studio Code, it's recommended to install [Prettier - Code form ## Testnet faucets In order to get testing tokens, read up the information here: -[faucet](./src/docs/faucet-info.md) +[faucet](./src/faucet-info.md) From 5dd7ef1e321e1e069fb147cf910a2e164ac23ce9 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 16 Jan 2020 14:02:08 +0100 Subject: [PATCH 23/53] Update README.md Co-Authored-By: Dmitry --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34dbb8683..a61068f13 100644 --- a/README.md +++ b/README.md @@ -94,4 +94,4 @@ If you use Visual Studio Code, it's recommended to install [Prettier - Code form ## Testnet faucets In order to get testing tokens, read up the information here: -[faucet](./src/faucet-info.md) +[faucet](./docs/faucet-info.md) From f445567e65a506160191ca4d356df347b10fc13e Mon Sep 17 00:00:00 2001 From: David Date: Thu, 16 Jan 2020 16:57:10 +0100 Subject: [PATCH 24/53] Card component refactor (#419) * styled order rows like wallet page * orderRow try 1 * lock * responsive css edit 2 1. make best fit from current HTML strcture * checkbox -> button when responsive * order image row added * better image row css * hide delete row in responsive * open/close cards * hide full order-details * ts error * CardTable comp for testing * added Approute for testing * OrderRow: Removed unnecessary CSS * Order index: added CardTable instead of prev code * Card: component created 1. CardTable works as html table comp w/set styles in grid * moved styled code into component.styled.ts * DepositWidget: Row 1. changed divs to tds 2. fixed tests * added equal px sizes to REPONSIVE_SIZES * DepositWidget.styled changes + Card.tsx 1. moved core Card css and styled code into Card.ts 2. changed div styles to new td styles == 3. Card.tsx: created + placed core card styles 4. added CardDrawer * DepositWidget/index: added CardTable 1. replaces duped code * moved Form styles/some code into form.styled 1. passed in CardDrawer * fixed broken dark mode changes during merge * removed unnecessary code + logic cleanup * comments + example --- src/App.tsx | 2 + src/components/DepositWidget/Form.styled.ts | 61 ++++ src/components/DepositWidget/Form.tsx | 91 +++-- src/components/DepositWidget/Row.tsx | 28 +- src/components/DepositWidget/Styled.tsx | 226 +----------- src/components/DepositWidget/index.tsx | 30 +- src/components/Layout/Card/Card.tsx | 345 ++++++++++++++++++ src/components/Layout/Card/index.tsx | 1 + .../OrdersWidget/OrderRow.styled.ts | 141 +++++++ src/components/OrdersWidget/OrderRow.tsx | 265 ++------------ .../OrdersWidget/OrdersWidget.styled.ts | 144 ++++++++ src/components/OrdersWidget/index.tsx | 246 +++---------- src/const.ts | 14 +- .../DepositWidget.components.test.tsx | 8 +- 14 files changed, 871 insertions(+), 731 deletions(-) create mode 100644 src/components/DepositWidget/Form.styled.ts create mode 100644 src/components/Layout/Card/Card.tsx create mode 100644 src/components/Layout/Card/index.tsx create mode 100644 src/components/OrdersWidget/OrderRow.styled.ts create mode 100644 src/components/OrdersWidget/OrdersWidget.styled.ts diff --git a/src/App.tsx b/src/App.tsx index d45525fc2..8823c6aa3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ import { walletApi } from 'api' // Global State import { withGlobalContext } from 'hooks/useGlobalState' import { rootReducer, INITIAL_STATE } from 'reducers-actions' +import { Test } from 'components/Layout/Card/Card' const PrivateRoute: React.FC = (props: RouteProps) => { const isConnected = walletApi.isConnected() @@ -72,6 +73,7 @@ const App: React.FC = () => ( + diff --git a/src/components/DepositWidget/Form.styled.ts b/src/components/DepositWidget/Form.styled.ts new file mode 100644 index 000000000..5f0a37013 --- /dev/null +++ b/src/components/DepositWidget/Form.styled.ts @@ -0,0 +1,61 @@ +import styled from 'styled-components' +import { RESPONSIVE_SIZES } from 'const' + +export const WalletDrawerInnerWrapper = styled.div` + display: grid; + grid-template-rows: repeat(2, auto) 1.25rem auto; + justify-content: stretch; + align-items: center; + + font-weight: bolder; + + margin: auto; + padding: 0.375rem; + width: 80%; + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + width: 95%; + } + + p.error { + color: red; + padding: 0 0.5rem 0.5rem; + margin: auto; + } + + div.wallet { + position: relative; + display: grid; + grid-template-columns: minmax(6.3125rem, 7.25rem) minmax(1.5625rem, 0.3fr) minmax(3.375rem, 0.6fr) 4.0625rem; + justify-content: center; + align-items: center; + text-align: center; + &:last-child { + margin: auto; + width: 80%; + text-align: center; + } + > p { + text-align: right; + } + > input { + background-color: #fff; + color: #000; + margin: 0; + width: 100%; + + &:disabled { + background-color: #6f6f6f; + } + } + } + + .buttons { + text-align: center; + padding-top: 1em; + button { + min-width: 7em; + margin-left: 1.2em; + } + } +` diff --git a/src/components/DepositWidget/Form.tsx b/src/components/DepositWidget/Form.tsx index e0dea0782..483746da6 100644 --- a/src/components/DepositWidget/Form.tsx +++ b/src/components/DepositWidget/Form.tsx @@ -3,13 +3,15 @@ import BN from 'bn.js' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { IconDefinition, faSpinner } from '@fortawesome/free-solid-svg-icons' -import { DynamicWrapper, InnerWrapper, LineSeparator } from './Styled' +import { CardDrawer } from 'components/Layout/Card' +import { WalletDrawerInnerWrapper } from './Form.styled' +import { LineSeparator } from './Styled' import useSafeState from 'hooks/useSafeState' +import useScrollIntoView from 'hooks/useScrollIntoView' import { TokenBalanceDetails } from 'types' import { formatAmountFull, parseAmount } from 'utils' -import useScrollIntoView from 'hooks/useScrollIntoView' export interface FormProps { tokenBalances: TokenBalanceDetails @@ -88,54 +90,49 @@ export const Form: React.FC = (props: FormProps) => { } } - const ref = useScrollIntoView() + const ref = useScrollIntoView() return ( - - - - × - -
-

{title}

-
- {/* Withdraw Row */} - - {/* Deposit Row */} -
-

{inputLabel}

- - ): void => setAmountInput(e.target.value)} - placeholder={symbol + ' amount'} - /> -
- {/* Error Message */} -

{errors.amountInput || ''}

- {/* Submit/Cancel Buttons */} -
- Cancel - -
+ cancelForm()} responsive={!!responsive} ref={ref}> +
+

{title}

+ + {/* Withdraw Row */} + + {/* Deposit Row */} +
+

{inputLabel}

+ + ): void => setAmountInput(e.target.value)} + placeholder={symbol + ' amount'} + /> +
+ {/* Error Message */} +

{errors.amountInput || ''}

+ {/* Submit/Cancel Buttons */} +
+ Cancel +
-
- - + +
+ ) } diff --git a/src/components/DepositWidget/Row.tsx b/src/components/DepositWidget/Row.tsx index ea636956c..7ce5ac035 100644 --- a/src/components/DepositWidget/Row.tsx +++ b/src/components/DepositWidget/Row.tsx @@ -5,7 +5,7 @@ import { faSpinner, faCheck, faClock, faPlus, faMinus } from '@fortawesome/free- import Form from './Form' import TokenImg from 'components/TokenImg' -import { RowTokenDiv, RowClaimButton, RowClaimLink } from './Styled' +import { TokenRow, RowClaimButton, RowClaimLink } from './Styled' import useNoScroll from 'hooks/useNoScroll' @@ -54,7 +54,7 @@ export const Row: React.FC = (props: RowProps) => { const [visibleForm, showForm] = useState<'deposit' | 'withdraw' | void>() // Checks innerWidth - const showResponsive = !!innerWidth && innerWidth < RESPONSIVE_SIZES.MOBILE_LARGE + const showResponsive = !!innerWidth && innerWidth < RESPONSIVE_SIZES.MOBILE_LARGE_PX useNoScroll(!!visibleForm && showResponsive) let className @@ -71,15 +71,15 @@ export const Row: React.FC = (props: RowProps) => { return ( <> - -
+ +
{name}
-
-
+ + {formatAmount(totalExchangeBalance, decimals)} -
-
+ + {claimable ? ( <> @@ -107,11 +107,11 @@ export const Row: React.FC = (props: RowProps) => { ) : ( 0 )} -
-
+ + {formatAmount(walletBalance, decimals)} -
-
+ + {enabled ? ( <> )} -
-
+ + {isDepositFormVisible && ( div { - margin-top: 2rem; - } - - span.symbol { - color: #b02ace; - } - - h4 { - margin: 2.5rem 1rem 1rem; - font-size: 1.3em; - font-weight: normal; - text-align: center; - } - - .WalletItemContainer { - display: grid; - grid-template-rows: repeat(2, auto) 1.25rem auto; - justify-content: stretch; - align-items: center; - - font-weight: bolder; - - margin: auto; - padding: 0.375rem; - width: 80%; - - @media only screen and (max-width: ${TABLET}em) { - width: 95%; - } - - p.error { - color: red; - padding: 0 0.5rem 0.5rem; - margin: auto; - } - - div.wallet { - position: relative; - display: grid; - grid-template-columns: minmax(6.3125rem, 7.25rem) minmax(1.5625rem, 0.3fr) minmax(3.375rem, 0.6fr) 4.0625rem; - - justify-content: center; - align-items: center; - text-align: center; - - &:last-child { - margin: auto; - width: 80%; - text-align: center; - } - > p { - text-align: right; - } - > input { - background-color: #fff; - color: #000; - margin: 0; - width: 100%; - - &:disabled { - background-color: #6f6f6f; - } - } - } - - .buttons { - text-align: center; - padding-top: 1em; - button { - min-width: 7em; - margin-left: 1.2em; - } - } - } - - .times { - position: absolute; - display: inline-block; - top: 0.5rem; - right: 0.5rem; - padding: 0 0.5em 0 0; - font-size: 2em; - text-decoration: none; - } -` - -export const DynamicWrapper = styled.div<{ responsive: boolean }>` - display: flex; - justify-content: center; - align-items: center; - - ${(props): string | false => - props.responsive && - ` - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; - background: #000000b5; - z-index: 99; - - > ${InnerWrapper} { - border-radius: var(--border-radius); - } - `} -` - -export const DepositWidgetWrapper = styled.section` - .gridContainer { - display: grid; - width: 100%; - } - - .headerContainer { - display: inherit; - justify-content: center; - align-items: center; - grid-template-columns: var(--grid-row-size-walletPage); - - > div { - color: var(--color-text-deposit-primary); - line-height: 1.5; - font-size: 0.8em; - text-transform: uppercase; - overflow-wrap: break-word; - padding: 0.5em; - font-weight: bolder; - } - - @media only screen and (max-width: ${TABLET}em) { - display: none; - } - } - - .rowContainer { - display: inherit; - grid-template-rows: auto; - } - - .row { - text-align: center; - transition: all 0.5s ease; - } -` export const ModalBodyWrapper = styled.div` div > p { @@ -170,27 +11,9 @@ export const ModalBodyWrapper = styled.div` } ` -export const RowTokenDiv = styled.div` - display: grid; - grid-template-columns: var(--grid-row-size-walletPage); - align-items: center; - justify-content: center; - - background: var(--color-background-pageWrapper); - border: 0.125rem solid transparent; - border-radius: var(--border-radius); - box-shadow: var(--box-shadow); - margin: 0.3rem 0; - - z-index: 1; - transition: all 0.2s ease-in-out; - - &:hover { - background: var(--color-background-selected); - border: 0.125rem solid var(--color-border); - } - - > div { +export const TokenRow = styled.tr` + // Each cell wrapper (td) + > * { margin: 0.1rem; padding: 0.7rem; text-align: center; @@ -221,43 +44,18 @@ export const RowTokenDiv = styled.div` } } - &.highlight { - background-color: var(--color-background-highlighted); - border-bottom-color: #fbdf8f; - } - - &.selected { - background-color: var(--color-button-disabled); - color: #fff; - } - &.loading { background-color: #f7f7f7; border-bottom-color: #b9b9b9; } @media only screen and (max-width: ${TABLET}em) { - grid-template-columns: none; - grid-template-rows: auto; - - align-items: center; - justify-content: stretch; - padding: 0 0.7rem; - - &.selected { - > div { - border-bottom: 0.0625rem solid #ffffff40; - } - } - > div { - display: flex; - flex-flow: row; - align-items: center; - border-bottom: 0.0625rem solid #00000024; - + > td { + // Each item in each cell > * { margin-left: 0.625rem; } + // Token &:first-child { grid-template-columns: 1fr max-content auto; @@ -266,6 +64,7 @@ export const RowTokenDiv = styled.div` margin-right: -0.5rem; } } + // Actions &:last-child { border: none; flex-flow: row nowrap; @@ -275,13 +74,10 @@ export const RowTokenDiv = styled.div` border-radius: 0 var(--border-radius) var(--border-radius); } } - - &::before { - content: attr(data-label); - margin-right: auto; - font-weight: bold; - text-transform: uppercase; - font-size: 0.7rem; + } + &.selected { + > div { + border-bottom: 0.0625rem solid #ffffff40; } } } diff --git a/src/components/DepositWidget/index.tsx b/src/components/DepositWidget/index.tsx index 527233398..4915e9e8d 100644 --- a/src/components/DepositWidget/index.tsx +++ b/src/components/DepositWidget/index.tsx @@ -2,10 +2,10 @@ import React from 'react' import Modali from 'modali' import BN from 'bn.js' +import { CardTable } from 'components/Layout/Card' import { Row } from './Row' import ErrorMsg from 'components/ErrorMsg' import Widget from 'components/Layout/Widget' -import { DepositWidgetWrapper } from './Styled' import { useTokenBalances } from 'hooks/useTokenBalances' import { useRowActions } from './useRowActions' @@ -77,20 +77,22 @@ const DepositWidget: React.FC = () => { } return ( - +
{error ? ( ) : ( -
-
-
Token
-
Exchange wallet
-
Pending withdrawals
-
Wallet
-
Actions
-
-
+ + + + Token + Exchange wallet + Pending withdrawals + Wallet + Actions + + + {balances && balances.map(tokenBalances => ( { {...windowSpecs} /> ))} -
-
+ + )}
- +
) } diff --git a/src/components/Layout/Card/Card.tsx b/src/components/Layout/Card/Card.tsx new file mode 100644 index 000000000..35976452e --- /dev/null +++ b/src/components/Layout/Card/Card.tsx @@ -0,0 +1,345 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import { RESPONSIVE_SIZES } from 'const' +import useWindowSizes from 'hooks/useWindowSizes' + +const CardRowDrawer = styled.tr<{ responsive: boolean }>` + display: flex; + justify-content: center; + align-items: center; + + background-color: transparent; + box-shadow: none; + + // Inner td wrapper + > td { + position: relative; + background-color: var(--color-background-selected-dark); + border-bottom: 0.125rem solid #0000000f; + border-radius: 0 0 var(--border-radius) var(--border-radius); + box-shadow: var(--box-shadow); + width: 80%; + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + width: 95%; + } + + > div { + margin-top: 2rem; + } + + span.symbol { + color: #b02ace; + } + + h4 { + margin: 2.5rem 1rem 1rem; + font-size: 1.3em; + font-weight: normal; + text-align: center; + } + + .times { + } + } + + ${(props): string | false => + props.responsive && + ` + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: #000000b5; + z-index: 99; + + > td { + border-radius: var(--border-radius); + } + `} +` +const CardDrawerCloser = styled.a` + position: absolute; + display: inline-block; + top: 0.5rem; + right: 0.5rem; + padding: 0 0.5em 0 0; + font-size: 2em; + text-decoration: none; +` + +interface CardDrawerProps { + children: React.ReactNode + closeDrawer: () => void + responsive: boolean +} + +export const CardDrawer = React.forwardRef(function ReffedCardDrawer( + { children, closeDrawer, responsive }, + ref, +) { + return ( + + + × + {children} + + + ) +}) + +export const CardTable = styled.table<{ + $bgColor?: string + + $headerGap?: string + $columns?: string + $rows?: string + $gap?: string + $cellSeparation?: string + $rowSeparation?: string + + $align?: string + $justify?: string + + $responsiveCSS?: string + $webCSS?: string +}>` + display: grid; + grid-gap: ${({ $headerGap = '0.3rem' }): string => $headerGap}; + width: 100%; + + > thead, tbody { + > tr:not(${CardRowDrawer}) { + position: relative; + display: grid; + grid-template-columns: ${({ $columns }): string => $columns || `repeat(auto-fit, minmax(3rem, 1fr))`}; + // grid-template-rows + ${({ $rows }): string => ($rows ? `grid-template-rows: ${$rows};` : '')} + // grid-gap + ${({ $gap }): string => ($gap ? `grid-gap: ${$gap};` : '')} + align-items: ${({ $align = 'center' }): string => $align}; + justify-content: ${({ $justify = 'center' }): string => $justify}; + + border-radius: var(--border-radius); + + // How much separation between ROWS + margin: ${({ $rowSeparation = '1rem' }): string => `${$rowSeparation} 0`}; + + text-align: center; + transition: all 0.2s ease-in-out; + z-index: 1; + + &.highlight { + background-color: var(--color-background-highlighted); + border-bottom-color: #fbdf8f; + } + + &.selected { + background-color: var(--color-button-disabled); + color: #fff; + } + + // Separation between CELLS + > * { + margin: ${({ $cellSeparation = '0 0.5rem' }): string => $cellSeparation}; + } + } + + > ${CardRowDrawer} { + > td { + margin-top: ${({ $rowSeparation = '1rem' }): string => `-${Number($rowSeparation.split('rem')[0]) * 2.2}rem`}; + } + } + } + + // Table Header + > thead { + // No styling for table header + > tr { + background-color: transparent; + box-shadow: none; + + > th { + color: var(--color-text-deposit-primary); + line-height: 1.5; + font-size: 0.8em; + text-transform: uppercase; + overflow-wrap: break-word; + padding: 0.5em; + font-weight: bolder; + } + } + } + + // Table Body + tbody { + > tr:not(${CardRowDrawer}) { + background-color: ${({ $bgColor = 'var(--color-background-pageWrapper)' }): string => $bgColor}; + border: 0.125rem solid transparent; + box-shadow: var(--box-shadow); + + > td { + &.cardOpener { + display: none; + } + } + + // Don't highlight on hover selected rows or the drawer + &:not(.selected):not(${CardRowDrawer}):hover { + background: var(--color-background-selected); + border: 0.125rem solid var(--color-border); + } + } + } + + // Top level custom CSS + ${({ $webCSS }): string | undefined => $webCSS} + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + > thead, tbody { + > tr:not(${CardRowDrawer}) { + grid-template-columns: none; + grid-template-rows: auto; + + align-items: center; + justify-content: stretch; + padding: 0 0.7rem; + + > td { + display: flex; + flex-flow: row; + align-items: center; + border-bottom: 0.0625rem solid #00000024; + padding: 0.7rem; + + &:last-child { + border: none; + } + + &.cardOpener { + cursor: pointer; + display: initial; + } + + &::before { + content: attr(data-label); + margin-right: auto; + font-weight: bold; + text-transform: uppercase; + font-size: 0.7rem; + } + } + + &.selected { + > td:not(:last-child) { + border-bottom: 0.0625rem solid #ffffff40; + } + } + } + } + + // Hide Table Header on smaller sizes + > thead { + display: none; + } + + // Top level custom css + ${({ $responsiveCSS }): string | undefined => $responsiveCSS} + } +` + +// TODO: remove all below +const fakeData = [ + { a: 1, b: 2, c: 3, d: 'Open Drawer' }, + { a: 1, b: 2, c: 3, d: 'Open Drawer' }, + { a: 1, b: 2, c: 3, d: 'Open Drawer' }, +] + +const webCSS = ` + background: transparent; + &:not(:first-child) { margin-top: 3rem; } + > tbody, thead { + > tr { + min-height: 3rem; + } + } +` +const responsiveCSS = ` + background: transparent; + color: #000; +` +export const Test: React.FC = () => { + const [openDrawer, setOpenDrawer] = useState(false) + const { innerWidth } = useWindowSizes() + + return ( + + + + One + Two + Three + Action + + + + {fakeData.map((item, index) => ( + + + {item.a} + {item.b} + {item.c} + + + + + {openDrawer && ( + setOpenDrawer(false)} + > + + + + One + Two + Three + Action + + + + {fakeData.map((item, index) => ( + + + {item.a} + {item.b} + {item.c} + Hello World + + + ))} + + + + )} + + ))} + + + ) +} diff --git a/src/components/Layout/Card/index.tsx b/src/components/Layout/Card/index.tsx new file mode 100644 index 000000000..067a7c226 --- /dev/null +++ b/src/components/Layout/Card/index.tsx @@ -0,0 +1 @@ +export * from './Card' diff --git a/src/components/OrdersWidget/OrderRow.styled.ts b/src/components/OrdersWidget/OrderRow.styled.ts new file mode 100644 index 000000000..43bbf63b5 --- /dev/null +++ b/src/components/OrdersWidget/OrderRow.styled.ts @@ -0,0 +1,141 @@ +import styled from 'styled-components' +import { RESPONSIVE_SIZES } from 'const' + +export const OrderRowWrapper = styled.tr<{ $color?: string; $open?: boolean }>` + color: ${({ $color = '' }): string => $color}; + min-height: 4rem; + + .order-image-row { + display: none; + } + + .checked { + > button { + display: none; + justify-content: center; + align-items: center; + + margin: 0 0 0 auto; + > * { + margin: 0 0.5rem; + } + } + } + + .order-details-responsive { + display: none; + } + + .order-details { + display: grid; + grid-template-columns: max-content max-content; + grid-gap: 0 1rem; + text-align: left; + justify-content: space-evenly; + + .order-details-subgrid { + display: grid; + grid-template-columns: min-content minmax(5.6rem, max-content); + grid-gap: 0 0.5rem; + justify-content: space-between; + } + } + + .sub-columns { + display: flex; + flex-flow: row wrap; + justify-content: center; + align-items: center; + + div:first-child { + justify-self: end; + } + + > *:not(:last-child) { + margin: 0 0.3rem; + } + } + + .pendingCell { + place-items: center; + + a { + top: 100%; + position: absolute; + } + } + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + &.selected { + > td { + border-bottom: 0.0625rem solid #ffffff40; + } + } + + // All TR row items + > td { + // Cancel Order Row - shown as button in responsive + &:first-child { + grid-row-start: 6; + + > img { + order: 2; + margin-right: -0.5rem; + } + } + + // First row - TokenA <-> TokenB - visible in Responsive + &.order-image-row { + display: initial; + + > div { + display: flex; + align-items: center; + justify-content: space-evenly; + max-width: 72%; + margin: auto; + + > div { + display: inherit; + justify-content: inherit; + align-items: center; + > * { + margin: 0 0.3rem; + } + } + } + } + + // Hide Web view "Order Details" for condensed responsive version + > .order-details { + display: none; + } + + > .order-details-responsive { + display: flex; + } + + // Web view checkbox for order cancellation + &.checked { + grid-template-columns: 0.5fr 1fr; + justify-self: stretch; + width: auto; + + > button { + display: initial; + } + > input { + display: none; + } + } + + ${(props): string | false => + !props.$open && + ` + &:not(:nth-child(2)):not(:nth-child(3)):not(:last-child) { + display: none; + } + `} + } + } +` diff --git a/src/components/OrdersWidget/OrderRow.tsx b/src/components/OrdersWidget/OrderRow.tsx index 448599a36..67c738141 100644 --- a/src/components/OrdersWidget/OrderRow.tsx +++ b/src/components/OrdersWidget/OrderRow.tsx @@ -1,20 +1,18 @@ import React, { useMemo, useEffect } from 'react' import BigNumber from 'bignumber.js' -import styled from 'styled-components' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faExclamationTriangle, faSpinner, + faTrashAlt, faExchangeAlt, faChevronUp, faChevronDown, - faTrashAlt, } from '@fortawesome/free-solid-svg-icons' import { toast } from 'react-toastify' import Highlight from 'components/Highlight' import { EtherscanLink } from 'components/EtherscanLink' -// import TokenImg from 'components/TokenImg' import { getTokenFromExchangeById } from 'services' import useSafeState from 'hooks/useSafeState' @@ -30,199 +28,17 @@ import { formatPrice, } from 'utils' import { onErrorFactory } from 'utils/onError' -import { MIN_UNLIMITED_SELL_ORDER, RESPONSIVE_SIZES } from 'const' +import { MIN_UNLIMITED_SELL_ORDER } from 'const' import { AuctionElement } from 'api/exchange/ExchangeApi' import TokenImg from 'components/TokenImg' - -export const OrderRowWrapper = styled.div<{ $open?: boolean }>` - display: grid; - grid-template-columns: 5rem minmax(13.625rem, 1fr) repeat(2, minmax(6.2rem, 0.6fr)) 6.5rem; - align-items: center; - justify-content: center; - background: var(--color-background-pageWrapper); - border-radius: var(--border-radius); - box-shadow: var(--box-shadow); - margin: 0.3rem 0; - min-height: 4rem; - text-align: center; - z-index: 1; - transition: all 0.2s ease-in-out; - - > div { - margin: 0 0.85rem; - } - - @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { - grid-template-columns: none; - grid-template-rows: auto; - - align-items: center; - justify-content: stretch; - padding: 0 0.7rem; - - &.selected { - > div { - border-bottom: 0.0625rem solid #ffffff40; - } - } - - > div { - display: flex; - flex-flow: row; - align-items: center; - border-bottom: 0.0625rem solid #00000024; - - margin: 0; - padding: 0.7rem; - - &:first-child { - grid-row-start: 6; - width: 100%; - - > img { - order: 2; - margin-right: -0.5rem; - } - } - - &.order-image-row, - &.cardOpener { - display: initial; - } - - &.order-image-row { - > div { - display: flex; - align-items: center; - justify-content: space-evenly; - max-width: 72%; - margin: auto; - - > div { - display: inherit; - justify-content: inherit; - align-items: center; - > * { - margin: 0 0.3rem; - } - } - } - } - - > .order-details { - display: none; - // grid-template-columns: min-content minmax(2.2rem, max-content); - } - - > .order-details-responsive { - display: flex; - } - - &.checked { - grid-template-columns: 0.5fr 1fr; - - > button { - display: flex; - } - > input { - display: none; - } - } - - &:not(:nth-child(2)):not(:last-child)::before { - content: attr(data-label); - margin-right: auto; - font-weight: bold; - text-transform: uppercase; - font-size: 0.7rem; - } - - ${(props): string | false => - !props.$open && - ` - &:not(:nth-child(2)):not(:nth-child(3)):not(:last-child) { - display: none; - } - `} - } - } - - .cardOpener { - cursor: pointer; - } - - .order-image-row, - .cardOpener { - display: none; - } - - .checked { - > button { - display: none; - justify-content: center; - align-items: center; - - margin: 0 0 0 auto; - > * { - margin: 0 0.5rem; - } - } - } - - .order-details-responsive { - display: none; - } - - .order-details { - display: grid; - grid-template-columns: max-content max-content; - grid-gap: 0 1rem; - text-align: left; - justify-content: space-evenly; - - .order-details-subgrid { - display: grid; - grid-template-columns: min-content minmax(5.6rem, max-content); - grid-gap: 0 0.5rem; - justify-content: space-between; - } - } - - .sub-columns { - display: flex; - flex-flow: row wrap; - justify-content: center; - align-items: center; - - div:first-child { - justify-self: end; - } - - > *:not(:last-child) { - margin: 0 0.3rem; - } - } - - .pendingCell { - place-items: center; - - a { - top: 100%; - position: absolute; - } - } - - &.pending { - color: grey; - } -` +import { OrderRowWrapper } from './OrderRow.styled' const PendingLink: React.FC> = ({ transactionHash }) => { return ( -
+ {transactionHash && view} />} -
+ ) } @@ -230,7 +46,7 @@ const DeleteOrder: React.FC> = ({ isMarkedForDeletion, toggleMarkedForDeletion, pending, disabled }) => ( -
+ Cancel Order -
+ ) function displayTokenSymbolOrLink(token: TokenDetails): React.ReactNode | string { @@ -270,7 +86,7 @@ interface OrderDetailsProps extends Pick { const OrderImage: React.FC> = ({ buyToken, sellToken }) => { return ( -
+
{/* e.g SELL DAI <-> BUY TUSD */}
@@ -284,7 +100,7 @@ const OrderImage: React.FC> =
-
+ ) } @@ -297,7 +113,7 @@ const OrderDetails: React.FC = ({ buyToken, sellToken, order, }, [buyToken, order.priceDenominator, order.priceNumerator, sellToken]) return ( -
+
Sell
@@ -318,7 +134,7 @@ const OrderDetails: React.FC = ({ buyToken, sellToken, order, {displayTokenSymbolOrLink(buyToken)}
-
+ ) } @@ -334,7 +150,7 @@ const UnfilledAmount: React.FC = ({ sellToken, order, pendi const unlimited = order.priceDenominator.gt(MIN_UNLIMITED_SELL_ORDER) return ( -
+ {unlimited ? ( no limit ) : ( @@ -345,7 +161,7 @@ const UnfilledAmount: React.FC = ({ sellToken, order, pendi
)} -
+ ) } @@ -361,7 +177,7 @@ const AccountBalance: React.FC = ({ sellToken, order, isOve const isActive = isOrderActive(order, new Date()) return ( -
+
{accountBalance}
{displayTokenSymbolOrLink(sellToken)} {isOverBalance && isActive && ( @@ -369,7 +185,7 @@ const AccountBalance: React.FC = ({ sellToken, order, isOve
)} -
+ ) } @@ -382,13 +198,13 @@ const Expires: React.FC> = ({ order, pending }) }, [order.validUntil]) return ( -
+ {isNeverExpires ? ( Never ) : ( {expiresOn} )} -
+ ) } @@ -419,9 +235,9 @@ interface ResponsiveRowSizeTogglerProps { const ResponsiveRowSizeToggler: React.FC = ({ handleOpen, openStatus }) => { return ( -
+ -
+ ) } @@ -461,28 +277,27 @@ const OrderRow: React.FC = props => { }, [networkId, order, setBuyToken, setSellToken]) return ( - <> - {sellToken && buyToken && ( - - {pending ? ( - - ) : ( - - )} - - - - - - setOpenCard(!openCard)} openStatus={openCard} /> - - )} - + sellToken && + buyToken && ( + + {pending ? ( + + ) : ( + + )} + + + + + + setOpenCard(!openCard)} openStatus={openCard} /> + + ) ) } diff --git a/src/components/OrdersWidget/OrdersWidget.styled.ts b/src/components/OrdersWidget/OrdersWidget.styled.ts new file mode 100644 index 000000000..a283ba8ab --- /dev/null +++ b/src/components/OrdersWidget/OrdersWidget.styled.ts @@ -0,0 +1,144 @@ +import styled from 'styled-components' +import Widget from 'components/Layout/Widget' +import { RESPONSIVE_SIZES } from 'const' + +export const OrdersWrapper = styled(Widget)` + > a { + margin-bottom: -2em; + } +` + +export const ButtonWithIcon = styled.button` + min-width: 10em; + + > svg { + margin: 0 0.25em; + } +` + +export const CreateButtons = styled.div` + margin-top: 2em; + + // 💙 grid + display: grid; + + &.withOrders { + justify-items: start; + grid-gap: 0.25em 0.75em; + grid: + 'tradeBtn strategyBtn' + '. strategyInfo' + / 1fr 1fr; + + .tradeBtn { + justify-self: end; + } + } + + &.withoutOrders { + // adjust grid layout when no orders + place-items: center; + grid-row-gap: 1em; + grid: + 'noOrdersInfo' + 'tradeBtn' + 'strategyBtn' + 'strategyInfo'; + + button { + // make buttons the same width + width: 15em; + } + } + + .noOrdersInfo { + grid-area: noOrdersInfo; + } + .tradeBtn { + grid-area: tradeBtn; + } + .strategyBtn { + grid-area: strategyBtn; + } + .strategyInfo { + grid-area: strategyInfo; + } + + button { + // resetting button margins to help with alignment + margin: 0; + } +` + +export const OrdersForm = styled.div` + .infoContainer { + display: grid; + grid-template-columns: repeat(2, 1fr); + align-items: center; + + margin: 1em 0; + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + grid-template-columns: 2fr 1fr; + } + + .warning { + justify-self: end; + } + + .countContainer { + display: grid; + grid: 'total active expired'; + align-items: center; + } + + .total { + grid-area: total; + } + .active { + grid-area: active; + } + .expired { + grid-area: expired; + } + } + + .ordersContainer { + display: grid; + } + + .checked { + display: flex; + justify-content: left; + align-items: center; + + > input { + width: auto; + } + } + + .deleteContainer { + display: flex; + flex-direction: column; + align-items: center; + + .hidden { + visibility: hidden; + } + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + display: none; + } + } + + .noOrders { + padding: 3em; + + display: flex; + justify-content: center; + } + + .warning { + color: orange; + } +` diff --git a/src/components/OrdersWidget/index.tsx b/src/components/OrdersWidget/index.tsx index 2fb38b58a..5347537fb 100644 --- a/src/components/OrdersWidget/index.tsx +++ b/src/components/OrdersWidget/index.tsx @@ -1,6 +1,5 @@ import React, { useMemo, useCallback, useEffect } from 'react' import { Link } from 'react-router-dom' -import styled from 'styled-components' import { faExchangeAlt, faTrashAlt, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -12,183 +11,12 @@ import { AuctionElement } from 'api/exchange/ExchangeApi' import { isOrderActive } from 'utils' -import Widget from 'components/Layout/Widget' +import { CardTable } from 'components/Layout/Card' import Highlight from 'components/Highlight' -import OrderRow, { OrderRowWrapper } from './OrderRow' -import { useDeleteOrders } from './useDeleteOrders' -import { RESPONSIVE_SIZES } from 'const' - -const OrdersWrapper = styled(Widget)` - > a { - margin-bottom: -2em; - } -` - -const ButtonWithIcon = styled.button` - min-width: 10em; - - > svg { - margin: 0 0.25em; - } -` - -const CreateButtons = styled.div` - margin-top: 2em; - - // 💙 grid - display: grid; - - &.withOrders { - justify-items: start; - grid-gap: 0.25em 0.75em; - grid: - 'tradeBtn strategyBtn' - '. strategyInfo' - / 1fr 1fr; - - .tradeBtn { - justify-self: end; - } - } - - &.withoutOrders { - // adjust grid layout when no orders - place-items: center; - grid-row-gap: 1em; - grid: - 'noOrdersInfo' - 'tradeBtn' - 'strategyBtn' - 'strategyInfo'; - - button { - // make buttons the same width - width: 15em; - } - } - - .noOrdersInfo { - grid-area: noOrdersInfo; - } - .tradeBtn { - grid-area: tradeBtn; - } - .strategyBtn { - grid-area: strategyBtn; - } - .strategyInfo { - grid-area: strategyInfo; - } - - button { - // resetting button margins to help with alignment - margin: 0; - } -` - -const OrdersForm = styled.div` - .infoContainer { - display: grid; - grid-template-columns: repeat(2, 1fr); - align-items: center; - - margin: 1em 0; - - @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { - grid-template-columns: 2fr 1fr; - } - - .warning { - justify-self: end; - } - - .countContainer { - display: grid; - grid: 'total active expired'; - align-items: center; - } +import OrderRow from './OrderRow' - .total { - grid-area: total; - } - .active { - grid-area: active; - } - .expired { - grid-area: expired; - } - } - - .ordersContainer { - display: grid; - } - - .checked { - // pull checkbox to the left to make divider line be further away - justify-self: left; - display: grid; - grid-template-columns: 1fr 1fr; - justify-content: center; - align-items: center; - gap: 0 0.6rem; - } - - .deleteContainer { - display: flex; - flex-direction: column; - align-items: center; - - .hidden { - visibility: hidden; - } - - @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { - display: none; - } - } - - .noOrders { - padding: 3em; - - display: flex; - justify-content: center; - } - - .warning { - color: orange; - } -` - -const OrdersHeader = styled(OrderRowWrapper)` - background: transparent; - box-shadow: none; - - text-transform: uppercase; - font-weight: bold; - font-size: 0.75em; - - @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { - display: none; - } - - .title { - // create a divider line only bellow titled columns - border-bottom: 0.125rem solid #ededed; - // push the border all the way to the bottom and extend it - place-self: stretch; - - // align that text! - display: flex; - align-items: center; - justify-content: center; - text-align: center; - } - - > * { - // more space for the divider line - padding-bottom: 0.5em; - } -` +import { useDeleteOrders } from './useDeleteOrders' +import { OrdersWrapper, CreateButtons, ButtonWithIcon, OrdersForm } from './OrdersWidget.styled' interface ShowOrdersButtonProps { type: 'active' | 'expired' @@ -333,35 +161,43 @@ const OrdersWidget: React.FC = () => { {shownOrdersCount ? (
- {/* GRID HEADER */} - -
- - All -
-
Order details
-
Unfilled amount
-
Account balance
-
Expires
-
- - {orders.map(order => ( - - ))} + + + + + + All + + Order details + Unfilled amount + Account balance + Expires + + + + {orders.map(order => ( + + ))} + +
diff --git a/src/const.ts b/src/const.ts index 51b8d88d3..f8889d325 100644 --- a/src/const.ts +++ b/src/const.ts @@ -39,14 +39,14 @@ export const LEGALDOCUMENT = { export const RESPONSIVE_SIZES = { // PX SIZES: - // MOBILE_SMALL: 320, - // MOBILE: 500, - // MOBILE_LARGE: 532, - // TABLET: 720, - // TABLET_LARGE: 866, - // WEB_SMALL: 1024, + MOBILE_SMALL_PX: 320, + MOBILE_PX: 500, + MOBILE_LARGE_PX: 532, + TABLET_PX: 720, + TABLET_LARGE_PX: 866, + WEB_SMALL_PX: 1024, - // REM SIZES: + // EM SIZES: MOBILE_SMALL: 20, MOBILE: 31.25, MOBILE_LARGE: 33.25, diff --git a/test/components/DepositWidget.components.test.tsx b/test/components/DepositWidget.components.test.tsx index 0452617b0..bf333f098 100644 --- a/test/components/DepositWidget.components.test.tsx +++ b/test/components/DepositWidget.components.test.tsx @@ -55,9 +55,9 @@ function _createRow(params: Partial = {}, rowProps = fakeRo } describe(' not enabled token', () => { - it('contains 6
elements', () => { + it('contains 5 elements', () => { const wrapper = render(_createRow()) - expect(wrapper.find('div')).toHaveLength(6) + expect(wrapper.find('td')).toHaveLength(5) }) it('contains 1 - - - {openDrawer && ( - setOpenDrawer(false)} - > - - - - One - Two - Three - Action - - - - {fakeData.map((item, index) => ( - - - {item.a} - {item.b} - {item.c} - Hello World - - - ))} - - - - )} - - ))} - - - ) -} From 667fa9edd4eebc3d1dee23ccef030d0c687439d0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 17 Jan 2020 17:43:37 +0300 Subject: [PATCH 26/53] Fix for TrustWallet in-app browser (#417) * getWalletInfo async when sync doesn't work * handle async WalletInfo in hook * extra isConnected check * no redirect before WalletInfo * async Redirect in PrivateRoute * catch provider query error, which happens on provider disconnect sometimes * remove commented out code * useWalletConnection in PrivateRoute * fix linter * Mobile debug console (#418) * make a console component * allow override of log function * add declarations * use Console in development on mobile only * fix linter * move providerState declaration higher * unsubscribe earlier * don't silence errors * Subscription alternative (#423) * alternative subscription -- smart interval check * use subscription helpers --- package.json | 1 + src/App.tsx | 14 +- src/Console.tsx | 120 ++++++++++++++++ src/api/wallet/WalletApi.ts | 151 +++++++++++++++----- src/api/wallet/subscriptionHelpers.ts | 138 ++++++++++++++++++ src/declarations.d.ts | 151 ++++++++++++++++++++ src/hooks/useTokenBalances.ts | 27 ++-- src/hooks/useWalletConnection.ts | 20 ++- src/pages/ConnectWallet.tsx | 5 +- src/utils/miscellaneous.ts | 3 +- yarn.lock | 194 +++++++++++++++++++++++++- 11 files changed, 760 insertions(+), 64 deletions(-) create mode 100644 src/Console.tsx create mode 100644 src/api/wallet/subscriptionHelpers.ts diff --git a/package.json b/package.json index e4ddea570..40d670f6f 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "babel-jest": "^24.9.0", "babel-loader": "^8.0.6", "babel-plugin-styled-components": "^1.10.6", + "console-feed": "^2.8.11", "coveralls": "^3.0.6", "css-loader": "^3.2.0", "dotenv": "^8.1.0", diff --git a/src/App.tsx b/src/App.tsx index d45525fc2..14ad65820 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,16 +23,23 @@ import Wallet from 'pages/Wallet' import SourceCode from 'pages/SourceCode' import NotFound from 'pages/NotFound' import ConnectWallet from 'pages/ConnectWallet' -import { walletApi } from 'api' // Global State import { withGlobalContext } from 'hooks/useGlobalState' import { rootReducer, INITIAL_STATE } from 'reducers-actions' +import Web3Connect from 'web3connect' +import { useWalletConnection } from 'hooks/useWalletConnection' + const PrivateRoute: React.FC = (props: RouteProps) => { - const isConnected = walletApi.isConnected() + const { pending, isConnected } = useWalletConnection() const { component: Component, ...rest } = props + + if (pending) { + return null} /> + } + return ( ( + {process.env.NODE_ENV === 'development' && + Web3Connect.isMobile() && + React.createElement(require('./Console').default)} ) diff --git a/src/Console.tsx b/src/Console.tsx new file mode 100644 index 000000000..a05c208d5 --- /dev/null +++ b/src/Console.tsx @@ -0,0 +1,120 @@ +import React, { useState, useEffect, useRef } from 'react' +import { Hook, Unhook, Console, Message, Decode } from 'console-feed' +import styled from 'styled-components' + +const ConsoleWrapper = styled.div` + position: fixed; + bottom: 0; + right: 0; + background-color: #242424; + max-height: 100%; + width: 100%; + overflow: auto; + z-index: 9999; +` +const ButtonGroup = styled.div` + position: fixed; + bottom: 0; + right: 0; + padding: 2em; + pointer-events: none; + + > * { + pointer-events: initial; + } +` + +const InputGroup = styled.div` + position: sticky; + bottom: 0.4em; + margin: 0; + margin-top: 5px; + display: flex; + + input:focus { + border: inherit; + } + + input { + margin: 0; + border-radius: 0; + } + + button { + margin: 0; + border: none; + border-radius: 0; + } +` + +const ConsoleFrame: React.FC = () => { + const [logs, setLogs] = useState([]) + const [showConsole, setShowConsole] = useState(false) + + const wrapper = useRef(null) + const input = useRef(null) + + useEffect(() => { + const hookedConsole = Hook(window.console, (log: Message) => setLogs(logs => logs.concat(Decode(log)))) + + return (): void => { + Unhook(hookedConsole) + } + }, []) + + const processCommand = (command?: string): void => { + if (!command) return + + if (command.includes('await ')) { + eval(` + (async function() { + return ${command} + })() + `).then(console.log, console.error) + } else { + try { + console.log(eval(command)) + } catch (error) { + console.error(error) + } + } + + if (input.current) input.current.value = '' + setTimeout(() => { + if (wrapper.current) wrapper.current.scrollTop = wrapper.current.scrollHeight + }, 0) + } + + const handleCommand = async (e: React.KeyboardEvent): Promise => { + const command = e.currentTarget.value + if (e.key === 'Enter' && command) { + processCommand(command) + } + } + + const handleClick = (): void => { + if (input.current && input.current.value) { + processCommand(input.current.value) + } + } + + return ( + + {showConsole && ( + <> + + + + + + + )} + + {showConsole && logs.length > 0 && } + + + + ) +} + +export default ConsoleFrame diff --git a/src/api/wallet/WalletApi.ts b/src/api/wallet/WalletApi.ts index 4d1fcb1d3..eb878626e 100644 --- a/src/api/wallet/WalletApi.ts +++ b/src/api/wallet/WalletApi.ts @@ -26,14 +26,16 @@ import { import { log, toBN } from 'utils' import { INFURA_ID } from 'const' +import { subscribeToWeb3Event } from './subscriptionHelpers' + export interface WalletApi { - isConnected(): boolean + isConnected(): boolean | Promise connect(givenProvider?: Provider): Promise disconnect(): Promise getAddress(): Promise getBalance(): Promise getNetworkId(): Promise - getWalletInfo(): WalletInfo + getWalletInfo(): WalletInfo | Promise addOnChangeWalletInfo(callback: (walletInfo: WalletInfo) => void, trigger?: boolean): Command removeOnChangeWalletInfo(callback: (walletInfo: WalletInfo) => void): void getProviderInfo(): ProviderInfo @@ -77,31 +79,62 @@ const subscribeToBlockchainUpdate = ({ }): BlockchainUpdatePromptCallback => { const subs = subscriptions || createSubscriptions(provider) - let networkUpdate: (callback: (chainId: number) => void) => Command + const blockUpdate = (cb: (blockHeader: BlockHeader) => void): Command => { + return subscribeToWeb3Event({ + web3, + interval: 8000, + callback: cb, + getter: web3 => web3.eth.getBlock('latest'), + event: 'newBlockHeaders', + }) + } - if (isMetamaskSubscriptions(subs)) networkUpdate = (cb): Command => subs.onNetworkChanged(networkId => cb(+networkId)) - if (isWalletConnectSubscriptions(subs)) networkUpdate = subs.onChainChanged + let blockchainPrompt: BlockchainUpdatePrompt - const accountsUpdate = subs.onAccountsChanged + const providerState = getProviderState(provider) - const blockUpdate = (cb: (blockHeader: BlockHeader) => void): Command => { - const blockSub = web3.eth.subscribe('newBlockHeaders').on('data', cb) - return (): void => { - blockSub.unsubscribe() + if (providerState) { + const { + accounts: [account], + chainId, + } = providerState + + blockchainPrompt = { + account, + chainId: +chainId, + blockHeader: null, + } + } else { + blockchainPrompt = { + account: '', + chainId: 0, + blockHeader: null, } } - const { - accounts: [account], - chainId, - } = getProviderState(provider) + if (!subs || !providerState) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + + const subscriptionHOC: BlockchainUpdatePromptCallback = callback => { + const unsubBlock = blockUpdate(blockHeader => { + blockchainPrompt = { ...blockchainPrompt, blockHeader } + log('block changed:', blockHeader.number) + callback(blockchainPrompt) + }) + + return unsubBlock + } - let blockchainPrompt: BlockchainUpdatePrompt = { - account, - chainId: +chainId, - blockHeader: null, + return subscriptionHOC } + let networkUpdate: (callback: (chainId: number) => void) => Command + + if (isMetamaskSubscriptions(subs)) networkUpdate = (cb): Command => subs.onNetworkChanged(networkId => cb(+networkId)) + if (isWalletConnectSubscriptions(subs)) networkUpdate = subs.onChainChanged + + const accountsUpdate = subs.onAccountsChanged + const subscriptionHOC: BlockchainUpdatePromptCallback = callback => { const unsubNetwork = networkUpdate(chainId => { blockchainPrompt = { ...blockchainPrompt, chainId } @@ -153,6 +186,10 @@ const closeOpenWebSocketConnection = (web3: Web3): void => { } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isPromise = (maybePromise: any): maybePromise is Promise => + maybePromise instanceof Promise || ('then' in maybePromise && typeof maybePromise.then === 'function') + /** * Basic implementation of Wallet API */ @@ -172,7 +209,7 @@ export class WalletApiImpl implements WalletApi { this._web3 = web3 } - public isConnected(): boolean { + public isConnected(): boolean | Promise { return this._connected } @@ -236,10 +273,10 @@ export class WalletApiImpl implements WalletApi { } public async disconnect(): Promise { - if (isWalletConnectProvider(this._provider) && this._connected) await this._provider.close() - this._unsubscribe() + if (isWalletConnectProvider(this._provider) && (await this._connected)) await this._provider.close() + this._provider = null this._web3?.setProvider(getDefaultProvider()) @@ -248,27 +285,30 @@ export class WalletApiImpl implements WalletApi { } public async getAddress(): Promise { - assert(this._connected, 'The wallet is not connected') + assert(await this._connected, 'The wallet is not connected') return this._user } public async getBalance(): Promise { - assert(this._connected, 'The wallet is not connected') + assert(await this._connected, 'The wallet is not connected') return toBN(await this._balance) } public async getNetworkId(): Promise { - assert(this._connected, 'The wallet is not connected') + assert(await this._connected, 'The wallet is not connected') return this._networkId } public addOnChangeWalletInfo(callback: OnChangeWalletInfo, trigger?: boolean): Command { this._listeners.push(callback) - if (trigger) { - callback(this.getWalletInfo()) + const walletInfo = this.getWalletInfo() + // if walletInfo can only be gotten asynchronously + // trigger callback as soon as it becomes available + if (trigger || isPromise(walletInfo)) { + Promise.resolve(walletInfo).then(callback) } return (): void => this.removeOnChangeWalletInfo(callback) @@ -282,8 +322,12 @@ export class WalletApiImpl implements WalletApi { return Web3Connect.getProviderInfo(this._provider) } - public getWalletInfo(): WalletInfo { - const { isConnected = false, accounts = [], chainId = 0 } = getProviderState(this._provider) || {} + public getWalletInfo(): WalletInfo | Promise { + const providerState = getProviderState(this._provider) + + if (!providerState) return this._getAsyncWalletInfo() + + const { isConnected = false, accounts = [], chainId = 0 } = providerState return { isConnected, userAddress: accounts[0], @@ -293,28 +337,59 @@ export class WalletApiImpl implements WalletApi { /* **************** Private Functions **************** */ + private async _getAsyncWalletInfo(): Promise { + try { + const [[userAddress], networkId] = await Promise.all([this._web3.eth.getAccounts(), this._web3.eth.net.getId()]) + + return { + userAddress, + networkId, + isConnected: !!userAddress && !!networkId, + } + } catch (error) { + log('Error asynchrously getting WalletInfo', error) + return { + userAddress: '', + networkId: 0, + isConnected: false, + } + } + } + private async _notifyListeners(blockchainUpdate?: BlockchainUpdatePrompt): Promise { if (blockchainUpdate) this.blockchainState = blockchainUpdate await Promise.resolve() - const walletInfo: WalletInfo = this.getWalletInfo() + + const walletInfo = await (this.getWalletInfo() || this._getAsyncWalletInfo()) + this._listeners.forEach(listener => listener(walletInfo)) } - private get _connected(): boolean { - return !!(getProviderState(this._provider) || {}).isConnected + private get _connected(): boolean | Promise { + const providerState = getProviderState(this._provider) + + if (providerState) return providerState.isConnected + + return this._getAsyncWalletInfo().then(walletInfo => walletInfo.isConnected) } - private get _user(): string { - const { accounts: [account] = [] } = getProviderState(this._provider) || {} - return account + private get _user(): string | Promise { + const providerState = getProviderState(this._provider) + + if (providerState) return providerState.accounts[0] + + return this._getAsyncWalletInfo().then(walletInfo => walletInfo.userAddress || '') } private get _balance(): Promise { if (!this._web3) return Promise.resolve('0') - return this._web3.eth.getBalance(this._user) + return Promise.resolve(this._user).then(user => this._web3.eth.getBalance(user)) } - private get _networkId(): Network { - const { chainId = 0 } = getProviderState(this._provider) || {} - return chainId + private get _networkId(): Network | Promise { + const providerState = getProviderState(this._provider) + + if (providerState) return providerState.chainId || 0 + + return this._getAsyncWalletInfo().then(walletInfo => walletInfo.networkId || 0) } } diff --git a/src/api/wallet/subscriptionHelpers.ts b/src/api/wallet/subscriptionHelpers.ts new file mode 100644 index 000000000..f80a09a01 --- /dev/null +++ b/src/api/wallet/subscriptionHelpers.ts @@ -0,0 +1,138 @@ +import Web3 from 'web3' +import { Command } from 'types' +import { BlockHeader, Syncing } from 'web3-eth' +import { LogsOptions, Log } from 'web3-core' +import { Subscription } from 'web3-core-subscriptions' + +import { log } from 'utils' + +const DEFAULT_BLOCK_INTERVAL = 10000 //ms + +type SubscribeEvent = 'pendingTransactions' | 'syncing' | 'newBlockHeaders' | 'logs' + +interface SubscribeParams { + web3: Web3 + event: T + logOptions?: T extends 'logs' ? LogsOptions : void + callback: (data: Event2Data[T]) => void +} + +interface Event2Data { + pendingTransactions: string + syncing: Syncing + newBlockHeaders: BlockHeader + logs: Log +} + +// detects if providerr supports any given subscription +const createWeb3Subscription = ({ + web3, + event, + logOptions, + callback, +}: SubscribeParams): Promise<() => void> => { + return new Promise>((resolve, reject) => { + const detectValidSubCb = (e: Error): void => { + if (e) { + reject(e) + return + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const params: [any, any] = (logOptions && event === 'logs' + ? [event, logOptions, detectValidSubCb] + : [event, detectValidSubCb]) as [T, Function] + const sub = web3.eth.subscribe(...params) as Subscription + sub + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + .on('connected', id => { + log('Subscription id for', event, id) + resolve(sub) + }) + .on('error', reject) + }).then(sub => { + sub.on('data', callback) + return (): void => { + sub.unsubscribe() + } + }) +} + +interface SubscribeParamsAlt extends SubscribeParams { + getter(web3: Web3): Promise + interval?: number +} + +interface ConditionalIntervalCheck { + interval?: number + condition(): Promise + callback: (blockNumber: number) => void +} + +// calls callback inly when condition is true +const conditionalIntervalCheck = ({ + interval = DEFAULT_BLOCK_INTERVAL, + condition, + callback, +}: ConditionalIntervalCheck): Command => { + const checkAndCall = async (): Promise => { + const changedBlockState = await condition() + if (changedBlockState.changed) callback(changedBlockState.currentBlockNumber) + } + const intervalId = setInterval(checkAndCall, interval) + // initial call before interval triggers + checkAndCall() + + return (): void => clearInterval(intervalId) +} + +interface ChangedBlockState { + changed: boolean + currentBlockNumber: number + error?: Error +} + +const checkIfBlockChangedFactory = (web3: Web3): (() => Promise) => { + // keep block number in closure + let currentBlockNumber: number + + return async (): Promise => { + try { + // quick check if new block was mined + const blockN = await web3.eth.getBlockNumber() + if (blockN !== currentBlockNumber) { + currentBlockNumber = blockN + return { changed: true, currentBlockNumber } + } + return { changed: false, currentBlockNumber } + } catch (error) { + log('Error getting block number', error) + return { changed: false, currentBlockNumber, error } + } + } +} + +export const subscribeToWeb3Event = (options: SubscribeParamsAlt): Command => { + const { web3, callback, getter, interval } = options + const subscriptionPromise = createWeb3Subscription(options).catch(error => { + // if subscription isn't supported + log('Error subscribing to', options.event, 'event:', error) + + // check on interval + return conditionalIntervalCheck({ + interval, + // factory condition to keep internal state + condition: checkIfBlockChangedFactory(web3), + callback: blockNumber => { + // only called if condition returns true + log('polled new block', blockNumber) + getter(web3).then(callback) + }, + }) + }) + + return (): void => { + subscriptionPromise.then(unsubscribe => unsubscribe()) + } +} diff --git a/src/declarations.d.ts b/src/declarations.d.ts index febba5624..de525ce19 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -1,3 +1,154 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +declare module 'console-feed' { + export type Methods = + | 'log' + | 'debug' + | 'info' + | 'warn' + | 'error' + | 'table' + | 'clear' + | 'time' + | 'timeEnd' + | 'count' + | 'assert' + + export interface LogMessage { + method: Methods + data?: any[] + } + export interface Payload extends LogMessage { + id: string + } + + export interface Message extends Payload { + data: any[] + } + + export type Variants = 'light' | 'dark' + + export interface Styles { + // Log icons + LOG_ICON_WIDTH?: string | number + LOG_ICON_HEIGHT?: string | number + + // Log colors + // LOG_ICON => CSS background-image property + LOG_COLOR?: string + LOG_ICON?: string + LOG_BACKGROUND?: string + LOG_ICON_BACKGROUND_SIZE?: string + LOG_BORDER?: string + + LOG_INFO_COLOR?: string + LOG_INFO_ICON?: string + LOG_INFO_BACKGROUND?: string + LOG_INFO_BORDER?: string + + LOG_COMMAND_COLOR?: string + LOG_COMMAND_ICON?: string + LOG_COMMAND_BACKGROUND?: string + LOG_COMMAND_BORDER?: string + + LOG_RESULT_COLOR?: string + LOG_RESULT_ICON?: string + LOG_RESULT_BACKGROUND?: string + LOG_RESULT_BORDER?: string + + LOG_WARN_COLOR?: string + LOG_WARN_ICON?: string + LOG_WARN_BACKGROUND?: string + LOG_WARN_BORDER?: string + + LOG_ERROR_COLOR?: string + LOG_ERROR_ICON?: string + LOG_ERROR_BACKGROUND?: string + LOG_ERROR_BORDER?: string + + // Fonts + BASE_FONT_FAMILY?: any + BASE_FONT_SIZE?: any + BASE_LINE_HEIGHT?: any + + // Spacing + PADDING?: string + + // react-inspector + BASE_BACKGROUND_COLOR?: any + BASE_COLOR?: any + + OBJECT_NAME_COLOR?: any + OBJECT_VALUE_NULL_COLOR?: any + OBJECT_VALUE_UNDEFINED_COLOR?: any + OBJECT_VALUE_REGEXP_COLOR?: any + OBJECT_VALUE_STRING_COLOR?: any + OBJECT_VALUE_SYMBOL_COLOR?: any + OBJECT_VALUE_NUMBER_COLOR?: any + OBJECT_VALUE_BOOLEAN_COLOR?: any + OBJECT_VALUE_FUNCTION_KEYWORD_COLOR?: any + + HTML_TAG_COLOR?: any + HTML_TAGNAME_COLOR?: any + HTML_TAGNAME_TEXT_TRANSFORM?: any + HTML_ATTRIBUTE_NAME_COLOR?: any + HTML_ATTRIBUTE_VALUE_COLOR?: any + HTML_COMMENT_COLOR?: any + HTML_DOCTYPE_COLOR?: any + + ARROW_COLOR?: any + ARROW_MARGIN_RIGHT?: any + ARROW_FONT_SIZE?: any + + TREENODE_FONT_FAMILY?: any + TREENODE_FONT_SIZE?: any + TREENODE_LINE_HEIGHT?: any + TREENODE_PADDING_LEFT?: any + + TABLE_BORDER_COLOR?: any + TABLE_TH_BACKGROUND_COLOR?: any + TABLE_TH_HOVER_COLOR?: any + TABLE_SORT_ICON_COLOR?: any + TABLE_DATA_BACKGROUND_IMAGE?: any + TABLE_DATA_BACKGROUND_SIZE?: any + + [style: string]: any + } + + interface Props { + logs: Message[] + variant?: Variants + styles?: Styles + filter?: Methods[] + searchKeywords?: string + logFilter?: Function + } + + // export type Console = new () => React.PureComponent + class ConsoleComponent extends React.PureComponent {} + export { ConsoleComponent as Console } + + export interface Storage { + pointers: { + [name: string]: Function + } + src: any + } + + export interface HookedConsole extends Console { + feed: Storage + } + + export type Callback = (encoded: any, message: LogMessage) => void + + export function Hook(console: Console, callback: Callback, encode = true): HookedConsole + + export function Unhook(console: HookedConsole): boolean + + export function Encode(data: any): T + + export function Decode(data: any): Message +} + declare module '@walletconnect/web3-provider' declare module '*.otf' declare module '*.woff' diff --git a/src/hooks/useTokenBalances.ts b/src/hooks/useTokenBalances.ts index fcf940faf..35c116221 100644 --- a/src/hooks/useTokenBalances.ts +++ b/src/hooks/useTokenBalances.ts @@ -90,19 +90,20 @@ export const useTokenBalances = (): UseTokenBalanceResult => { useEffect(() => { // can return NULL (if no address or network) - _getBalances(walletInfo) - .then(balances => { - log( - '[useTokenBalances] Wallet balances', - balances ? balances.map(b => formatAmount(b.walletBalance, b.decimals)) : null, - ) - setBalances(balances) - setError(false) - }) - .catch(error => { - console.error('Error loading balances', error) - setError(true) - }) + walletInfo.isConnected && + _getBalances(walletInfo) + .then(balances => { + log( + '[useTokenBalances] Wallet balances', + balances ? balances.map(b => formatAmount(b.walletBalance, b.decimals)) : null, + ) + setBalances(balances) + setError(false) + }) + .catch(error => { + console.error('Error loading balances', error) + setError(true) + }) }, [setBalances, setError, walletInfo]) return { balances, error } diff --git a/src/hooks/useWalletConnection.ts b/src/hooks/useWalletConnection.ts index 561407f1f..644d496ae 100644 --- a/src/hooks/useWalletConnection.ts +++ b/src/hooks/useWalletConnection.ts @@ -1,14 +1,24 @@ import { walletApi } from 'api' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' import { Command } from 'types' import useSafeState from './useSafeState' -import { WalletInfo } from 'api/wallet/WalletApi' +import { WalletInfo, isPromise } from 'api/wallet/WalletApi' -export const useWalletConnection = (): WalletInfo => { - const [walletInfo, setWalletInfo] = useSafeState(() => walletApi.getWalletInfo()) +const PendingState: { pending: true } & { [K in keyof WalletInfo]: undefined } = { + pending: true, + isConnected: undefined, + userAddress: undefined, + networkId: undefined, +} + +export const useWalletConnection = (): (WalletInfo & { pending: false }) | typeof PendingState => { + const [walletInfo, setWalletInfo] = useSafeState(() => walletApi.getWalletInfo()) useEffect((): Command => { return walletApi.addOnChangeWalletInfo(setWalletInfo) }, [setWalletInfo]) - return walletInfo + + return useMemo(() => { + return isPromise(walletInfo) ? PendingState : { ...walletInfo, pending: false } + }, [walletInfo]) } diff --git a/src/pages/ConnectWallet.tsx b/src/pages/ConnectWallet.tsx index b176f0408..2f39331cc 100644 --- a/src/pages/ConnectWallet.tsx +++ b/src/pages/ConnectWallet.tsx @@ -22,7 +22,10 @@ const IconWallet = styled(FontAwesomeIcon)` const ConnectWallet: React.FC = (props: ConnectWalletProps) => { const { from } = props.location.state || { from: { pathname: '/' } } - const { isConnected } = useWalletConnection() + const { isConnected, pending } = useWalletConnection() + + if (pending) return null + if (isConnected) { return } diff --git a/src/utils/miscellaneous.ts b/src/utils/miscellaneous.ts index cc82ce1a4..d2fdfaff9 100644 --- a/src/utils/miscellaneous.ts +++ b/src/utils/miscellaneous.ts @@ -18,7 +18,8 @@ export function assertNonNull(val: T, message: string): asserts val is NonNul // eslint-disable-next-line function noop(..._args: any[]): void {} -export const log = process.env.NODE_ENV === 'test' ? noop : console.log +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const log = process.env.NODE_ENV === 'test' ? noop : (...args: any[]): void => console.log(...args) export function getToken( key: K, diff --git a/yarn.lock b/yarn.lock index f0a09e92d..dc7512f96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -875,6 +875,18 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@emotion/babel-utils@^0.6.4": + version "0.6.10" + resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc" + integrity sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow== + dependencies: + "@emotion/hash" "^0.6.6" + "@emotion/memoize" "^0.6.6" + "@emotion/serialize" "^0.9.1" + convert-source-map "^1.5.1" + find-root "^1.1.0" + source-map "^0.7.2" + "@emotion/cache@^10.0.27", "@emotion/cache@^10.0.9": version "10.0.27" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.27.tgz#7895db204e2c1a991ae33d51262a3a44f6737303" @@ -911,6 +923,18 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.4.tgz#f14932887422c9056b15a8d222a9074a7dfa2831" integrity sha512-fxfMSBMX3tlIbKUdtGKxqB1fyrH6gVrX39Gsv3y8lRYKUqlgDt3UMqQyGnR1bQMa2B8aGnhLZokZgg8vT0Le+A== +"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6": + version "0.6.6" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44" + integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ== + +"@emotion/is-prop-valid@^0.6.1": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.6.8.tgz#68ad02831da41213a2089d2cab4e8ac8b30cbd85" + integrity sha512-IMSL7ekYhmFlILXcouA6ket3vV7u9BqStlXzbKOF9HBtpUPMMlHU+bBxrLOa2NvleVwNIxeq/zL8LafLbeUXcA== + dependencies: + "@emotion/memoize" "^0.6.6" + "@emotion/is-prop-valid@^0.7.3": version "0.7.3" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz#a6bf4fa5387cbba59d44e698a4680f481a8da6cc" @@ -935,6 +959,11 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== +"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6": + version "0.6.6" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" + integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ== + "@emotion/serialize@^0.11.15": version "0.11.15" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.15.tgz#9a0f5873fb458d87d4f23e034413c12ed60a705a" @@ -946,6 +975,16 @@ "@emotion/utils" "0.11.3" csstype "^2.5.7" +"@emotion/serialize@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145" + integrity sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ== + dependencies: + "@emotion/hash" "^0.6.6" + "@emotion/memoize" "^0.6.6" + "@emotion/unitless" "^0.6.7" + "@emotion/utils" "^0.8.2" + "@emotion/sheet@0.9.4": version "0.9.4" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" @@ -956,16 +995,31 @@ resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== +"@emotion/stylis@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" + integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ== + "@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.0": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== +"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397" + integrity sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg== + "@emotion/utils@0.11.3": version "0.11.3" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== +"@emotion/utils@^0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" + integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw== + "@emotion/weak-memoize@0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" @@ -1837,6 +1891,11 @@ abab@^2.0.0: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + abstract-leveldown@~2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz#1c5e8c6a5ef965ae8c35dfb3a8770c476b82c4b8" @@ -2423,6 +2482,24 @@ babel-plugin-emotion@^10.0.27: find-root "^1.1.0" source-map "^0.5.7" +babel-plugin-emotion@^9.2.11: + version "9.2.11" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728" + integrity sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/babel-utils" "^0.6.4" + "@emotion/hash" "^0.6.2" + "@emotion/memoize" "^0.6.1" + "@emotion/stylis" "^0.7.0" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + find-root "^1.1.0" + mkdirp "^0.5.1" + source-map "^0.5.7" + touch "^2.0.1" + babel-plugin-istanbul@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" @@ -3667,6 +3744,17 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== +console-feed@^2.8.11: + version "2.8.11" + resolved "https://registry.yarnpkg.com/console-feed/-/console-feed-2.8.11.tgz#d858c9a77c00e500b76343661050484720395064" + integrity sha512-XBAM6n08Ft6kofFX3VyMazE3e0R/G5ABvp4vbwinTR+2HTQ7ZJJXXKrK2VPDQm6rkCurRw9tUI4d/N80w5+ZXA== + dependencies: + emotion "^9.1.1" + emotion-theming "^9.0.0" + linkifyjs "^2.1.6" + react-emotion "^9.1.1" + react-inspector "^2.2.2" + constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -3806,6 +3894,26 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" +create-emotion-styled@^9.2.8: + version "9.2.8" + resolved "https://registry.yarnpkg.com/create-emotion-styled/-/create-emotion-styled-9.2.8.tgz#c0050e768ba439609bec108600467adf2de67cc3" + integrity sha512-2LrNM5MREWzI5hZK+LyiBHglwE18WE3AEbBQgpHQ1+zmyLSm/dJsUZBeFAwuIMb+TjNZP0KsMZlV776ufOtFdg== + dependencies: + "@emotion/is-prop-valid" "^0.6.1" + +create-emotion@^9.2.12: + version "9.2.12" + resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-9.2.12.tgz#0fc8e7f92c4f8bb924b0fef6781f66b1d07cb26f" + integrity sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA== + dependencies: + "@emotion/hash" "^0.6.2" + "@emotion/memoize" "^0.6.1" + "@emotion/stylis" "^0.7.0" + "@emotion/unitless" "^0.6.2" + csstype "^2.5.2" + stylis "^3.5.0" + stylis-rule-sheet "^0.0.10" + create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" @@ -3929,7 +4037,7 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" -csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7: +csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.7: version "2.6.8" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA== @@ -4467,6 +4575,21 @@ emojis-list@^2.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= +emotion-theming@^9.0.0: + version "9.2.9" + resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-9.2.9.tgz#2bfd77fdd47d3f5e60d59d97dd4cea4622657220" + integrity sha512-Ncyr1WocmDDrTbuYAzklIUC5iKiGtHy3e5ymoFXcka6SuvZl/EDMawegk4wVp72Agrcm1xemab3QOHfnOkpoMA== + dependencies: + hoist-non-react-statics "^2.3.1" + +emotion@^9.1.1: + version "9.2.12" + resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.12.tgz#53925aaa005614e65c6e43db8243c843574d1ea9" + integrity sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ== + dependencies: + babel-plugin-emotion "^9.2.11" + create-emotion "^9.2.12" + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -6267,6 +6390,11 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^2.3.1: + version "2.5.5" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" + integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== + hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#101685d3aff3b23ea213163f6e8e12f4f111e19f" @@ -6818,6 +6946,14 @@ is-directory@^0.3.1: resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= +is-dom@^1.0.9: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-dom/-/is-dom-1.1.0.tgz#af1fced292742443bb59ca3f76ab5e80907b4e8a" + integrity sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ== + dependencies: + is-object "^1.0.1" + is-window "^1.0.2" + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -7010,6 +7146,11 @@ is-what@^3.3.1: resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.5.0.tgz#c50b0e8f3021e0b39410c159bea43a5510d99027" integrity sha512-00pwt/Jf7IaRh5m2Dp93Iw8LG2cd3OpDj3NrD1XPNUpAWVxPvBP296p4IiGmIU4Ur0f3f56IoIM+fS2pFYF+tQ== +is-window@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" + integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0= + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -7480,6 +7621,11 @@ jest@^24.9.0: import-local "^2.0.0" jest-cli "^24.9.0" +jquery@^3.3.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" + integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== + js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" @@ -7836,6 +7982,15 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +linkifyjs@^2.1.6: + version "2.1.8" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-2.1.8.tgz#2bee2272674dc196cce3740b8436c43df2162f9c" + integrity sha512-j3QpiEr4UYzN5foKhrr9Sr06VI9vSlI4HisDWt+7Mq+TWDwpJ6H/LLpogYsXcyUIJLVhGblXXdUnblHsVNMPpg== + optionalDependencies: + jquery "^3.3.1" + react "^16.4.2" + react-dom "^16.4.2" + load-json-file@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" @@ -8603,6 +8758,13 @@ node-releases@^1.1.29, node-releases@^1.1.42: dependencies: semver "^6.3.0" +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -9715,7 +9877,7 @@ react-dev-utils@^9.0.1: strip-ansi "5.2.0" text-table "0.2.0" -react-dom@^16.8.6: +react-dom@^16.4.2, react-dom@^16.8.6: version "16.12.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11" integrity sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw== @@ -9725,6 +9887,14 @@ react-dom@^16.8.6: prop-types "^15.6.2" scheduler "^0.18.0" +react-emotion@^9.1.1: + version "9.2.12" + resolved "https://registry.yarnpkg.com/react-emotion/-/react-emotion-9.2.12.tgz#74d1494f89e22d0b9442e92a33ca052461955c83" + integrity sha512-qt7XbxnEKX5sZ73rERJ92JMbEOoyOwG3BuCRFRkXrsJhEe+rFBRTljRw7yOLHZUCQC4GBObZhjXIduQ8S0ZpYw== + dependencies: + babel-plugin-emotion "^9.2.11" + create-emotion-styled "^9.2.8" + react-error-overlay@^6.0.3: version "6.0.4" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.4.tgz#0d165d6d27488e660bc08e57bdabaad741366f7a" @@ -9756,6 +9926,15 @@ react-input-autosize@^2.2.2: dependencies: prop-types "^15.5.8" +react-inspector@^2.2.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-2.3.1.tgz#f0eb7f520669b545b441af9d38ec6d706e5f649c" + integrity sha512-tUUK7t3KWgZEIUktOYko5Ic/oYwvjEvQUFAGC1UeMeDaQ5za2yZFtItJa2RTwBJB//NxPr000WQK6sEbqC6y0Q== + dependencies: + babel-runtime "^6.26.0" + is-dom "^1.0.9" + prop-types "^15.6.1" + react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" @@ -9860,7 +10039,7 @@ react-transition-group@^4: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^16.8.6: +react@^16.4.2, react@^16.8.6: version "16.12.0" resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" integrity sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA== @@ -10867,7 +11046,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: +source-map@^0.7.2, source-map@^0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -11499,6 +11678,13 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +touch@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" + integrity sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A== + dependencies: + nopt "~1.0.10" + tough-cookie@^2.3.3, tough-cookie@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" From 45be0463276ed9eba8aa589633ce054a14304564 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 17 Jan 2020 21:50:28 +0100 Subject: [PATCH 27/53] 400/pooling interface step 1 + ProgressBar (#421) * PoolingWidget initial setup code 1. Strategies page exports defaut as page * 1200px and rem equiv to const RESPONSIVE_SIZES * border-radius to PageWrapper * condensed logic * moved styled components into own file * pass stepArray as props * TokenSelector 1. added proper styles as per document 2. passed selectedtokens prop 3. moved styles to file * added progress bar colour var * PoolingWidget.styled 1. added styled componetns: button wrappers, subtext 2. misc css changes * PoolingWidget index 1. Finish ProgressBar logic 2. StepDescription: created 3. StepTitle: created + added subtext 4. BaseComp: set up state, memoised necessary fns 5. SubComponents: comp to select sub comps based on step state * SubComponents: switch instead of ternary * changed selectedTokens back to map and fixed (thanks dima) --- src/components/Layout/PageWrapper.tsx | 1 + .../PoolingWidget/PoolingWidget.styled.ts | 115 ++++++++++ .../PoolingWidget/SubComponents.tsx | 31 +++ .../PoolingWidget/TokenSelector.styled.ts | 56 +++++ .../PoolingWidget/TokenSelector.tsx | 44 ++++ src/components/PoolingWidget/index.tsx | 204 ++++++++++++++++++ src/const.ts | 1 + src/pages/Strategies.tsx | 16 +- src/styles/variables.ts | 2 + 9 files changed, 455 insertions(+), 15 deletions(-) create mode 100644 src/components/PoolingWidget/PoolingWidget.styled.ts create mode 100644 src/components/PoolingWidget/SubComponents.tsx create mode 100644 src/components/PoolingWidget/TokenSelector.styled.ts create mode 100644 src/components/PoolingWidget/TokenSelector.tsx create mode 100644 src/components/PoolingWidget/index.tsx diff --git a/src/components/Layout/PageWrapper.tsx b/src/components/Layout/PageWrapper.tsx index 00b915731..ce593a88e 100644 --- a/src/components/Layout/PageWrapper.tsx +++ b/src/components/Layout/PageWrapper.tsx @@ -2,6 +2,7 @@ import styled from 'styled-components' const PageWrapper = styled.div<{ $bgColor?: string; $boxShadow?: string; $width?: string }>` background-color: ${({ $bgColor = 'var(--color-background-pageWrapper)' }): string => $bgColor}; + border-radius: var(--border-radius); box-shadow: ${({ $boxShadow = 'var(--box-shadow)' }): string => $boxShadow}; margin: auto; diff --git a/src/components/PoolingWidget/PoolingWidget.styled.ts b/src/components/PoolingWidget/PoolingWidget.styled.ts new file mode 100644 index 000000000..3163131bd --- /dev/null +++ b/src/components/PoolingWidget/PoolingWidget.styled.ts @@ -0,0 +1,115 @@ +import styled from 'styled-components' +import PageWrapper from 'components/Layout/PageWrapper' +import { RESPONSIVE_SIZES } from 'const' + +export const PoolingInterfaceWrapper = styled(PageWrapper)` + display: grid; + grid-gap: 1.5rem 0; + + align-items: center; + + > h2 { + margin-right: auto; + } +` + +export const ProgressStep = styled.div<{ $bgColor?: string }>` + display: flex; + align-items: center; + justify-content: center; + + border-radius: 100%; + font-size: xx-large; + font-weight: bolder; + + background: ${({ $bgColor = 'lightgrey' }): string => $bgColor}; +` + +export const ProgressStepText = styled.p<{ $bold: string }>` + font-weight: ${({ $bold }): string => $bold}; + margin: 0; +` + +export const GreySubText = styled.p<{ $justify?: string }>` + display: flex; + align-items: center; + justify-content: ${({ $justify = 'center' }): string => $justify} + + font-size: smaller; + font-style: italic; + + > * { + margin: 0 0.3rem; + } +` + +export const StepSeparator = styled.div<{ $bgColor?: string }>` + align-self: center; + height: 1rem; + + background: ${({ $bgColor = 'lightgrey' }): string => $bgColor}; +` + +export const StepButtonsWrapper = styled.div` + text-align: center; + + > button { + min-width: 8rem; + } +` + +export const BarWrapper = styled.div<{ $bgColor?: string; $minHeight?: string }>` + display: flex; + flex-flow: row nowrap; + align-items: stretch; + justify-content: space-evenly; + + margin: 0 auto; + min-height: ${({ $minHeight = '5vw' }): string => $minHeight}; + min-width: 35vw; + + > * { + display: flex; + flex: 1; + + text-align: center; + } + + > p, + ${StepSeparator} { + margin: 0 -0.1rem; + white-space: nowrap; + } + + > ${ProgressStep}, ${StepSeparator}, ${ProgressStepText}, ${StepButtonsWrapper} { + transition: all 0.7s ease-in-out; + } + + @media only screen and (max-width: ${RESPONSIVE_SIZES.WEB}em) { + min-height: 7.143vw; + min-width: 50vw; + + > p { + white-space: normal; + } + } +` + +export const StepDescriptionWrapper = styled.div` + align-self: left; + + > ul { + list-style: none; + padding-inline-start: 2rem; + + > li { + > svg { + color: green; + } + + > * { + margin-right: 0.5rem; + } + } + } +` diff --git a/src/components/PoolingWidget/SubComponents.tsx b/src/components/PoolingWidget/SubComponents.tsx new file mode 100644 index 000000000..1c5226717 --- /dev/null +++ b/src/components/PoolingWidget/SubComponents.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +import TokenSelector from './TokenSelector' +import { TokenSelectorProps } from './TokenSelector' + +interface SubComponentProps extends TokenSelectorProps /* , All Other Steps */ { + step: number +} + +const SubComponents: React.FC = props => { + const { step, handleTokenSelect, selectedTokensMap, tokens } = props + + switch (step) { + case 1: + return ( + + ) + case 2: + return
Step 2
+ case 3: + return
Step 3
+ case 4: + return
Step 4
+ default: + return ( + + ) + } +} + +export default SubComponents diff --git a/src/components/PoolingWidget/TokenSelector.styled.ts b/src/components/PoolingWidget/TokenSelector.styled.ts new file mode 100644 index 000000000..a3e19877e --- /dev/null +++ b/src/components/PoolingWidget/TokenSelector.styled.ts @@ -0,0 +1,56 @@ +import styled from 'styled-components' + +export const TokenSelectorWrapper = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + grid-gap: 1rem; + + justify-self: center; + width: 80%; +` + +export const CheckboxWrapper = styled.div` + position: absolute; + right: 0; + top: 0; + color: green; + font-weight: bolder; + font-size: 1.3rem; +` + +export const TokenBox = styled.div<{ $selected: boolean }>` + position: relative; + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; + + background: var(--color-background-pageWrapper); + box-shadow: ${({ $selected }): string => ($selected ? '0 0 0 0.175rem green' : 'var(--box-shadow)')}; + border: ${({ $selected }): string => + `0.05rem solid ${$selected ? 'transparent' : 'var(--color-background-selected-dark)'}`}; + border-radius: var(--border-radius); + cursor: pointer; + min-height: 6rem; + + &:hover { + background: var(--color-background); + } + + > * { + margin: 0 0.5rem; + } + + > img { + height: auto; + width: 2.3rem; + } + + > ${CheckboxWrapper} { + margin: 0.2rem 0.4rem; + opacity: ${({ $selected }): string => ($selected ? '1' : '0')}; + transition: inherit; + } + + transition: all 0.2s ease-in; +` diff --git a/src/components/PoolingWidget/TokenSelector.tsx b/src/components/PoolingWidget/TokenSelector.tsx new file mode 100644 index 000000000..77b83c57c --- /dev/null +++ b/src/components/PoolingWidget/TokenSelector.tsx @@ -0,0 +1,44 @@ +import React from 'react' + +import TokenImg from 'components/TokenImg' +import { ProgressStepText } from './PoolingWidget.styled' +import { TokenSelectorWrapper, TokenBox, CheckboxWrapper } from './TokenSelector.styled' + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheckCircle } from '@fortawesome/free-solid-svg-icons' + +import { TokenDetails } from '@gnosis.pm/dex-js' + +export interface TokenSelectorProps { + handleTokenSelect: (tokenData: TokenDetails) => void + selectedTokensMap: Map + tokens: TokenDetails[] +} + +const TokenSelector: React.FC = ({ handleTokenSelect, selectedTokensMap, tokens }) => { + return ( + + {tokens.map(tokenDetails => { + const { name, symbol, address, id, image } = tokenDetails + return ( + handleTokenSelect(tokenDetails)} + $selected={selectedTokensMap.has(id)} + > + + + + +
+ {symbol} + {name} +
+
+ ) + })} +
+ ) +} + +export default TokenSelector diff --git a/src/components/PoolingWidget/index.tsx b/src/components/PoolingWidget/index.tsx new file mode 100644 index 000000000..441efb740 --- /dev/null +++ b/src/components/PoolingWidget/index.tsx @@ -0,0 +1,204 @@ +import React, { useCallback, useMemo } from 'react' + +import SubComponents from './SubComponents' +import Widget from 'components/Layout/Widget' +import { + BarWrapper, + StepSeparator, + PoolingInterfaceWrapper, + ProgressStep, + ProgressStepText, + StepDescriptionWrapper, + StepButtonsWrapper, + GreySubText, +} from './PoolingWidget.styled' + +import { faCheckCircle } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +import useSafeState from 'hooks/useSafeState' +import { useWalletConnection } from 'hooks/useWalletConnection' + +import { tokenListApi } from 'api' + +import { TokenDetails } from '@gnosis.pm/dex-js' +import { Network } from 'types' + +interface ProgressBarProps { + step: number + stepArray: string[] +} + +const stepChecker = (step: number, index: number): boolean => step >= index + 1 && step <= 4 + +const ProgressBar: React.FC = ({ step, stepArray }) => { + return ( + <> + + {stepArray.map((stepName, index) => ( + + + {index + 1} + + {index + 1 < 4 && ( + + )} + + ))} + + + {stepArray.map((stepName, index) => ( + + {stepName} + {index + 1 < 4 &&

} + + ))} + + + ) +} + +const StepDescription: React.FC = () => ( + +

Setup your strategy once and allow your funds to be traded on your behalf.

+
    +
  • + + No maintenance needed +
  • +
  • + + No gas costs for trades +
  • +
  • + + Cancellation possible at any time +
  • +
+ Learn more about strategies + +) + +const StepTitle: React.FC> = ({ step }) => { + const { title, subtext }: { title: string; subtext?: string } = useMemo(() => { + switch (step) { + case 1: + return { + title: '1. Select your trusted stablecoins', + subtext: + 'Select two or more stablecoins you want to include in your liquidity provision and you believe are worth $1', + } + case 2: + return { title: '2. Define your spread', subtext: '' } + case 3: + return { title: '3. Create strategy', subtext: '' } + case 4: + return { title: '4. Add funding', subtext: '' } + default: + return { title: 'An error occurred, please try again' } + } + }, [step]) + + return ( +
+ + {title} + + {subtext && {subtext}} +
+ ) +} + +// function addRemoveItem(arr: number[], newItem: number): number[] { +// if (!arr.includes(newItem)) return arr.concat(newItem) + +// return arr.reduce((acc: number[], item) => { +// if (item === newItem) return acc + +// const newAcc = acc.concat(item) +// return newAcc +// }, []) +// } + +function addRemoveMapItem(map: Map, newToken: TokenDetails): Map { + // Cache map (no mutate) + const copyMap = new Map(map) + // Map item doesn't exist? Add that fool in + if (!copyMap.get(newToken.id)) return copyMap.set(newToken.id, newToken) + // Else remove that b + copyMap.delete(newToken.id) + return copyMap +} + +const PoolingInterface: React.FC = () => { + const [step, setStep] = useSafeState(1) + const [selectedTokensMap, setSelectedTokensMap] = useSafeState>(new Map()) + + const { networkId } = useWalletConnection() + // Avoid displaying an empty list of tokens when the wallet is not connected + const fallBackNetworkId = networkId ? networkId : Network.Mainnet // fallback to mainnet + + // TODO: switched to tagged tokens @anxo @leandro + const tokens = useMemo(() => tokenListApi.getTokens(fallBackNetworkId).filter(({ symbol }) => symbol !== 'WETH'), [ + fallBackNetworkId, + ]) + + const prevStep = (): void => { + if (step == 1) return + + return setStep(step - 1) + } + const nextStep = (): void => { + if (step == 4) return + + return setStep(step + 1) + } + + const handleTokenSelect = useCallback( + (token: TokenDetails): void => { + const state = addRemoveMapItem(selectedTokensMap, token) + return setSelectedTokensMap(state) + }, + [selectedTokensMap, setSelectedTokensMap], + ) + + const restProps = useMemo( + () => ({ + handleTokenSelect, + tokens, + selectedTokensMap, + }), + [handleTokenSelect, selectedTokensMap, tokens], + ) + + return ( + + +

New Strategy

+ + + + + {/* Main Components here */} + + + + + Please select at least two tokens to continue{' '} + {selectedTokensMap.size >= 2 && } + + + + +
+
+ ) +} + +export default PoolingInterface diff --git a/src/const.ts b/src/const.ts index f8889d325..a0cdc53d4 100644 --- a/src/const.ts +++ b/src/const.ts @@ -53,6 +53,7 @@ export const RESPONSIVE_SIZES = { TABLET: 45, TABLET_LARGE: 54.125, WEB_SMALL: 64, + WEB: 75, } export const DEFAULT_DECIMALS = 4 diff --git a/src/pages/Strategies.tsx b/src/pages/Strategies.tsx index 1878f2062..e862a25e0 100644 --- a/src/pages/Strategies.tsx +++ b/src/pages/Strategies.tsx @@ -1,15 +1 @@ -import React from 'react' -import PageWrapper from 'components/Layout/PageWrapper' - -const Strategies: React.FC = () => ( - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum -

-
-) - -export default Strategies +export { default } from 'components/PoolingWidget' diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 22738bad9..719fa0a68 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -10,6 +10,7 @@ const LightColors = ` --color-background-selected: ##d9d9d9; --color-background-selected-darker: #b6b6b6; --color-background-selected-dark: #bfbfbf; + --color-background-progressBar: lightskyblue; // Borders --color-border: transparent; @@ -46,6 +47,7 @@ const DarkColors = ` --color-background-selected: ##d9d9d9; --color-background-selected-darker: #b6b6b6; --color-background-selected-dark: #2a2d2f; + --color-background-progressBar: #4338b5; // Borders --color-border: #262626; From ee13a2dca78a0b0769a7a069ad2ff6aa52ac3327 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2020 02:34:24 +0000 Subject: [PATCH 28/53] Bump @types/styled-components from 4.4.1 to 4.4.2 Bumps [@types/styled-components](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/styled-components) from 4.4.1 to 4.4.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/styled-components) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index dc7512f96..fa31ef36e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1595,9 +1595,9 @@ integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== "@types/styled-components@^4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.1.tgz#bc40cf5ce0708032f4b148b04ab3c470d3e74026" - integrity sha512-cQXT4pkAkM0unk/s26UBrJx9RmJ2rNUn2aDTgzp1rtu+tTkScebE78jbxNWhlqkA43XF3d41CcDlyl9Ldotm2g== + version "4.4.2" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.2.tgz#709fa7afd7dc0963b8316a0159240f0fe19a026d" + integrity sha512-dngFx2PuGoy0MGE68eHayAmJvLSqWrnTe9w+DnQruu8PS+waWEsKmoBRhkzL2h2pK1OJhzJhVfuiz+oZa4etpA== dependencies: "@types/hoist-non-react-statics" "*" "@types/react" "*" From 880f70edfdcf856fccbc58b11217dc4aec7f5865 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2020 02:34:56 +0000 Subject: [PATCH 29/53] Bump typescript from 3.7.4 to 3.7.5 Bumps [typescript](https://github.com/Microsoft/TypeScript) from 3.7.4 to 3.7.5. - [Release notes](https://github.com/Microsoft/TypeScript/releases) - [Commits](https://github.com/Microsoft/TypeScript/compare/v3.7.4...v3.7.5) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index fa31ef36e..37cb75333 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11822,9 +11822,9 @@ typedarray@^0.0.6: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= typescript@^3.7.2: - version "3.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19" - integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw== + version "3.7.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" + integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== uglify-js@^3.1.4: version "3.7.3" From 5bfb0fc2e6688babf31c37373d4ec0c115f37664 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2020 02:35:21 +0000 Subject: [PATCH 30/53] Bump ts-jest from 24.2.0 to 24.3.0 Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 24.2.0 to 24.3.0. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v24.2.0...v24.3.0) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 37cb75333..295fd20e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11719,9 +11719,9 @@ tryer@^1.0.1: integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== ts-jest@^24.0.2: - version "24.2.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.2.0.tgz#7abca28c2b4b0a1fdd715cd667d65d047ea4e768" - integrity sha512-Yc+HLyldlIC9iIK8xEN7tV960Or56N49MDP7hubCZUeI7EbIOTsas6rXCMB4kQjLACJ7eDOF4xWEO5qumpKsag== + version "24.3.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869" + integrity sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ== dependencies: bs-logger "0.x" buffer-from "1.x" From d52f3ee04de43bd6482c844c8d084adee59a7d91 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2020 02:35:50 +0000 Subject: [PATCH 31/53] Bump css-loader from 3.4.1 to 3.4.2 Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 3.4.1 to 3.4.2. - [Release notes](https://github.com/webpack-contrib/css-loader/releases) - [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/css-loader/compare/v3.4.1...v3.4.2) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 295fd20e0..1962482bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3979,9 +3979,9 @@ css-color-keywords@^1.0.0: integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= css-loader@^3.2.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.4.1.tgz#dfb7968aa9bffb26bd20375afdffe77d5a234b77" - integrity sha512-+ybmv7sVxxNEenQhkifQDvny/1iNQM7YooJbSfVUdQQvisyg1aKIqgGjCjoFSyVLJMp17z9rfZFQaR5HGHcMbw== + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.4.2.tgz#d3fdb3358b43f233b78501c5ed7b1c6da6133202" + integrity sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA== dependencies: camelcase "^5.3.1" cssesc "^3.0.0" From 3f5c78486e888e4a43b617204b24b98fedb99254 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2020 02:36:18 +0000 Subject: [PATCH 32/53] Bump @walletconnect/web3-provider from 1.0.0-beta.42 to 1.0.0-beta.44 Bumps [@walletconnect/web3-provider](https://github.com/walletconnect/walletconnect-monorepo) from 1.0.0-beta.42 to 1.0.0-beta.44. - [Release notes](https://github.com/walletconnect/walletconnect-monorepo/releases) - [Commits](https://github.com/walletconnect/walletconnect-monorepo/compare/1.0.0-beta.42...1.0.0-beta.44) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 64 +++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1962482bf..60eaa8ed6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1659,55 +1659,55 @@ semver "^6.3.0" tsutils "^3.17.1" -"@walletconnect/browser@^1.0.0-beta.42": - version "1.0.0-beta.42" - resolved "https://registry.yarnpkg.com/@walletconnect/browser/-/browser-1.0.0-beta.42.tgz#913da3af0704dd5c9b8b881c593793275cd18e56" - integrity sha512-LbJnhiLXOrAIqu/TMNYO7iAX0rOuhkx9Wsy7kmPUXqUH5u/oiBH9+b9JU/hI/as66xM+90LMbxHKIz9yxPfuTA== +"@walletconnect/browser@^1.0.0-beta.44": + version "1.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@walletconnect/browser/-/browser-1.0.0-beta.44.tgz#0c484e61c4e00003d2d787f8f1f87345cea9f5c1" + integrity sha512-216TIe2DJBQlaHQajpfhuiMsDyCfmYprQ6ofSQkg0KMZTvHYHetAEVjxEydk+8sJ+rFb/SR4P11rb+BKvWnGxA== dependencies: - "@walletconnect/core" "^1.0.0-beta.42" - "@walletconnect/types" "^1.0.0-beta.42" - "@walletconnect/utils" "^1.0.0-beta.42" + "@walletconnect/core" "^1.0.0-beta.44" + "@walletconnect/types" "^1.0.0-beta.44" + "@walletconnect/utils" "^1.0.0-beta.44" -"@walletconnect/core@^1.0.0-beta.42": - version "1.0.0-beta.42" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.0.0-beta.42.tgz#cb9b098590255577571a90b9bda83a0f920020d8" - integrity sha512-Q7pDpTj2/bm7rkAbI+5/mf7SdurLXDfznjvE6jZGg8o23wsYamnkwLnqVMkqeFyUjY7bpHxWqakY813wmmlrZw== +"@walletconnect/core@^1.0.0-beta.44": + version "1.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.0.0-beta.44.tgz#49aca85ac46ea7b554fa7be20c15d83869714315" + integrity sha512-2Ok3ozDkYBy4jgHLqXz4FBqJpKuvQWGB6PwpDwmYwMygAXjAjfz0SrJEEII+39rhU9aofBKL8FrjS2Z1Vggziw== dependencies: - "@walletconnect/types" "^1.0.0-beta.42" - "@walletconnect/utils" "^1.0.0-beta.42" + "@walletconnect/types" "^1.0.0-beta.44" + "@walletconnect/utils" "^1.0.0-beta.44" -"@walletconnect/qrcode-modal@^1.0.0-beta.42": - version "1.0.0-beta.42" - resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.0.0-beta.42.tgz#9e3f7f4889868ff8e09be2d760d85bea65d9cbf6" - integrity sha512-iO5Ilx4OngORX6rWvM4JvRG3LpuUBRhPHeQUbI6fjATlmn4FXG5jR3OOG7mha1+R82PG+IhBOiuOe7pKWb5lJw== +"@walletconnect/qrcode-modal@^1.0.0-beta.44": + version "1.0.0-beta.45" + resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.0.0-beta.45.tgz#37510dc671191c136b029e41b16a472178aa6899" + integrity sha512-rxkj+DuJOOVR7Y7RiN0LcwA7IhLtcZdyrp5Xt3aJWLzPLz3n12/8LqaNWlfeLodek40VyyhbuX9PCQ/2BwSYsA== dependencies: qr-image "^3.2.0" qrcode-terminal "^0.12.0" -"@walletconnect/types@^1.0.0-beta.42": - version "1.0.0-beta.42" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.0.0-beta.42.tgz#1a0db31b37b7765985f78e2b0f2942407685cdcf" - integrity sha512-YBpcYdYGEacOob5VF0/8LRGz/uGynFcwoj9RIEOkDl9JqGQMaXhlZ1M48AfEp8NVQvXRF+cSc3moPDwu3+9USw== +"@walletconnect/types@^1.0.0-beta.44": + version "1.0.0-beta.45" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.0.0-beta.45.tgz#0ee4c78dd8e469eb2152ebaffb5e1dcee78deb84" + integrity sha512-Vm/uPBNl8AWs54M/Z71tQi56IqEqNHejLm1wd4pYEYfcA1YxR76vguQt+q+aJMEyN5JYWLBbd5zqOSIuJuvNwA== -"@walletconnect/utils@^1.0.0-beta.42": - version "1.0.0-beta.42" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.0.0-beta.42.tgz#60304d0ed902a532ac9e535905b5a1c4c8063069" - integrity sha512-/eLcm+tEcvqlYaEaG/L7c+3uNrugZs3a30bwYUbNvlmzYWTNBzU/EV18CGavlZLc/Pb2XtPXevGtDsLlvtR7/Q== +"@walletconnect/utils@^1.0.0-beta.44": + version "1.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.0.0-beta.44.tgz#c5a12143479e88eed03e2e1c0a72057d5c609ce4" + integrity sha512-bJXatlP/fpwPLR8hiFdO1X5vUD7K3bU4j4Al87923yq50bS/ujVC+9ceJw0ue7Kfty4xWubIOtI2GmVAcBZROg== dependencies: "@ethersproject/address" "^5.0.0-beta.125" "@ethersproject/bytes" "^5.0.0-beta.126" "@ethersproject/strings" "^5.0.0-beta.125" - "@walletconnect/types" "^1.0.0-beta.42" + "@walletconnect/types" "^1.0.0-beta.44" bignumber.js "^8.1.1" "@walletconnect/web3-provider@^1.0.0-beta.42": - version "1.0.0-beta.42" - resolved "https://registry.yarnpkg.com/@walletconnect/web3-provider/-/web3-provider-1.0.0-beta.42.tgz#9510dd7e155ff267ddaa0a97fdc5128c03540654" - integrity sha512-roW7PMaTHP8Ahzl9wM4VLjIfsQ3ippEUka2J6AFzAtFyIMffKw1QnhkG5cr1QgXgy4uI0B63f3pnoKT+eqQL/g== + version "1.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@walletconnect/web3-provider/-/web3-provider-1.0.0-beta.44.tgz#2d565c99930d8831f2a5ab30e5e361d72fdbb3a1" + integrity sha512-jCrDIorSnkEBqY4j9AWvva0RnvvdnV8WJD18hu4xQJ8KStxrRs5lh+GKsMNHoMcVbOioUTHzXf4KNZQP0Ro6WA== dependencies: - "@walletconnect/browser" "^1.0.0-beta.42" - "@walletconnect/qrcode-modal" "^1.0.0-beta.42" - "@walletconnect/types" "^1.0.0-beta.42" + "@walletconnect/browser" "^1.0.0-beta.44" + "@walletconnect/qrcode-modal" "^1.0.0-beta.44" + "@walletconnect/types" "^1.0.0-beta.44" web3-provider-engine "github:walletconnect/web3-provider-engine" xhr2-cookies "^1.1.0" From 0be64c12704a9d2ea176a347837a594e565c116c Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Mon, 20 Jan 2020 08:57:09 -0800 Subject: [PATCH 33/53] Adding implementation for placeValidFromOrders to ExchangeApi (#420) * Adding implementation for placeValidFromOrders to ExchangeApi * Refactoring simplifying map to string --- src/api/exchange/ExchangeApi.ts | 60 +++++++++++++++++- src/api/exchange/ExchangeApiMock.ts | 37 +++++++++++ test/api/ExchangeApi/ExchangeApiMock.test.ts | 66 ++++++++++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) diff --git a/src/api/exchange/ExchangeApi.ts b/src/api/exchange/ExchangeApi.ts index b23850782..8d10d3229 100644 --- a/src/api/exchange/ExchangeApi.ts +++ b/src/api/exchange/ExchangeApi.ts @@ -1,8 +1,9 @@ import BN from 'bn.js' +import Web3 from 'web3' + import { DepositApiImpl, DepositApi, InjectedDependencies } from 'api/deposit/DepositApi' import { Receipt, TxOptionalParams } from 'types' -import { log } from 'utils' -import Web3 from 'web3' +import { log, assert } from 'utils' import { decodeAuctionElements } from './utils/decodeAuctionElements' interface BaseParams { @@ -39,6 +40,16 @@ export interface PlaceOrderParams extends BaseParams, WithTxOptionalParams { sellAmount: BN } +export interface PlaceValidFromOrdersParams extends BaseParams, WithTxOptionalParams { + userAddress: string + buyTokens: number[] + sellTokens: number[] + validFroms: number[] + validUntils: number[] + buyAmounts: BN[] + sellAmounts: BN[] +} + export interface CancelOrdersParams extends BaseParams, WithTxOptionalParams { userAddress: string orderIds: number[] @@ -55,6 +66,7 @@ export interface ExchangeApi extends DepositApi { addToken(params: AddTokenParams): Promise placeOrder(params: PlaceOrderParams): Promise + placeValidFromOrders(params: PlaceValidFromOrdersParams): Promise cancelOrders(params: CancelOrdersParams): Promise } @@ -169,6 +181,50 @@ export class ExchangeApiImpl extends DepositApiImpl implements ExchangeApi { return tx } + public async placeValidFromOrders({ + userAddress, + networkId, + buyTokens, + sellTokens, + validFroms, + validUntils, + buyAmounts, + sellAmounts, + txOptionalParams, + }: PlaceValidFromOrdersParams): Promise { + const length = buyTokens.length + assert( + [sellTokens, validFroms, validUntils, buyAmounts, sellAmounts].every(el => el.length === length), + 'Parameters length do not match', + ) + assert(length > 0, 'At least one order required') + + const contract = await this._getContract(networkId) + + const buyAmountsStr = buyAmounts.map(String) + const sellAmountsStr = sellAmounts.map(String) + + const tx = contract.methods + .placeValidFromOrders(buyTokens, sellTokens, validFroms, validUntils, buyAmountsStr, sellAmountsStr) + .send({ from: userAddress, gasPrice: await this.fetchGasPrice() }) + + if (txOptionalParams?.onSentTransaction) { + tx.once('transactionHash', txOptionalParams.onSentTransaction) + } + + log( + `[ExchangeApiImpl] Placed multiple orders for user ${userAddress} with the following params:\n + buyTokens: ${buyTokens}\n + sellTokens: ${sellTokens}\n + validFroms: ${validFroms}\n + validUntils: ${validUntils}\n + buyAmounts: ${buyAmountsStr}\n + sellAmounts: ${sellAmountsStr}`, + ) + + return tx + } + public async cancelOrders({ userAddress, orderIds, diff --git a/src/api/exchange/ExchangeApiMock.ts b/src/api/exchange/ExchangeApiMock.ts index 88f02a735..24c68ae79 100644 --- a/src/api/exchange/ExchangeApiMock.ts +++ b/src/api/exchange/ExchangeApiMock.ts @@ -17,6 +17,7 @@ import { GetOrdersParams, GetTokenAddressByIdParams, GetTokenIdByAddressParams, + PlaceValidFromOrdersParams, } from './ExchangeApi' import { Erc20Api } from 'api/erc20/Erc20Api' @@ -117,6 +118,42 @@ export class ExchangeApiMock extends DepositApiMock implements ExchangeApi { return RECEIPT } + public async placeValidFromOrders({ + userAddress, + buyTokens, + sellTokens, + validFroms, + validUntils, + buyAmounts, + sellAmounts, + txOptionalParams, + }: PlaceValidFromOrdersParams): Promise { + const length = buyTokens.length + assert( + [sellTokens, validFroms, validUntils, buyAmounts, sellAmounts].every(el => el.length === length), + 'Parameters length do not match', + ) + assert(length > 0, 'At least one order required') + + await waitAndSendReceipt({ txOptionalParams }) + + this._initOrders(userAddress) + + for (let i = 0; i < length; i++) { + this.orders[userAddress].push({ + buyTokenId: buyTokens[i], + sellTokenId: sellTokens[i], + validFrom: validFroms[i], + validUntil: validUntils[i], + priceNumerator: buyAmounts[i], + priceDenominator: sellAmounts[i], + remainingAmount: sellAmounts[i], + }) + } + + return RECEIPT + } + public async cancelOrders({ userAddress, orderIds, txOptionalParams }: CancelOrdersParams): Promise { await waitAndSendReceipt({ txOptionalParams }) diff --git a/test/api/ExchangeApi/ExchangeApiMock.test.ts b/test/api/ExchangeApi/ExchangeApiMock.test.ts index b6cb3a22f..e331d1c80 100644 --- a/test/api/ExchangeApi/ExchangeApiMock.test.ts +++ b/test/api/ExchangeApi/ExchangeApiMock.test.ts @@ -165,6 +165,72 @@ describe('placeOrder', () => { expect(actual).toEqual({ ...expected, user: USER_2, id: '0' }) }) }) + +describe('placeValidFromOrders', () => { + const baseParams = { + userAddress: USER_1, + networkId: NETWORK_ID, + buyTokens: [], + sellTokens: [], + validFroms: [], + validUntils: [], + buyAmounts: [], + sellAmounts: [], + } + test('no order data provided', async () => { + try { + await instance.placeValidFromOrders(baseParams) + fail('Should not reach') + } catch (e) { + expect(e.message).toMatch(/At least one order required/) + } + }) + + test('parameters do not align', async () => { + const params = { ...baseParams, buyTokens: [1] } + + try { + await instance.placeValidFromOrders(params) + fail('Should not reach') + } catch (e) { + expect(e.message).toMatch(/Parameters length do not match/) + } + }) + + test('placing multiple orders', async () => { + const params = { + ...baseParams, + buyTokens: [1, 2], + sellTokens: [3, 1], + validFroms: [BATCH_ID, BATCH_ID], + validUntils: [BATCH_ID + 10, BATCH_ID + 101], + buyAmounts: [new BN(6), new BN(3)], + sellAmounts: [new BN(5), new BN(4)], + } + + const response = await instance.placeValidFromOrders(params) + expect(response).toBe(RECEIPT) + + // drop the first order, we just care about the last 2 + const [, ...orders] = await instance.getOrders(params) + + orders.forEach((order, index) => { + expect(order).toEqual({ + buyTokenId: params.buyTokens[index], + sellTokenId: params.sellTokens[index], + validFrom: params.validFroms[index], + validUntil: params.validUntils[index], + priceNumerator: params.buyAmounts[index], + priceDenominator: params.sellAmounts[index], + remainingAmount: params.sellAmounts[index], + user: params.userAddress, + id: (index + 1).toString(), + sellTokenBalance: new BN('1500000000000000000000').add(ONE), + }) + }) + }) +}) + describe('cancelOrder', () => { test('cancel existing order', async () => { const orderId = (await instance.getOrders({ userAddress: USER_1, networkId: NETWORK_ID })).length - 1 From 6eb400e2a3434bc2c0258e5aea0b7364e91c5199 Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Mon, 20 Jan 2020 09:07:49 -0800 Subject: [PATCH 34/53] 403/use place order (#425) * Adding placeMultipleOrders to usePlaceOrder hook * Re-enabling success notification after an order is placed --- src/hooks/usePlaceOrder.ts | 100 ++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/src/hooks/usePlaceOrder.ts b/src/hooks/usePlaceOrder.ts index b1405f556..f147936b1 100644 --- a/src/hooks/usePlaceOrder.ts +++ b/src/hooks/usePlaceOrder.ts @@ -8,18 +8,24 @@ import { PlaceOrderParams as ExchangeApiPlaceOrderParams } from 'api/exchange/Ex import { log } from 'utils' import { txOptionalParams } from 'utils/transaction' import { useWalletConnection } from './useWalletConnection' -import { DEFAULT_ORDER_DURATION } from 'const' +import { DEFAULT_ORDER_DURATION, MAX_BATCH_ID } from 'const' import useSafeState from './useSafeState' -interface PlaceOrderParams { +interface PlaceOrderParams { buyAmount: BN - buyToken: TokenDetails + buyToken: T sellAmount: BN - sellToken: TokenDetails + sellToken: T + validUntil?: number +} + +interface PlaceMultipleOrdersParams extends PlaceOrderParams { + validFrom?: number } interface Result { - placeOrder: (params: PlaceOrderParams) => Promise + placeOrder: (params: PlaceOrderParams) => Promise + placeMultipleOrders: (orders: PlaceMultipleOrdersParams[]) => Promise isSubmitting: boolean } @@ -28,7 +34,13 @@ export const usePlaceOrder = (): Result => { const { userAddress, networkId } = useWalletConnection() const placeOrder = useCallback( - async ({ buyAmount, buyToken, sellAmount, sellToken }: PlaceOrderParams): Promise => { + async ({ + buyAmount, + buyToken, + sellAmount, + sellToken, + validUntil, + }: PlaceOrderParams): Promise => { if (!userAddress || !networkId) { toast.error('Wallet is not connected!') return false @@ -51,13 +63,11 @@ export const usePlaceOrder = (): Result => { if (sellTokenId !== 0 || buyTokenId !== 0) { log('sellTokenId, buyTokenId, batchId', sellTokenId, buyTokenId, batchId, sellToken.address, buyToken.address) - const validUntil = batchId + DEFAULT_ORDER_DURATION - const params: ExchangeApiPlaceOrderParams = { userAddress, buyTokenId, sellTokenId, - validUntil, + validUntil: validUntil || batchId + DEFAULT_ORDER_DURATION, buyAmount, sellAmount, networkId, @@ -66,8 +76,8 @@ export const usePlaceOrder = (): Result => { const receipt = await exchangeApi.placeOrder(params) log(`The transaction has been mined: ${receipt.transactionHash}`) - // TODO: get order id in a separate call - // toast.success(`Placed order id=${receipt.data} valid for 30min`) + // TODO: show link to orders page? + toast.success(`Placed order valid for 30min`) return true } else { @@ -97,5 +107,71 @@ export const usePlaceOrder = (): Result => { [networkId, setIsSubmitting, userAddress], ) - return { placeOrder, isSubmitting } + const placeMultipleOrders = useCallback( + async (orders: PlaceMultipleOrdersParams[]): Promise => { + if (!userAddress || !networkId) { + toast.error('Wallet is not connected!') + return false + } + + setIsSubmitting(true) + log(`Placing ${orders.length} orders at once`) + + try { + const buyTokens: number[] = [] + const sellTokens: number[] = [] + + const validFroms: number[] = [] + const validUntils: number[] = [] + + const buyAmounts: BN[] = [] + const sellAmounts: BN[] = [] + + const currentBatchId = await exchangeApi.getCurrentBatchId(networkId) + + orders.forEach(order => { + buyTokens.push(order.buyToken) + sellTokens.push(order.sellToken) + + // if not set, order is valid from placement + validFroms.push(order.validFrom || currentBatchId) + // if not set, order is valid forever + validUntils.push(order.validUntil || MAX_BATCH_ID) + + buyAmounts.push(order.buyAmount) + sellAmounts.push(order.sellAmount) + }) + + const params = { + userAddress, + networkId, + buyTokens, + sellTokens, + validFroms, + validUntils, + buyAmounts, + sellAmounts, + } + + const receipt = await exchangeApi.placeValidFromOrders(params) + + log(`The transaction has been mined: ${receipt.transactionHash}`) + + // TODO: link to orders page? + toast.success(`Placed ${orders.length} orders`) + + return true + } catch (e) { + log(`Error placing orders`, e) + toast.error(`Error placing orders: ${e.message}`) + + return false + } finally { + setIsSubmitting(false) + } + }, + [networkId, setIsSubmitting, userAddress], + ) + + return { placeOrder, isSubmitting, placeMultipleOrders } } From 0d2757ce3cc814e2fcc80c9c6611b0410d86f1f1 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 20 Jan 2020 22:12:30 +0100 Subject: [PATCH 35/53] 401/Step 2: Define Spread (#426) * PoolingWidget initial setup code 1. Strategies page exports defaut as page * 1200px and rem equiv to const RESPONSIVE_SIZES * border-radius to PageWrapper * condensed logic * moved styled components into own file * pass stepArray as props * TokenSelector 1. added proper styles as per document 2. passed selectedtokens prop 3. moved styles to file * added progress bar colour var * PoolingWidget.styled 1. added styled componetns: button wrappers, subtext 2. misc css changes * PoolingWidget index 1. Finish ProgressBar logic 2. StepDescription: created 3. StepTitle: created + added subtext 4. BaseComp: set up state, memoised necessary fns 5. SubComponents: comp to select sub comps based on step state * SubComponents: switch instead of ternary * changed selectedTokens back to map and fixed (thanks dima) * DefineSpread > created file * moved DefineSpread styled into DefineSpread.styled * DefineSpread 1. added input change handlers 2. added table * Index 1. add selectedTokens getter using selectedTokenIds 2. added component descriptors * DefineSpread: fixed types on props * SubComponents 1. added GreySubText here 2. added checkmark icons 3. added DefineSpread * fixed DefineSpread mapping render + key + added Data-Label * DefineSpread 1. added CardTable formatting 2. styled responsive and web cells * row colour change + progress bar responsive sizing * PR requested changes 1. DefineSpread no longer renders rows (infinite rows) and instead renders string sentence in 2 rows 2. fixes spread percentage >0 and <= 100 * selectedTokensMap.values() > Dima suggested :) Co-Authored-By: Dmitry Co-authored-by: Dmitry --- .../PoolingWidget/DefineSpread.styled.ts | 66 +++++++++++ src/components/PoolingWidget/DefineSpread.tsx | 107 ++++++++++++++++++ .../PoolingWidget/PoolingWidget.styled.ts | 17 ++- .../PoolingWidget/SubComponents.tsx | 16 ++- src/components/PoolingWidget/index.tsx | 16 +-- 5 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 src/components/PoolingWidget/DefineSpread.styled.ts create mode 100644 src/components/PoolingWidget/DefineSpread.tsx diff --git a/src/components/PoolingWidget/DefineSpread.styled.ts b/src/components/PoolingWidget/DefineSpread.styled.ts new file mode 100644 index 000000000..d645d0f0a --- /dev/null +++ b/src/components/PoolingWidget/DefineSpread.styled.ts @@ -0,0 +1,66 @@ +import styled from 'styled-components' +import { ProgressStepText } from './PoolingWidget.styled' +import { RESPONSIVE_SIZES } from 'const' + +export const DefineSpreadWrapper = styled.div` + display: flex; + flex-flow: column nowrap; + > * { + margin: 0.5rem auto; + width: auto; + } + > input { + background: var(--color-background-highlighted); + border: 0.2rem solid var(--color-button-primary); + font-size: larger; + margin: auto; + width: 20%; + min-width: 5rem; + text-align: center; + + &:focus { + background: #bbfdbb87; + } +} + + + } + > div { + padding: 1rem; + } +` + +export const SpreadInformationWrapper = styled.div` + display: flex; + flex-flow: column nowrap; + justify-content: space-evenly; + align-items: center; + + background: var(--color-background); + border-radius: var(--border-radius); + + width: 82%; + + .responsive-cell { + display: none; + } + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + width: 95%; + + .web-cell { + display: none; + } + + .responsive-cell { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; + } + } +` + +export const RedBoldText = styled(ProgressStepText)` + color: red; +` diff --git a/src/components/PoolingWidget/DefineSpread.tsx b/src/components/PoolingWidget/DefineSpread.tsx new file mode 100644 index 000000000..a6d6e6d96 --- /dev/null +++ b/src/components/PoolingWidget/DefineSpread.tsx @@ -0,0 +1,107 @@ +import React from 'react' +import { TokenDetails } from '@gnosis.pm/dex-js' +import useSafeState from 'hooks/useSafeState' +import { SpreadInformationWrapper, DefineSpreadWrapper, RedBoldText } from './DefineSpread.styled' +import { DEFAULT_DECIMALS } from 'const' +import { CardTable } from 'components/Layout/Card' + +interface DefineSpreadProps { + selectedTokensMap: Map + defaultSpread?: number +} + +interface SpreadInformationProps extends DefineSpreadProps { + spread: number +} + +const SpreadInformation: React.FC = ({ selectedTokensMap, spread }) => { + const tokenSymbolsString = React.useMemo(() => Array.from(selectedTokensMap.values()).map(token => token.symbol), [ + selectedTokensMap, + ]) + + return ( + + tr { margin: 0 !important; }; + tbody > tr { + min-height: 3.5rem; + } + `} + $responsiveCSS={` + tbody > tr { + > td:first-child { + font-weight: bolder; + margin: auto; + width: 90%; + &::before { + content: none; + } + } + } + `} + > + + + Sell Spread + Buy Spread + + + + + {tokenSymbolsString.join(' - ')} + +
+ + ${(1 + spread / 100).toFixed(DEFAULT_DECIMALS)} + +
+
+ {tokenSymbolsString.join(', ')} for at least{' '} + + ${(1 + spread / 100).toFixed(DEFAULT_DECIMALS)} + +
+ + +
+ + ${(1 - spread / 100).toFixed(DEFAULT_DECIMALS)} + +
+
+ {tokenSymbolsString.join(', ')} for at most{' '} + + ${(1 - spread / 100).toFixed(DEFAULT_DECIMALS)} + +
+ + + +
+
+ ) +} + +const DefineSpread: React.FC = ({ selectedTokensMap, defaultSpread = 0.2 }) => { + const [spread, setSpread] = useSafeState(defaultSpread) + + const handleSpreadChange = ({ target: { value } }: React.ChangeEvent): void => { + if (+value < 0 || +value >= 100) return + + setSpread(+value) + } + + return ( + + + + + ) +} + +export default DefineSpread diff --git a/src/components/PoolingWidget/PoolingWidget.styled.ts b/src/components/PoolingWidget/PoolingWidget.styled.ts index 3163131bd..a03c2ce65 100644 --- a/src/components/PoolingWidget/PoolingWidget.styled.ts +++ b/src/components/PoolingWidget/PoolingWidget.styled.ts @@ -11,6 +11,11 @@ export const PoolingInterfaceWrapper = styled(PageWrapper)` > h2 { margin-right: auto; } + + @media only screen and (max-width: ${RESPONSIVE_SIZES.MOBILE_LARGE}em) { + padding: 1.5rem; + width: auto; + } ` export const ProgressStep = styled.div<{ $bgColor?: string }>` @@ -22,7 +27,7 @@ export const ProgressStep = styled.div<{ $bgColor?: string }>` font-size: xx-large; font-weight: bolder; - background: ${({ $bgColor = 'lightgrey' }): string => $bgColor}; + background: ${({ $bgColor = 'var(--color-background)' }): string => $bgColor}; ` export const ProgressStepText = styled.p<{ $bold: string }>` @@ -93,6 +98,16 @@ export const BarWrapper = styled.div<{ $bgColor?: string; $minHeight?: string }> white-space: normal; } } + + @media only screen and (max-width: ${RESPONSIVE_SIZES.MOBILE_LARGE}em) { + min-height: 10vw; + min-width: 72vw; + } + + @media only screen and (max-width: ${RESPONSIVE_SIZES.MOBILE_SMALL}em) { + margin: 0; + width: 90%; + } ` export const StepDescriptionWrapper = styled.div` diff --git a/src/components/PoolingWidget/SubComponents.tsx b/src/components/PoolingWidget/SubComponents.tsx index 1c5226717..bdcf89ea7 100644 --- a/src/components/PoolingWidget/SubComponents.tsx +++ b/src/components/PoolingWidget/SubComponents.tsx @@ -1,10 +1,16 @@ import React from 'react' import TokenSelector from './TokenSelector' +import DefineSpread from './DefineSpread' import { TokenSelectorProps } from './TokenSelector' +import { GreySubText } from './PoolingWidget.styled' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheckCircle } from '@fortawesome/free-solid-svg-icons' +import { TokenDetails } from '@gnosis.pm/dex-js' interface SubComponentProps extends TokenSelectorProps /* , All Other Steps */ { step: number + selectedTokensMap: Map } const SubComponents: React.FC = props => { @@ -13,10 +19,16 @@ const SubComponents: React.FC = props => { switch (step) { case 1: return ( - + <> + + + Please select at least two tokens to continue{' '} + {selectedTokensMap.size >= 2 && } + + ) case 2: - return
Step 2
+ return case 3: return
Step 3
case 4: diff --git a/src/components/PoolingWidget/index.tsx b/src/components/PoolingWidget/index.tsx index 441efb740..e2ded7a4f 100644 --- a/src/components/PoolingWidget/index.tsx +++ b/src/components/PoolingWidget/index.tsx @@ -37,12 +37,14 @@ const ProgressBar: React.FC = ({ step, stepArray }) => { {stepArray.map((stepName, index) => ( - + {index + 1} {index + 1 < 4 && ( )} @@ -91,7 +93,11 @@ const StepTitle: React.FC> = ({ step }) => { 'Select two or more stablecoins you want to include in your liquidity provision and you believe are worth $1', } case 2: - return { title: '2. Define your spread', subtext: '' } + return { + title: '2. Define your spread', + subtext: + 'The spread defines the percentage you want to sell above $1, and buy below $1 between all selected tokens', + } case 3: return { title: '3. Create strategy', subtext: '' } case 4: @@ -185,10 +191,6 @@ const PoolingInterface: React.FC = () => { - - Please select at least two tokens to continue{' '} - {selectedTokensMap.size >= 2 && } - From 7f84daa2a1e32d24b90e5d3849bab4c80b7ca1b2 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 21 Jan 2020 11:00:11 +0100 Subject: [PATCH 36/53] Pooling: Enable Liquidity in Nav + transition Strategy to Liquidity (syntax) (#434) * New Strategy > New Liquidity a. removed dead code * Added Liquidity to Nav (previously Strategy) * re-renable liquidity link in Orders --- src/App.tsx | 2 +- src/components/Layout/index.tsx | 13 ++++++------- src/components/OrdersWidget/index.tsx | 15 +++++++++------ src/components/PoolingWidget/index.tsx | 22 ++++++---------------- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 14ad65820..74b3eede8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -73,7 +73,7 @@ const App: React.FC = () => ( - + diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 5eb3d2e53..0524d00e0 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -47,13 +47,12 @@ const Layout: React.FC = ({ children }) => ( order: 3, withPastLocation: true, }, - // Place holder - // { - // label: 'Strategies', - // to: '/strategies', - // order: 4, - // withPastLocation: true, - // }, + { + label: 'Liquidity', + to: '/liquidity', + order: 4, + withPastLocation: true, + }, ]} />
{children}
diff --git a/src/components/OrdersWidget/index.tsx b/src/components/OrdersWidget/index.tsx index 5347537fb..8cd965b1f 100644 --- a/src/components/OrdersWidget/index.tsx +++ b/src/components/OrdersWidget/index.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useCallback, useEffect } from 'react' import { Link } from 'react-router-dom' -import { faExchangeAlt, faTrashAlt, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' +import { faExchangeAlt, faTrashAlt, faExclamationTriangle, faChartLine } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useWalletConnection } from 'hooks/useWalletConnection' @@ -121,12 +121,15 @@ const OrdersWidget: React.FC = () => { {/* TODO: enable when the strategy page is implemented */} - {/* - Create new strategy - + + + Create new liquidity + + + {/* TODO: replace href here */} - Learn more about strategies - */} + Learn more about liquidity +
{!noOrders && networkId && ( diff --git a/src/components/PoolingWidget/index.tsx b/src/components/PoolingWidget/index.tsx index e2ded7a4f..b3a9c0719 100644 --- a/src/components/PoolingWidget/index.tsx +++ b/src/components/PoolingWidget/index.tsx @@ -64,7 +64,7 @@ const ProgressBar: React.FC = ({ step, stepArray }) => { const StepDescription: React.FC = () => ( -

Setup your strategy once and allow your funds to be traded on your behalf.

+

Setup your liquidity provision once and allow your funds to be traded on your behalf.

  • @@ -79,7 +79,8 @@ const StepDescription: React.FC = () => ( Cancellation possible at any time
- Learn more about strategies + {/* TODO: add URL */} + Learn more about liquidity provision
) @@ -99,7 +100,7 @@ const StepTitle: React.FC> = ({ step }) => { 'The spread defines the percentage you want to sell above $1, and buy below $1 between all selected tokens', } case 3: - return { title: '3. Create strategy', subtext: '' } + return { title: '3. Create liquidity', subtext: '' } case 4: return { title: '4. Add funding', subtext: '' } default: @@ -117,17 +118,6 @@ const StepTitle: React.FC> = ({ step }) => { ) } -// function addRemoveItem(arr: number[], newItem: number): number[] { -// if (!arr.includes(newItem)) return arr.concat(newItem) - -// return arr.reduce((acc: number[], item) => { -// if (item === newItem) return acc - -// const newAcc = acc.concat(item) -// return newAcc -// }, []) -// } - function addRemoveMapItem(map: Map, newToken: TokenDetails): Map { // Cache map (no mutate) const copyMap = new Map(map) @@ -182,8 +172,8 @@ const PoolingInterface: React.FC = () => { return ( -

New Strategy

- +

New Liquidity

+ From 4fbf6bf7fe489df898c99be7a740393353558593 Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Tue, 21 Jan 2020 09:03:25 -0800 Subject: [PATCH 37/53] 402/price calculation (#427) * New constant UNLIMITED_ORDER_AMOUNT * New utils file price.ts exporsing maxAmountsForSpread for unlimited orders price calculation * Refactoring maxAmountsForSpread function * Max value actually needs to be 1 less than max * Creating a Bignumber constant for unlimited amount --- src/const.ts | 6 +- src/utils/index.ts | 1 + src/utils/price.ts | 77 +++++++++++ .../adjustAmountToLowerPrecision.spec.ts | 35 +++++ test/utils/maxAmountsForSpread.spec.ts | 126 ++++++++++++++++++ 5 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/utils/price.ts create mode 100644 test/utils/adjustAmountToLowerPrecision.spec.ts create mode 100644 test/utils/maxAmountsForSpread.spec.ts diff --git a/src/const.ts b/src/const.ts index a0cdc53d4..55d3d461b 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,4 +1,5 @@ import BN from 'bn.js' +import BigNumber from 'bignumber.js' export const APP_NAME = 'fuse' @@ -17,6 +18,9 @@ export const ALLOWANCE_MAX_VALUE = TWO.pow(new BN(256)).sub(ONE) // 115792089237 export const ALLOWANCE_FOR_ENABLED_TOKEN = TWO.pow(new BN(128)) // 340282366920938463463374607431768211456 // How much a sell order must be selling to be considered `unlimited` export const MIN_UNLIMITED_SELL_ORDER = ALLOWANCE_MAX_VALUE.div(TEN).mul(new BN(7)) // Currently set to 70% of max allowance +// Amount for an order to be considered unlimited, from contract's point of view: https://github.com/gnosis/dex-contracts/blob/master/contracts/BatchExchange.sol#L35 +export const UNLIMITED_ORDER_AMOUNT = TWO.pow(new BN(128)).sub(ONE) +export const UNLIMITED_ORDER_AMOUNT_BIGNUMBER = new BigNumber(UNLIMITED_ORDER_AMOUNT.toString()) // Model constants export const FEE_DENOMINATOR = 1000 // Fee is 1/fee_denominator i.e. 1/1000 = 0.1% @@ -29,7 +33,7 @@ export const MIN_UNLIMITED_SELL_ORDER_EXPIRATION_TIME = 60 * 24 * 365 * 100 // 1 // UI constants export const HIGHLIGHT_TIME = 5000 -export const FEE_PERCENTAGE = (1 / FEE_DENOMINATOR) * 100 // syntatic sugar for displaying purposes +export const FEE_PERCENTAGE = (1 / FEE_DENOMINATOR) * 100 // syntactic sugar for displaying purposes export const LEGALDOCUMENT = { CONTACT_ADDRESS: '[INSERT ADDRESS]', diff --git a/src/utils/index.ts b/src/utils/index.ts index 8acb287c1..dfb053911 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,3 +3,4 @@ export * from './format' export * from './ethereum' export * from './miscellaneous' export * from './autoconnect' +export * from './price' diff --git a/src/utils/price.ts b/src/utils/price.ts new file mode 100644 index 000000000..cd1577ad7 --- /dev/null +++ b/src/utils/price.ts @@ -0,0 +1,77 @@ +import BN from 'bn.js' +import { assert } from './miscellaneous' +import { TEN, UNLIMITED_ORDER_AMOUNT_BIGNUMBER } from 'const' +import BigNumber from 'bignumber.js' + +export function adjustAmountToLowerPrecision(amount: BN, higherPrecision: number, lowerPrecision: number): BN { + if (higherPrecision === lowerPrecision) { + // no adjustment required + return amount + } + + assert(higherPrecision > lowerPrecision, 'higherPrecision must be > lowerPrecision') + + const difference = new BN(higherPrecision - lowerPrecision) + + // divides amount by difference in precision, rounding. E.g: + // 1.2345, precision 4, amount == 12345 + // 1.2345, precision 2, amount == 12345 / 10 ^ (4 - 2) => 12345 / 100 => 123 + // 1.5550, precision 1, amount == 15550 / 10 ^ (4 - 1) => 15550 / 1000 => 16 + return amount.divRound(TEN.pow(difference)) +} + +function bigNumberToBN(n: BigNumber | BN): BN { + if (n instanceof BN) { + return n + } + return new BN(n.integerValue().toString(10)) +} + +interface Amounts { + buyAmount: BN + sellAmount: BN +} + +/** + * Calculates the max amounts within given `spread` for unlimited orders + * + * Uses BigNumber internally to keep track of decimals + * Returns BN for compatibility with ExchangeApi + * + * @param spread Value between 0 and 100, not inclusive + * @param buyTokenPrecision Decimals of buy token + * @param sellTokenPrecision Decimals of sell token + */ +export function maxAmountsForSpread(spread: number, buyTokenPrecision: number, sellTokenPrecision: number): Amounts { + // Enforcing positive spreads: 0 < spread < 100 + assert(spread > 0 && spread < 100, 'Invalid spread amount') + + const MAX = UNLIMITED_ORDER_AMOUNT_BIGNUMBER + const ONE = new BigNumber(1) + + const spreadPercentage = new BigNumber(spread).dividedBy(new BigNumber(100)) + + let buyAmount + let sellAmount + + if (buyTokenPrecision === sellTokenPrecision) { + // case 1: same precision + // buyAmount == MAX, sellAmount == buyAmount * (1 - (spread/100)) + buyAmount = MAX + sellAmount = buyAmount.multipliedBy(ONE.minus(spreadPercentage)) + } else if (buyTokenPrecision > sellTokenPrecision) { + // case 2: buyTokenPrecision > sellTokenPrecision + // buyAmount == MAX, sellAmount == buyAmount * (1 - (spread/100)) + buyAmount = MAX + const rawSellAmount = buyAmount.multipliedBy(ONE.minus(spreadPercentage)) + sellAmount = adjustAmountToLowerPrecision(bigNumberToBN(rawSellAmount), buyTokenPrecision, sellTokenPrecision) + } else { + // case 3: buyTokenPrecision < sellTokenPrecision + // sellAmount == MAX, buyAmount == sellAmount * (1 + (spread/100)) + sellAmount = MAX + const rawBuyAmount = sellAmount.multipliedBy(ONE.plus(spreadPercentage)) + buyAmount = adjustAmountToLowerPrecision(bigNumberToBN(rawBuyAmount), sellTokenPrecision, buyTokenPrecision) + } + + return { buyAmount: bigNumberToBN(buyAmount), sellAmount: bigNumberToBN(sellAmount) } +} diff --git a/test/utils/adjustAmountToLowerPrecision.spec.ts b/test/utils/adjustAmountToLowerPrecision.spec.ts new file mode 100644 index 000000000..4266fa2c3 --- /dev/null +++ b/test/utils/adjustAmountToLowerPrecision.spec.ts @@ -0,0 +1,35 @@ +import BN from 'bn.js' +import { adjustAmountToLowerPrecision } from 'utils' + +const higherPrecision = 4 +const amount = new BN(12345) // 1.2345 + +test('equal precision', () => { + const precision = 5 + + expect(adjustAmountToLowerPrecision(amount, precision, precision)).toStrictEqual(amount) +}) + +test('higher precision not higher than lower precision', () => { + try { + adjustAmountToLowerPrecision(amount, 1, 5) + fail('Should not reach') + } catch (e) { + expect(e.message).toMatch(/higherPrecision must be > lowerPrecision/) + } +}) + +test('rounding down', () => { + const lowerPrecision = 2 + const expected = new BN(123) // 1.23 + + expect(adjustAmountToLowerPrecision(amount, higherPrecision, lowerPrecision)).toStrictEqual(expected) +}) + +test('rounding up', () => { + const lowerPrecision = 1 + const amount = new BN(15555) // 1.5555 + const expected = new BN(16) // 1.6 + + expect(adjustAmountToLowerPrecision(amount, higherPrecision, lowerPrecision)).toStrictEqual(expected) +}) diff --git a/test/utils/maxAmountsForSpread.spec.ts b/test/utils/maxAmountsForSpread.spec.ts new file mode 100644 index 000000000..bb6153db1 --- /dev/null +++ b/test/utils/maxAmountsForSpread.spec.ts @@ -0,0 +1,126 @@ +import BN from 'bn.js' +import BigNumber from 'bignumber.js' + +import { UNLIMITED_ORDER_AMOUNT } from 'const' +import { maxAmountsForSpread } from 'utils' + +function assertSpread( + spread: number, + buyTokenAmount: BN, + buyTokenPrecision: number, + sellTokenAmount: BN, + sellTokenPrecision: number, +): void { + const ten = new BigNumber(10) + + const buyAmount = new BigNumber(buyTokenAmount.toString()).dividedBy(ten.exponentiatedBy(buyTokenPrecision)) + const sellAmount = new BigNumber(sellTokenAmount.toString()).dividedBy(ten.exponentiatedBy(sellTokenPrecision)) + + expect( + buyAmount + .dividedBy(sellAmount) + .minus(1) + .multipliedBy(100) + .toPrecision(new BigNumber(spread).precision(true) + 1), + ).toMatch(new RegExp(`^${spread.toString()}`)) +} + +describe('invalid input', () => { + test('negative spread', () => { + const spread = -1 + + try { + maxAmountsForSpread(spread, 1, 1) + fail('Should not reach') + } catch (e) { + expect(e.message).toMatch(/Invalid spread amount/) + } + }) + + test('spread over 100%', () => { + const spread = 200 + + try { + maxAmountsForSpread(spread, 1, 1) + fail('Should not reach') + } catch (e) { + expect(e.message).toMatch(/Invalid spread amount/) + } + }) +}) + +describe('same precision', () => { + test('precision 2, spread 0.5%', () => { + const precision = 2 + const spread = 0.5 + + const { buyAmount, sellAmount } = maxAmountsForSpread(spread, precision, precision) + + expect(buyAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) + + assertSpread(spread, buyAmount, precision, sellAmount, precision) + }) + + test('precision 18, spread 1%', () => { + const precision = 18 + const spread = 1 + + const { buyAmount, sellAmount } = maxAmountsForSpread(spread, precision, precision) + + expect(buyAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) + + assertSpread(spread, buyAmount, precision, sellAmount, precision) + }) +}) + +describe('buyTokenPrecision > sellTokenPrecision', () => { + test('buyPrecision 5, sellPrecision 2, spread 0.05%', () => { + const buyPrecision = 5 + const sellPrecision = 2 + const spread = 0.05 + + const { buyAmount, sellAmount } = maxAmountsForSpread(spread, buyPrecision, sellPrecision) + + expect(buyAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) + + assertSpread(spread, buyAmount, buyPrecision, sellAmount, sellPrecision) + }) + + test('buyPrecision 18, sellPrecision 2, spread 0.01%', () => { + const buyPrecision = 18 + const sellPrecision = 2 + const spread = 0.01 + + const { buyAmount, sellAmount } = maxAmountsForSpread(spread, buyPrecision, sellPrecision) + + expect(buyAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) + + assertSpread(spread, buyAmount, buyPrecision, sellAmount, sellPrecision) + }) +}) + +describe('buyTokenPrecision < sellTokenPrecision', () => { + test('buyPrecision 3, sellPrecision 4, spread 0.03%', () => { + const buyPrecision = 3 + const sellPrecision = 4 + const spread = 0.03 + + const { buyAmount, sellAmount } = maxAmountsForSpread(spread, buyPrecision, sellPrecision) + + expect(sellAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) + + assertSpread(spread, buyAmount, buyPrecision, sellAmount, sellPrecision) + }) + + test('buyPrecision 2, sellPrecision 18, spread 0.1%', () => { + const buyPrecision = 2 + const sellPrecision = 18 + const spread = 0.1 + + const { buyAmount, sellAmount } = maxAmountsForSpread(spread, buyPrecision, sellPrecision) + + expect(sellAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) + + assertSpread(spread, buyAmount, buyPrecision, sellAmount, sellPrecision) + }) +}) From 06f9f972d269cf5f823e392b5c0fc3377010cb3c Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Tue, 21 Jan 2020 09:37:25 -0800 Subject: [PATCH 38/53] 209/update orders on new block (#386) * WalletInfo exposes/subscribes to blockNumber/block changes * Updating orders on every new block --- src/api/wallet/WalletApi.ts | 6 ++++-- src/hooks/useOrders.ts | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/api/wallet/WalletApi.ts b/src/api/wallet/WalletApi.ts index eb878626e..fd9da0ed5 100644 --- a/src/api/wallet/WalletApi.ts +++ b/src/api/wallet/WalletApi.ts @@ -46,6 +46,7 @@ export interface WalletInfo { isConnected: boolean userAddress?: string networkId?: number + blockNumber?: number } export type ProviderInfo = ReturnType @@ -347,7 +348,7 @@ export class WalletApiImpl implements WalletApi { isConnected: !!userAddress && !!networkId, } } catch (error) { - log('Error asynchrously getting WalletInfo', error) + log('Error asynchronously getting WalletInfo', error) return { userAddress: '', networkId: 0, @@ -362,8 +363,9 @@ export class WalletApiImpl implements WalletApi { await Promise.resolve() const walletInfo = await (this.getWalletInfo() || this._getAsyncWalletInfo()) + const wInfoExtended = { ...walletInfo, blockNumber: blockchainUpdate?.blockHeader?.number } - this._listeners.forEach(listener => listener(walletInfo)) + this._listeners.forEach(listener => listener(wInfoExtended)) } private get _connected(): boolean | Promise { diff --git a/src/hooks/useOrders.ts b/src/hooks/useOrders.ts index 38fe8a9bb..b1301aa78 100644 --- a/src/hooks/useOrders.ts +++ b/src/hooks/useOrders.ts @@ -28,7 +28,7 @@ function filterDeletedOrders(orders: AuctionElement[]): AuctionElement[] { } export function useOrders(): AuctionElement[] { - const { userAddress, networkId } = useWalletConnection() + const { userAddress, networkId, blockNumber } = useWalletConnection() const [orders, setOrders] = useSafeState([]) useEffect(() => { @@ -38,7 +38,8 @@ export function useOrders(): AuctionElement[] { .getOrders({ userAddress, networkId }) .then(filterDeletedOrders) .then(setOrders) - }, [networkId, setOrders, userAddress]) + // updating list of orders on every block by listening on `blockNumber` + }, [networkId, setOrders, userAddress, blockNumber]) return orders } From 35f65b7e9971d4e3e17274ca735da02d67f517b5 Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Tue, 21 Jan 2020 13:57:52 -0800 Subject: [PATCH 39/53] 402/step 3 (#435) * placeOrder and placeMultipleOrders now return an object * Added CreatStrategy component * Moved spread state to index * useCallback for next/prevStep functions * Added function to create params for placeMultipleOrders function * Sending tx to place orders * Using text decoration instead of for emphasizing text * Accepting txOptionalParams on usePlaceOrder hook functions * Moving to next step once tx is sent and storing txHash * Added error handling for when order placement fails --- .../PoolingWidget/CreateStrategy.styled.ts | 27 +++++ .../PoolingWidget/CreateStrategy.tsx | 42 ++++++++ src/components/PoolingWidget/DefineSpread.tsx | 13 +-- .../PoolingWidget/SubComponents.tsx | 17 ++- src/components/PoolingWidget/index.tsx | 100 ++++++++++++++++-- src/components/TradeWidget/index.tsx | 2 +- src/hooks/usePlaceOrder.ts | 43 +++++--- 7 files changed, 206 insertions(+), 38 deletions(-) create mode 100644 src/components/PoolingWidget/CreateStrategy.styled.ts create mode 100644 src/components/PoolingWidget/CreateStrategy.tsx diff --git a/src/components/PoolingWidget/CreateStrategy.styled.ts b/src/components/PoolingWidget/CreateStrategy.styled.ts new file mode 100644 index 000000000..8defc85bd --- /dev/null +++ b/src/components/PoolingWidget/CreateStrategy.styled.ts @@ -0,0 +1,27 @@ +import styled from 'styled-components' +import { RESPONSIVE_SIZES } from 'const' + +export const CreateStrategyWrapper = styled.div` + background: var(--color-background); + padding: 1em 3em; +` + +export const StrategyDetailsWrapper = styled.div` + display: grid; + grid-template-columns: auto 1fr; + column-gap: 1em; + justify-items: center; + align-items: center; + + @media only screen and (max-width: ${RESPONSIVE_SIZES.TABLET}em) { + grid-template-columns: 1fr; + + .graph { + display: none; + } + } +` + +export const UnderlinedText = styled.span` + text-decoration: underline; +` diff --git a/src/components/PoolingWidget/CreateStrategy.tsx b/src/components/PoolingWidget/CreateStrategy.tsx new file mode 100644 index 000000000..9cc60b6c9 --- /dev/null +++ b/src/components/PoolingWidget/CreateStrategy.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faChartArea } from '@fortawesome/free-solid-svg-icons' +import { TokenDetails } from '@gnosis.pm/dex-js' + +import { RedBoldText } from './DefineSpread.styled' +import { SpreadInformation } from './DefineSpread' +import { CreateStrategyWrapper, StrategyDetailsWrapper, UnderlinedText } from './CreateStrategy.styled' + +export interface CreateStrategyProps { + selectedTokensMap: Map + spread: number +} + +export const CreateStrategy: React.FC = ({ selectedTokensMap, spread }) => { + const ordersCount = selectedTokensMap.size * (selectedTokensMap.size - 1) + + return ( + +

New strategy details

+ + + + +

+ Once the transaction is sent and mined,{' '} + + {ordersCount} orders + {' '} + will be created. +

+

+ These orders can only be executed with the deposited balance in the{' '} + Exchange Wallet. +

+

+ In the next step, you can review the balance you have, and possibly deposit some tokens so these trades can be + executed. +

+
+ ) +} diff --git a/src/components/PoolingWidget/DefineSpread.tsx b/src/components/PoolingWidget/DefineSpread.tsx index a6d6e6d96..c0baba52f 100644 --- a/src/components/PoolingWidget/DefineSpread.tsx +++ b/src/components/PoolingWidget/DefineSpread.tsx @@ -1,20 +1,19 @@ import React from 'react' import { TokenDetails } from '@gnosis.pm/dex-js' -import useSafeState from 'hooks/useSafeState' import { SpreadInformationWrapper, DefineSpreadWrapper, RedBoldText } from './DefineSpread.styled' import { DEFAULT_DECIMALS } from 'const' import { CardTable } from 'components/Layout/Card' interface DefineSpreadProps { selectedTokensMap: Map - defaultSpread?: number -} -interface SpreadInformationProps extends DefineSpreadProps { spread: number + setSpread: React.Dispatch> } -const SpreadInformation: React.FC = ({ selectedTokensMap, spread }) => { +type SpreadInformationProps = Omit + +export const SpreadInformation: React.FC = ({ selectedTokensMap, spread }) => { const tokenSymbolsString = React.useMemo(() => Array.from(selectedTokensMap.values()).map(token => token.symbol), [ selectedTokensMap, ]) @@ -87,9 +86,7 @@ const SpreadInformation: React.FC = ({ selectedTokensMap ) } -const DefineSpread: React.FC = ({ selectedTokensMap, defaultSpread = 0.2 }) => { - const [spread, setSpread] = useSafeState(defaultSpread) - +const DefineSpread: React.FC = ({ selectedTokensMap, spread, setSpread }) => { const handleSpreadChange = ({ target: { value } }: React.ChangeEvent): void => { if (+value < 0 || +value >= 100) return diff --git a/src/components/PoolingWidget/SubComponents.tsx b/src/components/PoolingWidget/SubComponents.tsx index bdcf89ea7..d6a4017d9 100644 --- a/src/components/PoolingWidget/SubComponents.tsx +++ b/src/components/PoolingWidget/SubComponents.tsx @@ -7,14 +7,18 @@ import { GreySubText } from './PoolingWidget.styled' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCheckCircle } from '@fortawesome/free-solid-svg-icons' import { TokenDetails } from '@gnosis.pm/dex-js' +import { CreateStrategy } from './CreateStrategy' interface SubComponentProps extends TokenSelectorProps /* , All Other Steps */ { step: number selectedTokensMap: Map + spread: number + setSpread: React.Dispatch> + txHash: string } const SubComponents: React.FC = props => { - const { step, handleTokenSelect, selectedTokensMap, tokens } = props + const { step, handleTokenSelect, selectedTokensMap, tokens, spread, setSpread, txHash } = props switch (step) { case 1: @@ -28,11 +32,16 @@ const SubComponents: React.FC = props => { ) case 2: - return + return case 3: - return
Step 3
+ return ( + <> + + Review your strategy summary above and then send your transaction + + ) case 4: - return
Step 4
+ return
Pending transaction: {txHash}
default: return ( diff --git a/src/components/PoolingWidget/index.tsx b/src/components/PoolingWidget/index.tsx index b3a9c0719..03b70607f 100644 --- a/src/components/PoolingWidget/index.tsx +++ b/src/components/PoolingWidget/index.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useMemo } from 'react' +import { toast } from 'react-toastify' import SubComponents from './SubComponents' import Widget from 'components/Layout/Widget' @@ -13,17 +14,21 @@ import { GreySubText, } from './PoolingWidget.styled' -import { faCheckCircle } from '@fortawesome/free-solid-svg-icons' +import { faCheckCircle, faSpinner, faPaperPlane } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import useSafeState from 'hooks/useSafeState' import { useWalletConnection } from 'hooks/useWalletConnection' +import { usePlaceOrder, MultipleOrdersOrder } from 'hooks/usePlaceOrder' import { tokenListApi } from 'api' import { TokenDetails } from '@gnosis.pm/dex-js' import { Network } from 'types' +import { maxAmountsForSpread, log } from 'utils' +import { DEFAULT_PRECISION } from 'const' + interface ProgressBarProps { step: number stepArray: string[] @@ -128,9 +133,46 @@ function addRemoveMapItem(map: Map, newToken: TokenDetails return copyMap } +// TODO: Decide the best place to put this. This file is too long already, but feels to specific for utils +export function createOrderParams(tokens: TokenDetails[], spread: number): MultipleOrdersOrder[] { + // We'll create 2 orders for each pair: SELL_A -> BUY_B and SELL_B -> BUY_A + // where buyAmount == max uint128, buyAmount > sellAmount and sellAmount == buyAmount * (1 - spread / 100) + + // With 2 tokens A, B, we have 1 pair [(A, B)] == 2 orders + // With 3 tokens A, B, C, we have 3 pairs [(A, B), (A, C), (B, C)] == 6 orders + // With 4 tokens A, B, C, D, we have 6 pairs [(A, B), (A, C), (A, D), (B, C), (B, D), (C, D)] == 12 orders + // And so on... + // The number of orders is equal to num_tokens * (num_tokens -1) + const orders: MultipleOrdersOrder[] = [] + + tokens.forEach(buyToken => + tokens.forEach(sellToken => { + // We don't want to pair a token with itself + if (buyToken !== sellToken) { + // calculating buy/sell amounts + const { buyAmount, sellAmount } = maxAmountsForSpread( + spread, + buyToken.decimals || DEFAULT_PRECISION, + sellToken.decimals || DEFAULT_PRECISION, + ) + + orders.push({ + buyToken: buyToken.id, + sellToken: sellToken.id, + buyAmount, + sellAmount, + }) + } + }), + ) + + return orders +} + const PoolingInterface: React.FC = () => { const [step, setStep] = useSafeState(1) const [selectedTokensMap, setSelectedTokensMap] = useSafeState>(new Map()) + const [spread, setSpread] = useSafeState(0.2) const { networkId } = useWalletConnection() // Avoid displaying an empty list of tokens when the wallet is not connected @@ -141,16 +183,45 @@ const PoolingInterface: React.FC = () => { fallBackNetworkId, ]) - const prevStep = (): void => { + const prevStep = useCallback((): void => { if (step == 1) return return setStep(step - 1) - } - const nextStep = (): void => { + }, [setStep, step]) + const nextStep = useCallback((): void => { if (step == 4) return return setStep(step + 1) - } + }, [setStep, step]) + + const { isSubmitting, placeMultipleOrders } = usePlaceOrder() + + const [txHash, setTxHash] = useSafeState('') + + const onSentTransaction = useCallback( + txHash => { + setTxHash(txHash) + nextStep() + }, + [nextStep, setTxHash], + ) + + const sendTransaction = useCallback(() => { + const orders = createOrderParams(Array.from(selectedTokensMap.values()), spread) + + placeMultipleOrders({ + orders, + txOptionalParams: { + onSentTransaction, + }, + }).catch(e => { + log('Failed to place orders for strategy', e) + toast.error('Not able to create your orders, please try again') + + // Get back to current step + setStep(3) + }) + }, [onSentTransaction, placeMultipleOrders, selectedTokensMap, setStep, spread]) const handleTokenSelect = useCallback( (token: TokenDetails): void => { @@ -165,8 +236,11 @@ const PoolingInterface: React.FC = () => { handleTokenSelect, tokens, selectedTokensMap, + spread, + setSpread, + txHash, }), - [handleTokenSelect, selectedTokensMap, tokens], + [handleTokenSelect, selectedTokensMap, setSpread, spread, tokens, txHash], ) return ( @@ -181,12 +255,18 @@ const PoolingInterface: React.FC = () => { - - + {step !== 3 ? ( + + ) : ( + + )}
diff --git a/src/components/TradeWidget/index.tsx b/src/components/TradeWidget/index.tsx index a0c5c9581..d32002357 100644 --- a/src/components/TradeWidget/index.tsx +++ b/src/components/TradeWidget/index.tsx @@ -154,7 +154,7 @@ const TradeWidget: React.FC = () => { if (!buyAmount || !sellAmount || !cachedBuyToken || !cachedSellToken) return if (isConnected) { - const success = await placeOrder({ + const { success } = await placeOrder({ buyAmount, buyToken: cachedBuyToken, sellAmount, diff --git a/src/hooks/usePlaceOrder.ts b/src/hooks/usePlaceOrder.ts index f147936b1..6d0c84ca3 100644 --- a/src/hooks/usePlaceOrder.ts +++ b/src/hooks/usePlaceOrder.ts @@ -2,11 +2,11 @@ import { useCallback } from 'react' import BN from 'bn.js' import { toast } from 'react-toastify' -import { TokenDetails } from 'types' +import { TokenDetails, Receipt, TxOptionalParams } from 'types' import { exchangeApi } from 'api' import { PlaceOrderParams as ExchangeApiPlaceOrderParams } from 'api/exchange/ExchangeApi' import { log } from 'utils' -import { txOptionalParams } from 'utils/transaction' +import { txOptionalParams as defaultTxOptionalParams } from 'utils/transaction' import { useWalletConnection } from './useWalletConnection' import { DEFAULT_ORDER_DURATION, MAX_BATCH_ID } from 'const' import useSafeState from './useSafeState' @@ -17,18 +17,29 @@ interface PlaceOrderParams { sellAmount: BN sellToken: T validUntil?: number + txOptionalParams?: TxOptionalParams } -interface PlaceMultipleOrdersParams extends PlaceOrderParams { +export interface MultipleOrdersOrder extends Omit, 'txOptionalParams'> { validFrom?: number } +interface PlaceMultipleOrdersParams { + orders: MultipleOrdersOrder[] + txOptionalParams?: TxOptionalParams +} + interface Result { - placeOrder: (params: PlaceOrderParams) => Promise - placeMultipleOrders: (orders: PlaceMultipleOrdersParams[]) => Promise + placeOrder: (params: PlaceOrderParams) => Promise + placeMultipleOrders: (params: PlaceMultipleOrdersParams) => Promise isSubmitting: boolean } +interface PlaceOrderResult { + success: boolean + receipt?: Receipt +} + export const usePlaceOrder = (): Result => { const [isSubmitting, setIsSubmitting] = useSafeState(false) const { userAddress, networkId } = useWalletConnection() @@ -40,10 +51,11 @@ export const usePlaceOrder = (): Result => { sellAmount, sellToken, validUntil, - }: PlaceOrderParams): Promise => { + txOptionalParams, + }: PlaceOrderParams): Promise => { if (!userAddress || !networkId) { toast.error('Wallet is not connected!') - return false + return { success: false } } setIsSubmitting(true) @@ -71,7 +83,7 @@ export const usePlaceOrder = (): Result => { buyAmount, sellAmount, networkId, - txOptionalParams, + txOptionalParams: txOptionalParams || defaultTxOptionalParams, } const receipt = await exchangeApi.placeOrder(params) log(`The transaction has been mined: ${receipt.transactionHash}`) @@ -79,7 +91,7 @@ export const usePlaceOrder = (): Result => { // TODO: show link to orders page? toast.success(`Placed order valid for 30min`) - return true + return { success: true, receipt } } else { // TODO: Handle better this case // TODO: Review in the contracts, cause it looks like fee token is 0, what is also used for unregistered tokens @@ -93,13 +105,13 @@ export const usePlaceOrder = (): Result => { buyToken, buyTokenId, }) - return false + return { success: false } } } catch (e) { log(`Error placing order`, e) toast.error(`Error placing order: ${e.message}`) - return false + return { success: false } } finally { setIsSubmitting(false) } @@ -108,10 +120,10 @@ export const usePlaceOrder = (): Result => { ) const placeMultipleOrders = useCallback( - async (orders: PlaceMultipleOrdersParams[]): Promise => { + async ({ orders, txOptionalParams }: PlaceMultipleOrdersParams): Promise => { if (!userAddress || !networkId) { toast.error('Wallet is not connected!') - return false + return { success: false } } setIsSubmitting(true) @@ -151,6 +163,7 @@ export const usePlaceOrder = (): Result => { validUntils, buyAmounts, sellAmounts, + txOptionalParams: txOptionalParams || defaultTxOptionalParams, } const receipt = await exchangeApi.placeValidFromOrders(params) @@ -160,12 +173,12 @@ export const usePlaceOrder = (): Result => { // TODO: link to orders page? toast.success(`Placed ${orders.length} orders`) - return true + return { success: true, receipt } } catch (e) { log(`Error placing orders`, e) toast.error(`Error placing orders: ${e.message}`) - return false + return { success: false } } finally { setIsSubmitting(false) } From 871ef3ba58bd7a4e1f4fe3252227c19acf6106c4 Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Tue, 21 Jan 2020 14:49:54 -0800 Subject: [PATCH 40/53] Making function on utils/price.ts accept obj parameters (#438) * Making function on utils/price.ts accept obj parameters * Adjust parameters usage on PoolingWidget --- src/components/PoolingWidget/index.tsx | 9 ++- src/utils/price.ts | 32 +++++++++-- .../adjustAmountToLowerPrecision.spec.ts | 10 ++-- test/utils/maxAmountsForSpread.spec.ts | 56 +++++++++++-------- 4 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/components/PoolingWidget/index.tsx b/src/components/PoolingWidget/index.tsx index 03b70607f..ab49157c6 100644 --- a/src/components/PoolingWidget/index.tsx +++ b/src/components/PoolingWidget/index.tsx @@ -136,7 +136,6 @@ function addRemoveMapItem(map: Map, newToken: TokenDetails // TODO: Decide the best place to put this. This file is too long already, but feels to specific for utils export function createOrderParams(tokens: TokenDetails[], spread: number): MultipleOrdersOrder[] { // We'll create 2 orders for each pair: SELL_A -> BUY_B and SELL_B -> BUY_A - // where buyAmount == max uint128, buyAmount > sellAmount and sellAmount == buyAmount * (1 - spread / 100) // With 2 tokens A, B, we have 1 pair [(A, B)] == 2 orders // With 3 tokens A, B, C, we have 3 pairs [(A, B), (A, C), (B, C)] == 6 orders @@ -150,11 +149,11 @@ export function createOrderParams(tokens: TokenDetails[], spread: number): Multi // We don't want to pair a token with itself if (buyToken !== sellToken) { // calculating buy/sell amounts - const { buyAmount, sellAmount } = maxAmountsForSpread( + const { buyAmount, sellAmount } = maxAmountsForSpread({ spread, - buyToken.decimals || DEFAULT_PRECISION, - sellToken.decimals || DEFAULT_PRECISION, - ) + buyTokenPrecision: buyToken.decimals || DEFAULT_PRECISION, + sellTokenPrecision: sellToken.decimals || DEFAULT_PRECISION, + }) orders.push({ buyToken: buyToken.id, diff --git a/src/utils/price.ts b/src/utils/price.ts index cd1577ad7..1bbc0742d 100644 --- a/src/utils/price.ts +++ b/src/utils/price.ts @@ -3,7 +3,13 @@ import { assert } from './miscellaneous' import { TEN, UNLIMITED_ORDER_AMOUNT_BIGNUMBER } from 'const' import BigNumber from 'bignumber.js' -export function adjustAmountToLowerPrecision(amount: BN, higherPrecision: number, lowerPrecision: number): BN { +interface AdjustAmountParams { + amount: BN + higherPrecision: number + lowerPrecision: number +} + +export function adjustAmountToLowerPrecision({ amount, higherPrecision, lowerPrecision }: AdjustAmountParams): BN { if (higherPrecision === lowerPrecision) { // no adjustment required return amount @@ -27,6 +33,12 @@ function bigNumberToBN(n: BigNumber | BN): BN { return new BN(n.integerValue().toString(10)) } +interface MaxAmountForSpreadParam { + spread: number + buyTokenPrecision: number + sellTokenPrecision: number +} + interface Amounts { buyAmount: BN sellAmount: BN @@ -42,7 +54,11 @@ interface Amounts { * @param buyTokenPrecision Decimals of buy token * @param sellTokenPrecision Decimals of sell token */ -export function maxAmountsForSpread(spread: number, buyTokenPrecision: number, sellTokenPrecision: number): Amounts { +export function maxAmountsForSpread({ + spread, + buyTokenPrecision, + sellTokenPrecision, +}: MaxAmountForSpreadParam): Amounts { // Enforcing positive spreads: 0 < spread < 100 assert(spread > 0 && spread < 100, 'Invalid spread amount') @@ -64,13 +80,21 @@ export function maxAmountsForSpread(spread: number, buyTokenPrecision: number, s // buyAmount == MAX, sellAmount == buyAmount * (1 - (spread/100)) buyAmount = MAX const rawSellAmount = buyAmount.multipliedBy(ONE.minus(spreadPercentage)) - sellAmount = adjustAmountToLowerPrecision(bigNumberToBN(rawSellAmount), buyTokenPrecision, sellTokenPrecision) + sellAmount = adjustAmountToLowerPrecision({ + amount: bigNumberToBN(rawSellAmount), + higherPrecision: buyTokenPrecision, + lowerPrecision: sellTokenPrecision, + }) } else { // case 3: buyTokenPrecision < sellTokenPrecision // sellAmount == MAX, buyAmount == sellAmount * (1 + (spread/100)) sellAmount = MAX const rawBuyAmount = sellAmount.multipliedBy(ONE.plus(spreadPercentage)) - buyAmount = adjustAmountToLowerPrecision(bigNumberToBN(rawBuyAmount), sellTokenPrecision, buyTokenPrecision) + buyAmount = adjustAmountToLowerPrecision({ + amount: bigNumberToBN(rawBuyAmount), + higherPrecision: sellTokenPrecision, + lowerPrecision: buyTokenPrecision, + }) } return { buyAmount: bigNumberToBN(buyAmount), sellAmount: bigNumberToBN(sellAmount) } diff --git a/test/utils/adjustAmountToLowerPrecision.spec.ts b/test/utils/adjustAmountToLowerPrecision.spec.ts index 4266fa2c3..bbcc8b41e 100644 --- a/test/utils/adjustAmountToLowerPrecision.spec.ts +++ b/test/utils/adjustAmountToLowerPrecision.spec.ts @@ -7,12 +7,14 @@ const amount = new BN(12345) // 1.2345 test('equal precision', () => { const precision = 5 - expect(adjustAmountToLowerPrecision(amount, precision, precision)).toStrictEqual(amount) + expect(adjustAmountToLowerPrecision({ amount, higherPrecision: precision, lowerPrecision: precision })).toStrictEqual( + amount, + ) }) test('higher precision not higher than lower precision', () => { try { - adjustAmountToLowerPrecision(amount, 1, 5) + adjustAmountToLowerPrecision({ amount, higherPrecision: 1, lowerPrecision: 5 }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/higherPrecision must be > lowerPrecision/) @@ -23,7 +25,7 @@ test('rounding down', () => { const lowerPrecision = 2 const expected = new BN(123) // 1.23 - expect(adjustAmountToLowerPrecision(amount, higherPrecision, lowerPrecision)).toStrictEqual(expected) + expect(adjustAmountToLowerPrecision({ amount, higherPrecision, lowerPrecision })).toStrictEqual(expected) }) test('rounding up', () => { @@ -31,5 +33,5 @@ test('rounding up', () => { const amount = new BN(15555) // 1.5555 const expected = new BN(16) // 1.6 - expect(adjustAmountToLowerPrecision(amount, higherPrecision, lowerPrecision)).toStrictEqual(expected) + expect(adjustAmountToLowerPrecision({ amount, higherPrecision, lowerPrecision })).toStrictEqual(expected) }) diff --git a/test/utils/maxAmountsForSpread.spec.ts b/test/utils/maxAmountsForSpread.spec.ts index bb6153db1..3aa0341de 100644 --- a/test/utils/maxAmountsForSpread.spec.ts +++ b/test/utils/maxAmountsForSpread.spec.ts @@ -30,7 +30,7 @@ describe('invalid input', () => { const spread = -1 try { - maxAmountsForSpread(spread, 1, 1) + maxAmountsForSpread({ spread, buyTokenPrecision: 1, sellTokenPrecision: 1 }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/Invalid spread amount/) @@ -41,7 +41,7 @@ describe('invalid input', () => { const spread = 200 try { - maxAmountsForSpread(spread, 1, 1) + maxAmountsForSpread({ spread, buyTokenPrecision: 1, sellTokenPrecision: 1 }) fail('Should not reach') } catch (e) { expect(e.message).toMatch(/Invalid spread amount/) @@ -54,7 +54,11 @@ describe('same precision', () => { const precision = 2 const spread = 0.5 - const { buyAmount, sellAmount } = maxAmountsForSpread(spread, precision, precision) + const { buyAmount, sellAmount } = maxAmountsForSpread({ + spread, + buyTokenPrecision: precision, + sellTokenPrecision: precision, + }) expect(buyAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) @@ -65,7 +69,11 @@ describe('same precision', () => { const precision = 18 const spread = 1 - const { buyAmount, sellAmount } = maxAmountsForSpread(spread, precision, precision) + const { buyAmount, sellAmount } = maxAmountsForSpread({ + spread, + buyTokenPrecision: precision, + sellTokenPrecision: precision, + }) expect(buyAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) @@ -74,53 +82,53 @@ describe('same precision', () => { }) describe('buyTokenPrecision > sellTokenPrecision', () => { - test('buyPrecision 5, sellPrecision 2, spread 0.05%', () => { - const buyPrecision = 5 - const sellPrecision = 2 + test('buyTokenPrecision 5, sellTokenPrecision 2, spread 0.05%', () => { + const buyTokenPrecision = 5 + const sellTokenPrecision = 2 const spread = 0.05 - const { buyAmount, sellAmount } = maxAmountsForSpread(spread, buyPrecision, sellPrecision) + const { buyAmount, sellAmount } = maxAmountsForSpread({ spread, buyTokenPrecision, sellTokenPrecision }) expect(buyAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) - assertSpread(spread, buyAmount, buyPrecision, sellAmount, sellPrecision) + assertSpread(spread, buyAmount, buyTokenPrecision, sellAmount, sellTokenPrecision) }) - test('buyPrecision 18, sellPrecision 2, spread 0.01%', () => { - const buyPrecision = 18 - const sellPrecision = 2 + test('buyTokenPrecision 18, sellTokenPrecision 2, spread 0.01%', () => { + const buyTokenPrecision = 18 + const sellTokenPrecision = 2 const spread = 0.01 - const { buyAmount, sellAmount } = maxAmountsForSpread(spread, buyPrecision, sellPrecision) + const { buyAmount, sellAmount } = maxAmountsForSpread({ spread, buyTokenPrecision, sellTokenPrecision }) expect(buyAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) - assertSpread(spread, buyAmount, buyPrecision, sellAmount, sellPrecision) + assertSpread(spread, buyAmount, buyTokenPrecision, sellAmount, sellTokenPrecision) }) }) describe('buyTokenPrecision < sellTokenPrecision', () => { - test('buyPrecision 3, sellPrecision 4, spread 0.03%', () => { - const buyPrecision = 3 - const sellPrecision = 4 + test('buyTokenPrecision 3, sellTokenPrecision 4, spread 0.03%', () => { + const buyTokenPrecision = 3 + const sellTokenPrecision = 4 const spread = 0.03 - const { buyAmount, sellAmount } = maxAmountsForSpread(spread, buyPrecision, sellPrecision) + const { buyAmount, sellAmount } = maxAmountsForSpread({ spread, buyTokenPrecision, sellTokenPrecision }) expect(sellAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) - assertSpread(spread, buyAmount, buyPrecision, sellAmount, sellPrecision) + assertSpread(spread, buyAmount, buyTokenPrecision, sellAmount, sellTokenPrecision) }) - test('buyPrecision 2, sellPrecision 18, spread 0.1%', () => { - const buyPrecision = 2 - const sellPrecision = 18 + test('buyTokenPrecision 2, sellTokenPrecision 18, spread 0.1%', () => { + const buyTokenPrecision = 2 + const sellTokenPrecision = 18 const spread = 0.1 - const { buyAmount, sellAmount } = maxAmountsForSpread(spread, buyPrecision, sellPrecision) + const { buyAmount, sellAmount } = maxAmountsForSpread({ spread, buyTokenPrecision, sellTokenPrecision }) expect(sellAmount.toString()).toEqual(UNLIMITED_ORDER_AMOUNT.toString()) - assertSpread(spread, buyAmount, buyPrecision, sellAmount, sellPrecision) + assertSpread(spread, buyAmount, buyTokenPrecision, sellAmount, sellTokenPrecision) }) }) From 6d3ac3966b2fb6671cae138289864b8e28272bf3 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 22 Jan 2020 15:09:59 +0300 Subject: [PATCH 41/53] consider .eth.link as IPFS hosted (#433) --- src/html/index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/html/index.html b/src/html/index.html index a914c44a3..23cb2540e 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -5,20 +5,20 @@ <%if (htmlWebpackPlugin.options.ipfsHack) { %>