diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..19c174e6c --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +REACT_APP_MAIL_SRV=https://testnet.sovryn.app/backend/ +REACT_APP_MAILCHIMP=https://gmail.us17.list-manage.com/subscribe/post?u=e66850f0b51ebbdbe1f2c3e36&id=ef5d452839 diff --git a/.env.local b/.env.local index 2f73c8dad..03541cef3 100644 --- a/.env.local +++ b/.env.local @@ -3,3 +3,16 @@ EXTEND_ESLINT=true REACT_APP_NETWORK=testnet REACT_APP_SENTRY_DSN=https://2981633dd7f04f0d9fb3facf8c1332b8@o459269.ingest.sentry.io/5458134 + +#REACT_APP_TEST_FIRST_BLOCK=1271014 +REACT_APP_TEST_FIRST_BLOCK=0 + +REACT_APP_PORTIS_ID=469a25c8-1101-4c57-823d-c47cb328f788 + +REACT_APP_WHITELIST=false +REACT_APP_WHITELIST_TOKEN= + +REACT_APP_YBUG_ID= + +REACT_APP_MAIL_SRV=https://testnet.sovryn.app/backend/ +REACT_APP_MAILCHIMP=https://gmail.us17.list-manage.com/subscribe/post?u=e66850f0b51ebbdbe1f2c3e36&id=ef5d452839 diff --git a/.env.mainnet.local b/.env.mainnet.local index 83b9ee159..7314875e2 100644 --- a/.env.mainnet.local +++ b/.env.mainnet.local @@ -3,3 +3,14 @@ EXTEND_ESLINT=true REACT_APP_NETWORK=mainnet REACT_APP_SENTRY_DSN= + +REACT_APP_PORTIS_ID=469a25c8-1101-4c57-823d-c47cb328f788 + +REACT_APP_WHITELIST=false +REACT_APP_WHITELIST_TOKEN=0x576aE218aeCfD4CbD2DBe07250b47e26060932B1 + +REACT_APP_YBUG_ID= + +REACT_APP_MAIL_SRV=https://backend.sovryn.app/ +REACT_APP_MAILCHIMP=https://gmail.us17.list-manage.com/subscribe/post?u=e66850f0b51ebbdbe1f2c3e36&id=ef5d452839 + diff --git a/.env.production b/.env.production index 79feb4a95..7823a2406 100644 --- a/.env.production +++ b/.env.production @@ -2,3 +2,14 @@ GENERATE_SOURCEMAP=false REACT_APP_NETWORK=mainnet REACT_APP_SENTRY_DSN=https://2981633dd7f04f0d9fb3facf8c1332b8@o459269.ingest.sentry.io/5458134 + +REACT_APP_PORTIS_ID=469a25c8-1101-4c57-823d-c47cb328f788 + +REACT_APP_WHITELIST=true +REACT_APP_WHITELIST_TOKEN=0x576aE218aeCfD4CbD2DBe07250b47e26060932B1 + +REACT_APP_YBUG_ID=3f1jrxvzrhkn1b975t8b + + +REACT_APP_MAIL_SRV=https://backend.sovryn.app/ +REACT_APP_MAILCHIMP=https://gmail.us17.list-manage.com/subscribe/post?u=e66850f0b51ebbdbe1f2c3e36&id=ef5d452839 diff --git a/.env.testnet b/.env.testnet index aa0cae499..232fbffb7 100644 --- a/.env.testnet +++ b/.env.testnet @@ -2,3 +2,13 @@ GENERATE_SOURCEMAP=false REACT_APP_NETWORK=testnet REACT_APP_SENTRY_DSN=https://2981633dd7f04f0d9fb3facf8c1332b8@o459269.ingest.sentry.io/5458134 + +REACT_APP_PORTIS_ID=469a25c8-1101-4c57-823d-c47cb328f788 +REACT_APP_YBUG_ID=3f1jrxvzrhkn1b975t8b + +REACT_APP_MAIL_SRV=https://testnet.sovryn.app/backend/ + +REACT_APP_MAILCHIMP=https://gmail.us17.list-manage.com/subscribe/post?u=e66850f0b51ebbdbe1f2c3e36&id=ef5d452839 + +REACT_APP_WHITELIST=false +REACT_APP_WHITELIST_TOKEN= diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..f6c3f6b3c --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +src/libs diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..dd84ea782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/test-successfull-build.yml b/.github/workflows/test-successfull-build.yml new file mode 100644 index 000000000..60508dfff --- /dev/null +++ b/.github/workflows/test-successfull-build.yml @@ -0,0 +1,21 @@ +name: Test if builds successfully +on: + pull_request: + branches: [development] + push: + branches: [development] + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v2.3.1 + with: + persist-credentials: false + + - name: Install and Build 🔧 + run: | + yarn --frozen-lockfile + yarn build diff --git a/.nvmrc b/.nvmrc index 03128968b..e1fcd1ea2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/dubnium +lts/erbium diff --git a/.prettierrc b/.prettierrc index b88daf722..13607f5e6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,5 +5,6 @@ "semi": true, "singleQuote": true, "trailingComma": "all", - "arrowParens": "avoid" + "arrowParens": "avoid", + "endOfLine": "lf" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 29a9cba5d..cc9f76cee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,5 +21,8 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "jest.autoEnable": false, - "jest.runAllTestsFirst": false + "jest.runAllTestsFirst": false, + "files.eol": "\n", + "eslint.alwaysShowStatus": true, + "eslint.packageManager": "yarn" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..b9ff0bff9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 DistributedCollective + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/webpack.config.js b/config/webpack.config.js index dca7cf721..43f62d8cc 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -23,6 +23,7 @@ const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin'); const typescriptFormatter = require('react-dev-utils/typescriptFormatter'); const SentryWebpackPlugin = require('@sentry/webpack-plugin'); +const SriPlugin = require('webpack-subresource-integrity'); const postcssNormalize = require('postcss-normalize'); @@ -195,6 +196,7 @@ module.exports = function (webpackEnv) { // this defaults to 'window', but by setting it to 'this' then // module chunks which are built will work in web workers as well. globalObject: 'this', + crossOriginLoading: 'anonymous', }, optimization: { minimize: isEnvProduction, @@ -387,6 +389,7 @@ module.exports = function (webpackEnv) { }, }, ], + ['@babel/plugin-proposal-object-rest-spread'], ], // This is a feature of `babel-loader` for webpack (not Babel itself). // It enables caching results in ./node_modules/.cache/babel-loader/ @@ -666,6 +669,11 @@ module.exports = function (webpackEnv) { include: '.', ignore: ['node_modules', 'config', 'internals', 'scripts'], }), + // Sri only enabled in production as it can interfere with hot reloading + isEnvProduction && + new SriPlugin({ + hashFuncNames: ['sha256', 'sha384'], + }), ].filter(Boolean), // Some libraries import Node modules but don't use them in the browser. // Tell webpack to provide empty mocks for them so importing them works. diff --git a/package.json b/package.json index 961ad9fa9..4fd194717 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sovryn-app", - "version": "0.1.0", + "version": "0.2.0", "private": true, "dependencies": { "@babel/core": "7.9.0", @@ -9,6 +9,7 @@ "@fortawesome/fontawesome-svg-core": "1.2.30", "@fortawesome/free-solid-svg-icons": "5.14.0", "@fortawesome/react-fontawesome": "0.1.11", + "@portis/web3": "2.0.0-beta.59", "@reduxjs/toolkit": "1.3.2", "@rsksmart/rsk3": "0.3.4", "@sentry/react": "5.25.0", @@ -30,6 +31,14 @@ "@types/webpack-env": "1.15.1", "@typescript-eslint/eslint-plugin": "^2.10.0", "@typescript-eslint/parser": "^2.10.0", + "@visx/gradient": "1.0.0", + "@visx/group": "1.0.0", + "@visx/mock-data": "1.0.0", + "@visx/responsive": "1.1.0", + "@visx/scale": "1.1.0", + "@visx/shape": "1.2.0", + "@visx/text": "1.1.0", + "@visx/tooltip": "1.1.0", "@walletconnect/web3-provider": "1.0.3", "babel-eslint": "10.1.0", "babel-jest": "^24.9.0", @@ -37,15 +46,19 @@ "babel-plugin-named-asset-import": "^0.3.6", "babel-preset-react-app": "^9.1.2", "blockies-ts": "1.0.0", - "bootstrap": "4.5.2", + "bootstrap": "4.5.3", "camelcase": "^5.3.1", "case-sensitive-paths-webpack-plugin": "2.3.0", + "clsx": "1.1.1", + "core-js": "3.7.0", "cross-env": "7.0.2", "css-loader": "3.4.2", + "d3-array": "2.8.0", "debounce": "1.2.0", "deep-equal": "2.0.3", "dotenv": "8.2.0", "dotenv-expand": "5.1.0", + "eccrypto-js": "5.3.0", "env-cmd": "10.1.0", "eslint": "^6.6.0", "eslint-config-prettier": "6.10.1", @@ -80,6 +93,7 @@ "optimize-css-assets-webpack-plugin": "5.0.3", "plop": "2.6.0", "pnp-webpack-plugin": "1.6.4", + "popper.js": "1.16.1", "postcss-flexbugs-fixes": "4.1.0", "postcss-loader": "3.0.0", "postcss-normalize": "8.0.1", @@ -87,27 +101,38 @@ "postcss-safe-parser": "4.0.1", "prettier": "2.0.1", "prop-types": "15.7.2", + "qrcode.react": "1.0.0", + "raf": "3.4.1", "react": "16.13.0", "react-app-polyfill": "^1.0.6", "react-bootstrap": "1.3.0", + "react-bootstrap-icons": "1.0.0", + "react-copy-to-clipboard": "5.0.2", "react-dev-utils": "^10.2.1", "react-dom": "16.13.0", "react-helmet-async": "1.0.4", "react-i18next": "11.3.4", + "react-mailchimp-subscribe": "2.1.0", "react-redux": "7.2.0", "react-router-dom": "5.1.2", "react-scripts": "3.4.1", "react-table": "7.5.1", "react-test-renderer": "16.13.0", + "react-transition-group": "4.4.1", + "reactjs-localstorage": "1.0.1", "redux-injectors": "1.3.0", "redux-saga": "1.1.3", "resolve": "1.15.0", "resolve-url-loader": "3.1.1", "sanitize.css": "11.0.0", "sass-loader": "8.0.2", + "secp256k1": "4.0.2", "semver": "6.3.0", "serve": "11.3.0", "shelljs": "0.8.3", + "socket.io-client": "2.3.1", + "source-map-explorer": "2.5.0", + "storage-factory": "0.1.1", "style-loader": "0.23.1", "styled-components": "5.1.1", "stylelint": "13.2.1", @@ -125,7 +150,8 @@ "webpack": "4.42.0", "webpack-dev-server": "3.10.3", "webpack-manifest-plugin": "2.2.0", - "workbox-webpack-plugin": "4.3.1" + "workbox-webpack-plugin": "4.3.1", + "yarn": "1.22.10" }, "scripts": { "start": "node scripts/start.js", @@ -141,7 +167,8 @@ "lint:fix": "yarn eslint -- --fix src", "lint:css": "stylelint src/**/*.css", "generate": "cross-env TS_NODE_PROJECT='./internals/ts-node.tsconfig.json' plop --plopfile internals/generators/plopfile.ts", - "prettify": "prettier --write" + "prettify": "prettier --write", + "analyze": "source-map-explorer 'build/static/js/*.js'" }, "browserslist": { "production": [ @@ -156,8 +183,8 @@ ] }, "engines": { - "npm": ">=6.4.1", - "node": ">=10.13.0" + "npm": ">=6.14.0", + "node": ">=12.19.0" }, "lint-staged": { "*.{ts,tsx,js,jsx}": [ @@ -237,9 +264,12 @@ } }, "devDependencies": { + "@babel/plugin-proposal-object-rest-spread": "7.12.1", "@sentry/webpack-plugin": "1.13.0", "@types/mathjs": "6.0.5", - "node-sass": "4.14.1" + "@types/socket.io-client": "1.4.34", + "node-sass": "4.14.1", + "webpack-subresource-integrity": "1.5.2" }, "babel": { "presets": [ diff --git a/public/.htaccess b/public/.htaccess index 6f264dab7..98c31402f 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -23,13 +23,13 @@ # # # FileETag None -# Header unset ETag -# Header unset Pragma -# Header unset Cache-Control -# Header unset Last-Modified -# Header set Pragma "no-cache" -# Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate" -# Header set Expires "Mon, 10 Apr 1972 00:00:00 GMT" +# Index unset ETag +# Index unset Pragma +# Index unset Cache-Control +# Index unset Last-Modified +# Index set Pragma "no-cache" +# Index set Cache-Control "max-age=0, no-cache, no-store, must-revalidate" +# Index set Expires "Mon, 10 Apr 1972 00:00:00 GMT" # # #------------ diff --git a/public/index.html b/public/index.html index 081fb7f44..3e5a40c57 100644 --- a/public/index.html +++ b/public/index.html @@ -2,13 +2,19 @@ - + + - + + -
is loading...
+ + <% if (process.env.REACT_APP_YBUG_ID) { %> + + + + <% } %> diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg new file mode 100644 index 000000000..d277e6e9b --- /dev/null +++ b/public/safari-pinned-tab.svg @@ -0,0 +1,42 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + + + diff --git a/scripts/build.js b/scripts/build.js index 967cf0bd8..d1ee2fcb4 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -88,7 +88,6 @@ checkBrowsers(paths.appPath, isInteractive) WARN_AFTER_BUNDLE_GZIP_SIZE, WARN_AFTER_CHUNK_GZIP_SIZE, ); - console.log(); const appPackage = require(paths.appPackageJson); const publicUrl = paths.publicUrlOrPath; diff --git a/src/app/components/ActiveBorrowTable/BorrowAmount.tsx b/src/app/components/ActiveBorrowTable/BorrowAmount.tsx new file mode 100644 index 000000000..2fd7709c1 --- /dev/null +++ b/src/app/components/ActiveBorrowTable/BorrowAmount.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { Asset } from 'types/asset'; +import { weiTo4 } from 'utils/blockchain/math-helpers'; + +interface Props { + amount: string; + asset: Asset; +} + +export function BorrowAmount({ amount, asset }: Props) { + const loading = false; + return ( + + {weiTo4(amount)} {asset}{' '} + + ); +} diff --git a/src/app/components/ActiveBorrowTable/CollateralAmount.tsx b/src/app/components/ActiveBorrowTable/CollateralAmount.tsx new file mode 100644 index 000000000..dfaa01282 --- /dev/null +++ b/src/app/components/ActiveBorrowTable/CollateralAmount.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { Asset } from 'types/asset'; +import { weiTo4 } from 'utils/blockchain/math-helpers'; + +interface Props { + amount: string; + asset: Asset; +} + +export function CollateralAmount({ amount, asset }: Props) { + const loading = false; + return ( + + {weiTo4(amount)} {asset}{' '} + + ); +} diff --git a/src/app/components/ActiveBorrowTable/index.tsx b/src/app/components/ActiveBorrowTable/index.tsx new file mode 100644 index 000000000..de4ac2ac6 --- /dev/null +++ b/src/app/components/ActiveBorrowTable/index.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { useTable, useSortBy } from 'react-table'; +import { Icon, Text } from '@blueprintjs/core'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; + +import { AssetsDictionary } from 'utils/dictionaries/assets-dictionary'; +import { actions } from 'app/containers/LendBorrowSovryn/slice'; + +import { InterestAPR } from '../ActiveUserLoanContainer/components/InterestAPR'; +import { DisplayDate } from '../ActiveUserLoanContainer/components/DisplayDate'; +import { BorrowAmount } from './BorrowAmount'; +import { CollateralAmount } from './CollateralAmount'; + +interface Props { + data: any; +} + +export function ActiveBorrowTable(props: Props) { + const dispatch = useDispatch(); + const columns = React.useMemo( + () => [ + { + Header: 'Borrowed', + accessor: 'borrowAmount', + sortType: 'alphanumeric', + sortable: true, + }, + { + Header: 'Collateral', + accessor: 'collateralAmount', + sortType: 'alphanumeric', + sortable: true, + }, + { + Header: 'Interest APR', + accessor: 'interestAPR', + sortable: true, + }, + { + Header: 'Payback until', + accessor: 'endTimestamp', + sortable: true, + }, + { + Header: '', + accessor: 'actions', + }, + ], + [], + ); + const data = React.useMemo(() => { + return props.data.map(item => { + return { + id: item.loanId, + borrowAmount: ( + + ), + collateralAmount: ( + + ), + interestAPR: ( + + ), + endTimestamp: , + actions: ( +
+
+ dispatch(actions.openRepayModal(item.loanId))} + > + Repay + +
+
+ ), + }; + }); + }, [props.data, dispatch]); + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + } = useTable({ columns, data }, useSortBy); + return ( +
+ + + {headerGroups.map(headerGroup => ( + + {headerGroup.headers.map(column => ( + + ))} + + ))} + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => { + return ( + + ); + })} + + ); + })} + +
+ + {column.render('Header')} + {column.sortable && ( + + {column.isSorted ? ( + column.isSortedDesc ? ( + + ) : ( + + ) + ) : ( + + )} + + )} + +
+ {cell.render('Cell')} +
+
+ ); +} + +const StyledRepayButton = styled.button.attrs(_ => ({ type: 'button' }))` + border: 2px solid var(--green); + width: 77px; + height: 32px; + color: var(--green); + background-color: var(--primary); + border-radius: 8px; +`; diff --git a/src/app/components/ActiveLoanRow/index.tsx b/src/app/components/ActiveLoanRow/index.tsx deleted file mode 100644 index a5e4e7f17..000000000 --- a/src/app/components/ActiveLoanRow/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/** - * - * ActiveLoanRow - * - */ -import React from 'react'; - -interface Props {} - -export function ActiveLoanRow(props: Props) { - return
; -} diff --git a/src/app/components/ActiveLoanTable/index.tsx b/src/app/components/ActiveLoanTable/index.tsx deleted file mode 100644 index 94669ce5d..000000000 --- a/src/app/components/ActiveLoanTable/index.tsx +++ /dev/null @@ -1,253 +0,0 @@ -/** - * - * ActiveLoanTable - * - */ -import React, { useEffect, useState } from 'react'; -import { useTable, useSortBy } from 'react-table'; -import { CloseTradingPositionHandler } from '../../containers/CloseTradingPositionHandler'; -import { TopUpTradingPositionHandler } from '../../containers/TopUpTradingPositionHandler'; -import { DisplayDate } from '../DisplayDate'; -import { CurrentMargin } from '../CurrentMargin'; -import { InterestAPR } from '../InterestAPR'; -import { - weiTo2, - weiTo18, - weiToFixed, -} from '../../../utils/blockchain/math-helpers'; -import { symbolByTokenAddress } from '../../../utils/blockchain/contract-helpers'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import styled from 'styled-components'; -import { - faArrowAltCircleDown, - faArrowAltCircleUp, - faSort, - faSortUp, - faSortDown, -} from '@fortawesome/free-solid-svg-icons'; - -interface Props { - data: any; - activeTrades: boolean; -} - -export function ActiveLoanTable(props: Props) { - const [positionCloseModalOpen, setPositionCloseModalOpen] = useState(false); - const [positionMarginModalOpen, setPositionMarginModalOpen] = useState(false); - const [selectedItem, setSelectedItem] = useState(props.data[0]); - - const data = React.useMemo(() => { - return props.data.map(item => { - return { - item: item, - icon: - symbolByTokenAddress(item.collateralToken) === 'BTC' ? ( - - ) : ( - - ), - positionSize: `${parseFloat(weiToFixed(item.collateral, 4))} - ${symbolByTokenAddress(item.collateralToken)}`, - currentMargin: ( - - ), - interestAPR: ( - - ), - startPrice: - symbolByTokenAddress(item.collateralToken) === 'BTC' - ? `$ ${parseFloat(weiTo2(item.startRate)).toLocaleString('en')}` - : `$ ${(1 / parseFloat(weiTo18(item.startRate))).toLocaleString( - 'en', - )}`, - endDate: , - borrowed: '', - startMargin: '', - maintenanceMargin: '', - currentPrice: '', - liquidationPrice: '', - topUp: ( - { - setPositionMarginModalOpen(true); - setSelectedItem(item); - }} - > - Top-Up - - ), - close: ( - { - setPositionCloseModalOpen(true); - setSelectedItem(item); - }} - > - Close - - ), - }; - }); - }, [props.data]); - - useEffect(() => { - // Resets selected item in modals if items was changed. - if (selectedItem && selectedItem.loanId) { - const loan = props.data.find(item => item.loanId === selectedItem.loanId); - setSelectedItem(loan); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.data]); - - const columns = React.useMemo( - () => [ - { - Header: '', - accessor: 'icon', - sortable: false, - }, - { - Header: 'Position Size', - accessor: 'positionSize', - sortType: 'alphanumeric', - sortable: true, - }, - { - Header: 'Current Margin', - accessor: 'currentMargin', - sortType: 'alphanumeric', - sortable: true, - }, - { - Header: 'Interest APR', - accessor: 'interestAPR', - }, - { - Header: 'Start Price', - accessor: 'startPrice', - }, - { - Header: 'Renewal Date', - accessor: 'endDate', - sortable: true, - }, - { - Header: '', - accessor: 'topUp', - }, - { - Header: '', - accessor: 'close', - }, - ], - [], - ); - - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, - prepareRow, - } = useTable({ columns, data }, useSortBy); - - return ( - <> - - - {headerGroups.map(headerGroup => ( - - {headerGroup.headers.map(column => ( - - ))} - - ))} - - - {rows.map(row => { - prepareRow(row); - return ( - - {row.cells.map(cell => { - return ( - - ); - })} - - ); - })} - -
- {column.render('Header')} - {column.sortable && ( - - {column.isSorted ? ( - column.isSortedDesc ? ( - - ) : ( - - ) - ) : ( - - )} - - )} -
- {cell.render('Cell')} -
- - setPositionCloseModalOpen(false)} - /> - - {selectedItem && ( - setPositionMarginModalOpen(false)} - /> - )} - - ); -} - -const TopUpButton = styled.button` - border: 1px solid var(--Green); - width: 77px; - height: 32px; - color: var(--Green); - background-color: var(--bg-secondary); -`; - -const CloseButton = styled.button` - border: 1px solid var(--Red); - width: 77px; - height: 32px; - color: var(--Red); - background-color: var(--bg-secondary); -`; diff --git a/src/app/components/ActiveUserLoan/index.tsx b/src/app/components/ActiveUserLoan/index.tsx deleted file mode 100644 index 06a32cb23..000000000 --- a/src/app/components/ActiveUserLoan/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/** - * - * ActiveUserLoans - * - */ -import React, { useState } from 'react'; -import { ActiveLoan } from 'app/hooks/trading/useGetActiveLoans'; -import { weiTo4 } from '../../../utils/blockchain/math-helpers'; -import { symbolByTokenAddress } from '../../../utils/blockchain/contract-helpers'; -import { CloseTradingPositionHandler } from '../../containers/CloseTradingPositionHandler/Loadable'; -import { TopUpTradingPositionHandler } from '../../containers/TopUpTradingPositionHandler/Loadable'; - -interface Props { - item: ActiveLoan; -} - -export function ActiveUserLoan({ item }: Props) { - const date = (timestamp: string) => - new Date(Number(timestamp) * 1e3).toLocaleDateString(); - - const [positionCloseModalOpen, setPositionCloseModalOpen] = useState(false); - const [positionMarginModalOpen, setPositionMarginModalOpen] = useState(false); - - return ( - <> -
-
-
-
Loan Token
-
{symbolByTokenAddress(item.loanToken)}
-
-
-
Collateral Token
-
{symbolByTokenAddress(item.collateralToken)}
-
- {/*
*/} - {/*
Interest deposit remaining
*/} - {/*
*/} - {/* {weiTo4(item.interestDepositRemaining)}{' '}*/} - {/* {symbolByTokenAddress(item.loanToken)}*/} - {/*
*/} - {/*
*/} -
-
Borrowed amount
-
- {weiTo4(item.principal)} {symbolByTokenAddress(item.loanToken)} -
-
-
-
Position size
-
- {weiTo4(item.collateral)}{' '} - {symbolByTokenAddress(item.collateralToken)} -
-
- {/*
*/} - {/*
InterestOwedPerDay
*/} - {/*
*/} - {/* {weiTo4(item.interestOwedPerDay)}{' '}*/} - {/* {symbolByTokenAddress(item.loanToken)}*/} - {/*
*/} - {/*
*/} -
-
- - -
-
-
-
-
-
Start Rate
-
{weiTo4(item.startRate)}%
-
-
-
Start Margin
-
{weiTo4(item.startMargin)}%
-
-
-
Maintenance Margin
-
{weiTo4(item.maintenanceMargin)}%
-
-
-
Current Margin
-
{weiTo4(item.currentMargin)}%
-
-
-
End date
-
{date(item.endTimestamp)}
-
-
-
- setPositionCloseModalOpen(false)} - /> - setPositionMarginModalOpen(false)} - /> - - ); -} diff --git a/src/app/components/ActiveUserLoanContainer/components/ActiveLoanExpandedRow/index.tsx b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanExpandedRow/index.tsx new file mode 100644 index 000000000..f7ededbaf --- /dev/null +++ b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanExpandedRow/index.tsx @@ -0,0 +1,45 @@ +/** + * + * ActiveLoanExpandedRow + * + */ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { translations } from 'locales/i18n'; +import { numberToPercent, numberToUSD } from 'utils/display-text/format'; + +export function ActiveLoanExpandedRow(props) { + const { t } = useTranslation(); + + return ( + <> + + + {t(translations.activeLoan.expandedRow.leverage)} + {t(translations.activeLoan.expandedRow.startMargin)} + {t(translations.activeLoan.expandedRow.mainenanceMargin)} + {t(translations.activeLoan.expandedRow.currentPrice)} + {t(translations.activeLoan.expandedRow.liquidationPrice)} + {t(translations.activeLoan.expandedRow.renewalDate)} + + + + + {props.data.leverage}X + {numberToPercent(props.data.startMargin, 2)} + {props.data.maintenanceMargin} + {numberToUSD(props.data.currentPrice, 2)} + {props.data.liquidationPrice} + {props.data.endDate} + + + ); +} diff --git a/src/app/components/ActiveUserLoanContainer/components/ActiveLoanLiquidation/index.tsx b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanLiquidation/index.tsx new file mode 100644 index 000000000..008cd1b57 --- /dev/null +++ b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanLiquidation/index.tsx @@ -0,0 +1,54 @@ +/** + * + * ActiveLoanLiquidation + * + */ + +import React, { useState, useEffect } from 'react'; +import { weiTo18, toWei } from 'utils/blockchain/math-helpers'; +import { useBorrowLiquidationPrice } from '../../../../hooks/trading/useBorrowLiquidationPrice'; +import { TradingPosition } from 'types/trading-position'; +import { Asset } from 'types/asset'; +import { leverageFromMargin } from 'utils/blockchain/leverage-from-start-margin'; + +interface Props { + asset: Asset; + isLong: boolean; + item: any; // todo add type + currentPrice: any; // todo fix type +} + +export function ActiveLoanLiquidation(props: Props) { + const [danger, setDanger] = useState(false); + + const priceInWei = props.isLong + ? props.item.startRate + : toWei(1 / parseFloat(weiTo18(props.item.startRate))); + const leverage = leverageFromMargin(props.item.startMargin); + const position = props.isLong ? TradingPosition.LONG : TradingPosition.SHORT; + + const { value: liquidationPrice } = useBorrowLiquidationPrice( + props.asset, + priceInWei, + leverage, + position, + ); + + useEffect(() => { + parseFloat(liquidationPrice) < parseFloat(props.currentPrice) * 1.1 && + parseFloat(liquidationPrice) > parseFloat(props.currentPrice) * 0.9 + ? setDanger(true) + : setDanger(false); + }, [liquidationPrice, props.currentPrice]); + + return ( + + {parseFloat(weiTo18(liquidationPrice)).toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + ); +} diff --git a/src/app/components/ActiveUserLoanContainer/components/ActiveLoanTableContainer/index.tsx b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanTableContainer/index.tsx new file mode 100644 index 000000000..7e102059b --- /dev/null +++ b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanTableContainer/index.tsx @@ -0,0 +1,231 @@ +/** + * + * ActiveLoanTableContainer + * + */ +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { Icon } from '@blueprintjs/core'; +import { useTranslation } from 'react-i18next'; +import { translations } from 'locales/i18n'; +import { CloseTradingPositionHandler } from '../../../../containers/CloseTradingPositionHandler'; +import { TopUpTradingPositionHandler } from '../../../../containers/TopUpTradingPositionHandler'; +import { ActiveLoanLiquidation } from '../ActiveLoanLiquidation'; +import { ActiveLoanTableMobile } from '../ActiveLoanTableMobile'; +import { ActiveLoanTableDesktop } from '../ActiveLoanTableDesktop'; +import { + assetByTokenAddress, + symbolByTokenAddress, +} from 'utils/blockchain/contract-helpers'; +import { leverageFromMargin } from '../../../../../utils/blockchain/leverage-from-start-margin'; +import { Asset } from 'types/asset'; +import { + formatAsBTCPrice, + stringToPercent, + formatAsNumber, + calculateProfit, +} from 'utils/display-text/format'; +import { fromWei } from '../../../../../utils/blockchain/math-helpers'; +import { TradingPairDictionary } from '../../../../../utils/dictionaries/trading-pair-dictionary'; +import { usePriceFeeds_tradingPairRates } from '../../../../hooks/price-feeds/usePriceFeeds_tradingPairRates'; +import { AssetsDictionary } from '../../../../../utils/dictionaries/assets-dictionary'; +import { CachedAssetRate } from '../../../../containers/WalletProvider/types'; + +interface Props { + data: any; + activeTrades: boolean; +} + +function getAssetPrice(source: Asset, target: Asset, items: CachedAssetRate[]) { + const item = items.find( + item => item.source === source && item.target === target, + ); + return item?.value?.rate || '0'; +} + +export function ActiveLoanTableContainer(props: Props) { + const [positionCloseModalOpen, setPositionCloseModalOpen] = useState(false); + const [positionMarginModalOpen, setPositionMarginModalOpen] = useState(false); + const [selectedItem, setSelectedItem] = useState(props.data[0]); + const [expandedItem, setExpandedItem] = useState(''); + const [expandedId, setExpandedId] = useState(''); + const { t } = useTranslation(); + + const items = usePriceFeeds_tradingPairRates(); + + const data = React.useMemo(() => { + return props.data.map((item, i) => { + const currentMargin = formatAsNumber(item.currentMargin, 4); + const startMargin = formatAsNumber(item.startMargin, 4); + const currency = symbolByTokenAddress(item.collateralToken); + const loanAsset = assetByTokenAddress(item.loanToken); + const collateralAsset = assetByTokenAddress(item.collateralToken); + + const isLong = TradingPairDictionary.longPositionTokens.includes( + loanAsset, + ); + const startPrice = formatAsBTCPrice(item.startRate, isLong); + const currentRate = parseFloat( + fromWei(getAssetPrice(loanAsset, collateralAsset, items)), + ); + const currentPrice = isLong ? 1 / currentRate : currentRate; + + const profit = calculateProfit( + startPrice, + currentPrice, + isLong, + item.collateral, + item.startRate, + ); + + return { + id: item.loanId, + pair: AssetsDictionary.get(loanAsset).symbol, + currency: currency, + icon: isLong ? 'LONG' : 'SHORT', + positionSize: formatAsNumber(item.collateral, 4), + positionInUSD: isLong + ? formatAsNumber(item.collateral, 4) * currentPrice + : formatAsNumber(item.collateral, 4), + positionCurrency: symbolByTokenAddress(item.collateralToken), + currentMargin: currentMargin, + startMargin: startMargin, + marginDiff: ((currentMargin - startMargin) * 100) / startMargin, + interestAPR: ( + ((item.interestOwedPerDay * 365) / item.principal) * + 100 + ).toFixed(2), + startPrice, + startRate: item.startRate, + endDate: new Date(Number(item.endTimestamp) * 1e3).toLocaleString( + 'en-GB', + { + timeZone: 'GMT', + }, + ), + leverage: leverageFromMargin(item.startMargin), + profit: + isNaN(profit) || !isFinite(profit) || !currentPrice ? null : profit, + liquidationPrice: ( + + ), + currentPrice, + maintenanceMargin: stringToPercent(item.maintenanceMargin, 2), + mobileActions: ( +
+ { + setPositionMarginModalOpen(true); + setSelectedItem(item); + }} + /> + { + setPositionCloseModalOpen(true); + setSelectedItem(item); + }} + /> +
+ ), + actions: ( +
+
+ { + setPositionMarginModalOpen(true); + setSelectedItem(item); + }} + > + {t(translations.activeLoan.table.container.topUp)} + +
+
+ { + setPositionCloseModalOpen(true); + setSelectedItem(item); + }} + > + {t(translations.activeLoan.table.container.close)} + +
+
+ ), + }; + }); + }, [props.data, t, items]); + + useEffect(() => { + // Resets selected item in modals if items was changed. + if (selectedItem && selectedItem.loanId) { + const loan = props.data.find(item => item.loanId === selectedItem.loanId); + setSelectedItem(loan); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.data]); + + useEffect(() => { + if (!expandedId) { + setExpandedItem(''); + } + }, [expandedId]); + + return ( + <> + + + setPositionCloseModalOpen(false)} + /> + + {selectedItem && ( + setPositionMarginModalOpen(false)} + /> + )} + + ); +} + +const TopUpButton = styled.button.attrs(_ => ({ type: 'button' }))` + border: 2px solid var(--green); + width: 77px; + height: 32px; + color: var(--green); + background-color: var(--primary); + border-radius: 8px; +`; + +const CloseButton = styled.button.attrs(_ => ({ type: 'button' }))` + border: 2px solid var(--red); + width: 77px; + height: 32px; + color: var(--red); + background-color: var(--primary); + border-radius: 8px; +`; diff --git a/src/app/components/ActiveUserLoanContainer/components/ActiveLoanTableDesktop/index.tsx b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanTableDesktop/index.tsx new file mode 100644 index 000000000..29a6daaba --- /dev/null +++ b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanTableDesktop/index.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import { ActiveLoanExpandedRow } from '../ActiveLoanExpandedRow'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Icon } from '@blueprintjs/core'; +import { + formatAsBTC, + numberToUSD, + numberToPercent, +} from 'utils/display-text/format'; +import { + faLongArrowAltUp, + faLongArrowAltDown, +} from '@fortawesome/free-solid-svg-icons'; + +interface Props { + data: any; + setExpandedId: any; + setExpandedItem: any; + expandedId: any; + expandedItem: any; +} + +const useSortableData = ( + items, + config = { key: 'none', direction: 'none' }, +) => { + const [sortConfig, setSortConfig] = React.useState(config); + + const sortedItems = React.useMemo(() => { + let sortableItems = [...items]; + if (sortConfig !== null) { + sortableItems.sort((a, b) => { + if (a[sortConfig.key] < b[sortConfig.key]) { + return sortConfig.direction === 'ascending' ? -1 : 1; + } + if (a[sortConfig.key] > b[sortConfig.key]) { + return sortConfig.direction === 'ascending' ? 1 : -1; + } + return 0; + }); + } + return sortableItems; + }, [items, sortConfig]); + + const requestSort = key => { + let direction = 'ascending'; + if ( + sortConfig && + sortConfig.key === key && + sortConfig.direction === 'ascending' + ) { + direction = 'descending'; + } + setSortConfig({ key, direction }); + }; + + return { items: sortedItems, requestSort, sortConfig }; +}; + +export function ActiveLoanTableDesktop(props: Props) { + const { items, requestSort, sortConfig } = useSortableData(props.data); + function getIcons(name) { + if (sortConfig.key !== name) { + return ; + } else if (sortConfig.direction === 'ascending') { + return ; + } else { + return ; + } + } + + return ( +
+ + + + + + + + + + + + + {items.map(item => { + const expanded = props.expandedId === item.id; + return ( + + { + if (!expanded) { + props.setExpandedItem(item); + props.setExpandedId(item.id); + } else { + props.setExpandedId(''); + } + }} + className={`cursor-pointer ${ + props.expandedId && !expanded && 'opaque' + }`} + > + + + + + + + + + {props.expandedId === item.id && ( + props.setExpandedId('')} + /> + )} + + ); + })} + +
requestSort('icon')}>{getIcons('icon')} requestSort('positionInUSD')}> + Position Size {getIcons('positionInUSD')} + requestSort('currentMargin')}> + Current Margin {getIcons('currentMargin')} + requestSort('interestAPR')}> + Interest APR {getIcons('interestAPR')} + requestSort('startMargin')}> + Start Price {getIcons('startMargin')} + requestSort('profit')}> + Profit / Loss {getIcons('profit')} + +
+ {item.icon === 'LONG' && ( + + )} + {item.icon === 'SHORT' && ( + + )}{' '} + {item.pair} + {formatAsBTC(item.positionSize, item.currency)} + {numberToPercent(item.currentMargin, 2)} + 0 ? 'text-green' : 'text-red' + }`} + > +
+ 0 + ? faLongArrowAltUp + : faLongArrowAltDown + } + /> + {numberToPercent(item.marginDiff, 2)} +
+
+
{item.interestAPR} %{numberToUSD(item.startPrice, 2)} 0 ? 'text-green' : 'text-red'}`} + > + {numberToUSD(item.profit, 4)} + {item.actions}
+
+ ); +} diff --git a/src/app/components/ActiveUserLoanContainer/components/ActiveLoanTableMobile/index.tsx b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanTableMobile/index.tsx new file mode 100644 index 000000000..856314607 --- /dev/null +++ b/src/app/components/ActiveUserLoanContainer/components/ActiveLoanTableMobile/index.tsx @@ -0,0 +1,107 @@ +/** + * + * ActiveLoanTableMobile + * + */ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { translations } from 'locales/i18n'; +import { ExpandedRowMobile } from '../ExpandedRowMobile'; +import { Icon } from '@blueprintjs/core'; +import { formatAsBTC, numberToUSD } from 'utils/display-text/format'; + +interface Props { + data: Array<{ + actions: any; + currentMargin: any; + currentPrice: string; + endDate: any; + icon: any; + id: string; + interestAPR: any; + leverage: string; + liquidationPrice: any; + maintenanceMargin: string; + mobileActions: any; + positionSize: string; + profit: any; + startMargin: string; + startPrice: string; + currency: any; + }>; + setExpandedId: any; + expandedId: string; +} + +export function ActiveLoanTableMobile(props: Props) { + const { t } = useTranslation(); + const rows = props.data.map(item => { + const expanded = item.id === props.expandedId || !props.expandedId; + return ( + +
{ + props.setExpandedId(props.expandedId === item.id ? '' : item.id); + }} + > +
+ {item.icon === 'LONG' && ( + + )} + {item.icon === 'SHORT' && ( + + )} +
+
+ {formatAsBTC(item.positionSize, item.currency)} +
+
0 ? 'text-green' : 'text-red'}`} + > + {numberToUSD(item.profit, 2)} +
+
+ {item.mobileActions} +
+
+ {item.id === props.expandedId && ( + props.setExpandedId('')} + /> + )} +
+ ); + }); + + return ( +
+
+
+
+
+ {t(translations.activeLoan.table.positionSize)} +
+
{t(translations.activeLoan.table.profit)}
+
+
+ {rows} +
+
+ ); +} diff --git a/src/app/components/DisplayDate/index.tsx b/src/app/components/ActiveUserLoanContainer/components/DisplayDate/index.tsx similarity index 100% rename from src/app/components/DisplayDate/index.tsx rename to src/app/components/ActiveUserLoanContainer/components/DisplayDate/index.tsx diff --git a/src/app/components/ActiveUserLoanContainer/components/ExpandedRowMobile/index.tsx b/src/app/components/ActiveUserLoanContainer/components/ExpandedRowMobile/index.tsx new file mode 100644 index 000000000..cb9cf140c --- /dev/null +++ b/src/app/components/ActiveUserLoanContainer/components/ExpandedRowMobile/index.tsx @@ -0,0 +1,62 @@ +/** + * + * ExpandedRowMobile + * + */ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { translations } from 'locales/i18n'; +import { numberToUSD, numberToPercent } from 'utils/display-text/format'; + +export function ExpandedRowMobile(props) { + const { t } = useTranslation(); + const s = translations.expandedRowMobile; + + return ( +
+
+
{t(s.currentMargin)}
+
{t(s.interestAPR)}
+
{t(s.startPrice)}
+
+
+
+ {numberToPercent(props.item.currentMargin, 2)} +
+
{props.item.interestAPR} %
+
{numberToUSD(props.item.startPrice, 2)}
+
+
+
{t(s.leverage)}
+
{t(s.startMargin)}
+
{t(s.maintenanceMargin)}
+
+
+
{props.item.leverage}X
+
+ {numberToPercent(props.item.startMargin, 2)} +
+
{props.item.maintenanceMargin}
+
+
+
{t(s.currentPrice)}
+
{t(s.liquidationPrice)}
+
+
+
+
{numberToUSD(props.item.currentPrice, 2)}
+
{props.item.liquidationPrice}
+
+
+
+
{t(s.renewalDate)}
+
+
+
+
{props.item.endDate}
+
{props.item.mobileActions}
+
+
+ ); +} diff --git a/src/app/components/InterestAPR/index.tsx b/src/app/components/ActiveUserLoanContainer/components/InterestAPR/index.tsx similarity index 100% rename from src/app/components/InterestAPR/index.tsx rename to src/app/components/ActiveUserLoanContainer/components/InterestAPR/index.tsx diff --git a/src/app/components/Announcement/index.tsx b/src/app/components/Announcement/index.tsx new file mode 100644 index 000000000..a3920a243 --- /dev/null +++ b/src/app/components/Announcement/index.tsx @@ -0,0 +1,25 @@ +/** + * + * Announcement + * + */ + +import React from 'react'; +import { Trans } from 'react-i18next'; +import { translations } from 'locales/i18n'; +import { InfoBox } from '../InfoBox'; + +export function Announcement() { + return ( + ]} + /> + } + localStorageRef="txLimitInfo" + /> + ); +} diff --git a/src/app/components/AssetWalletBalance/index.tsx b/src/app/components/AssetWalletBalance/index.tsx index 8c535979a..0200e9b9b 100644 --- a/src/app/components/AssetWalletBalance/index.tsx +++ b/src/app/components/AssetWalletBalance/index.tsx @@ -4,11 +4,13 @@ * */ import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { Asset } from 'types/asset'; -import { LoadableValue } from '../LoadableValue'; import { weiToFixed } from 'utils/blockchain/math-helpers'; -import { useTokenBalanceOf } from 'app/hooks/useTokenBalanceOf'; +import { useAssetBalanceOf } from 'app/hooks/useAssetBalanceOf'; import { useIsConnected } from 'app/hooks/useAccount'; +import { translations } from 'locales/i18n'; +import { LoadableValue } from '../LoadableValue'; interface Props { asset: Asset; @@ -16,7 +18,8 @@ interface Props { } export function AssetWalletBalance(props: Props) { - const { value, loading } = useTokenBalanceOf(props.asset); + const { value, loading } = useAssetBalanceOf(props.asset); + const { t } = useTranslation(); const connected = useIsConnected(); useEffect(() => { @@ -26,27 +29,19 @@ export function AssetWalletBalance(props: Props) { }, [props, value]); return ( - <> -
Account Balance
-
- - - {props.asset} - {' '} - {weiToFixed(value, 4)} - - ) : ( - - Connect to wallet - - ) - } - loading={loading} - /> +
+
+ {t(translations.assetWalletBalance.accountBalance)}
- + {!connected && {t(translations.assetWalletBalance.connect)}} + {connected && ( +
+ {props.asset} + + + +
+ )} +
); } diff --git a/src/app/components/BorrowActivity/Loadable.tsx b/src/app/components/BorrowActivity/Loadable.tsx new file mode 100644 index 000000000..2c2c30b31 --- /dev/null +++ b/src/app/components/BorrowActivity/Loadable.tsx @@ -0,0 +1,12 @@ +/** + * + * Asynchronously loads the component for BorrowActivity + * + */ + +import { lazyLoad } from 'utils/loadable'; + +export const BorrowActivity = lazyLoad( + () => import('./index'), + module => module.BorrowActivity, +); diff --git a/src/app/components/BorrowActivity/index.tsx b/src/app/components/BorrowActivity/index.tsx new file mode 100644 index 000000000..bb8fb057b --- /dev/null +++ b/src/app/components/BorrowActivity/index.tsx @@ -0,0 +1,61 @@ +/** + * + * BorrowActivity + * + */ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { translations } from 'locales/i18n'; +import { useIsConnected } from 'app/hooks/useAccount'; +import { ActiveUserBorrows } from 'app/containers/ActiveUserBorrows'; +import { BorrowHistory } from 'app/containers/BorrowHistory/Loadable'; + +import { Tab } from '../Tab'; +import { SkeletonRow } from '../Skeleton/SkeletonRow'; + +interface Props {} + +export function BorrowActivity(props: Props) { + const { t } = useTranslation(); + const isConnected = useIsConnected(); + const [activeBorrows, setActiveBorrows] = useState(true); + return ( +
+
+

+ {t(translations.borrowActivity.title)} +

+ +
+
+ setActiveBorrows(true)} + /> +
+
+ setActiveBorrows(false)} + /> +
+
+
+
+
+ {!isConnected ? ( + + ) : activeBorrows ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/src/app/components/BorrowForm/index.tsx b/src/app/components/BorrowForm/index.tsx deleted file mode 100644 index 60d529d29..000000000 --- a/src/app/components/BorrowForm/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/** - * - * BorrowForm - * - */ -import React, { useCallback, useEffect, useState } from 'react'; -import { Asset } from '../../../types/asset'; -import { TradingPosition } from '../../../types/trading-position'; -import { InputGroup, Tag } from '@blueprintjs/core'; -import { useWeiAmount } from '../../hooks/useWeiAmount'; -import { SendTxProgress } from '../SendTxProgress'; -import { useApproveAndTrade } from '../../hooks/trading/useApproveAndTrade'; -import { useIsConnected } from '../../hooks/useAccount'; - -interface Props { - asset: Asset; - position: TradingPosition; - leverage: number; - onChange: (weiAmount: string) => void; -} - -/** - * @deprecated Used TradeDialog instead. - * @param props - * @constructor - */ -export function BorrowForm(props: Props) { - const isConnected = useIsConnected(); - - const handleContractToken = useCallback(() => { - if (props.position === TradingPosition.LONG) { - return Asset.DOC; - } - return Asset.BTC; - }, [props.position]); - - const handleCollateralToken = useCallback(() => { - if (props.position === TradingPosition.LONG) { - return Asset.BTC; - } - return Asset.DOC; - }, [props.position]); - - const [contractToken, setContractToken] = useState(handleContractToken); - const [collateralToken, setCollateralToken] = useState(handleCollateralToken); - - useEffect(() => { - setCollateralToken(handleCollateralToken); - setContractToken(handleContractToken); - }, [handleCollateralToken, handleContractToken, props.position, props.asset]); - - const [amount, setAmount] = useState('0'); - const weiAmount = useWeiAmount(amount); - - useEffect(() => { - props.onChange(weiAmount); - }, [props, weiAmount]); - - const { trade, loading, txHash, status, type } = useApproveAndTrade( - contractToken, - collateralToken, - props.leverage, - weiAmount, - ); - - return ( -
-
Trade:
-
- setAmount(e.target.value)} - rightElement={{collateralToken}} - /> -
-
- {/* -
-
- {type !== 'none' && ( - - )} -
-
- ); -} - -BorrowForm.defaultProps = { - onChange: (_: string) => {}, -}; diff --git a/src/app/components/BorrowInterestRate/index.tsx b/src/app/components/BorrowInterestRate/index.tsx index cac404569..f3b303dcd 100644 --- a/src/app/components/BorrowInterestRate/index.tsx +++ b/src/app/components/BorrowInterestRate/index.tsx @@ -7,15 +7,18 @@ import React, { useEffect, useState } from 'react'; import { bignumber } from 'mathjs'; import { Asset } from 'types/asset'; import { weiToFixed } from 'utils/blockchain/math-helpers'; -import { useBorrowInterestRate } from 'app/hooks/trading/useBorrowInterestRate'; +import { useLending_nextBorrowInterestRate } from 'app/hooks/trading/useLending_nextBorrowInterestRate'; import { usePriceFeeds_QueryRate } from 'app/hooks/price-feeds/useQueryRate'; import { LoadableValue } from '../LoadableValue'; +import { FieldGroup } from '../FieldGroup'; +import { DummyField } from '../DummyField'; interface Props { asset: Asset; collateral: Asset; leverage: number; weiAmount: string; + labelColor: string; } export function BorrowInterestRate(props: Props) { @@ -54,17 +57,27 @@ export function BorrowInterestRate(props: Props) { ); }, [totalDeposit, props.leverage]); - const { value, loading } = useBorrowInterestRate(props.asset, borrowAmount); + const { value, loading } = useLending_nextBorrowInterestRate( + props.asset, + borrowAmount, + ); return ( -
-
Interest APR
-
+ + {weiToFixed(value, 2)} %} + value={ + <> + {weiToFixed(value, 2)} % + + } loading={loading} /> -
-
+ + ); } + +BorrowInterestRate.defaultProps = { + labelColor: 'var(--dark-gray)', +}; diff --git a/src/app/components/BorrowLiquidationPrice/index.tsx b/src/app/components/BorrowLiquidationPrice/index.tsx index 07725a5c5..403d6ffed 100644 --- a/src/app/components/BorrowLiquidationPrice/index.tsx +++ b/src/app/components/BorrowLiquidationPrice/index.tsx @@ -10,30 +10,44 @@ import { useBorrowAssetPrice } from 'app/hooks/trading/useBorrowAssetPrice'; import { weiToFixed } from 'utils/blockchain/math-helpers'; import { useBorrowLiquidationPrice } from 'app/hooks/trading/useBorrowLiquidationPrice'; import { LoadableValue } from '../LoadableValue'; +import { FieldGroup } from '../FieldGroup'; +import { DummyField } from '../DummyField'; interface Props { asset: Asset; leverage: number; position: TradingPosition; + labelColor: string; } export function BorrowLiquidationPrice(props: Props) { - const { value: price } = useBorrowAssetPrice(props.asset, Asset.DOC); - const { value, loading } = useBorrowLiquidationPrice( + const { value: price, loading: loadingPrice } = useBorrowAssetPrice( + props.asset, + Asset.DOC, + ); + const { value, loading: loadingLiq } = useBorrowLiquidationPrice( props.asset, price, props.leverage, props.position, ); return ( -
-
Liquidation Price
-
+ + $ {weiToFixed(value, 2)}} - loading={loading} + value={ + <> + $ + {weiToFixed(value, 2)} + + } + loading={loadingPrice || loadingLiq} /> -
-
+ + ); } + +BorrowLiquidationPrice.defaultProps = { + labelColor: 'var(--dar-gray)', +}; diff --git a/src/app/components/CloseModalButton/index.tsx b/src/app/components/CloseModalButton/index.tsx index 8952405ea..6c87def7c 100644 --- a/src/app/components/CloseModalButton/index.tsx +++ b/src/app/components/CloseModalButton/index.tsx @@ -1,12 +1,16 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { translations } from 'locales/i18n'; export function CloseModalButton() { + const { t } = useTranslation(); + return (
- Close X + {t(translations.modal.close)} X
); } diff --git a/src/app/components/CloseTradingPositionForm/index.tsx b/src/app/components/CloseTradingPositionForm/index.tsx index 8b58ad465..a7d6946b6 100644 --- a/src/app/components/CloseTradingPositionForm/index.tsx +++ b/src/app/components/CloseTradingPositionForm/index.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { ActiveLoan } from '../../hooks/trading/useGetActiveLoans'; -import { AssetsDictionary } from '../../../utils/blockchain/assets-dictionary'; +import { AssetsDictionary } from '../../../utils/dictionaries/assets-dictionary'; import { Asset } from '../../../types/asset'; interface SwapOptions {} diff --git a/src/app/components/CurrentMargin/index.tsx b/src/app/components/CurrentMargin/index.tsx deleted file mode 100644 index 7c76e6067..000000000 --- a/src/app/components/CurrentMargin/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { weiTo4 } from '../../../utils/blockchain/math-helpers'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - faLongArrowAltUp, - faLongArrowAltDown, -} from '@fortawesome/free-solid-svg-icons'; - -interface Props { - currentMargin: string; - startMargin: string; -} - -export function CurrentMargin(props: Props) { - const diff = - Number(weiTo4(props.currentMargin)) - Number(weiTo4(props.startMargin)); - - return ( - <> -
{`${weiTo4(props.currentMargin)} %`}
-
0 ? 'Green' : 'Red' }} - > -
- 0 ? faLongArrowAltUp : faLongArrowAltDown} - /> - {` ${diff.toFixed(4)} %`} -
-
- - ); -} diff --git a/src/app/components/DialogButton/Loadable.tsx b/src/app/components/DialogButton/Loadable.tsx new file mode 100644 index 000000000..085534998 --- /dev/null +++ b/src/app/components/DialogButton/Loadable.tsx @@ -0,0 +1,12 @@ +/** + * + * Asynchronously loads the component for TradeButton + * + */ + +import { lazyLoad } from 'utils/loadable'; + +export const DialogButton = lazyLoad( + () => import('./index'), + module => module.DialogButton, +); diff --git a/src/app/components/DialogButton/index.tsx b/src/app/components/DialogButton/index.tsx new file mode 100644 index 000000000..3f5c2ea3d --- /dev/null +++ b/src/app/components/DialogButton/index.tsx @@ -0,0 +1,71 @@ +/** + * + * TradeButton + * + */ +import React from 'react'; +import styled, { css } from 'styled-components/macro'; +import { Spinner, Text } from '@blueprintjs/core'; + +interface Props { + text: React.ReactNode; + textColor: string; + backgroundColor: string; + type?: 'button' | 'reset' | 'submit'; + disabled?: boolean; + loading?: boolean; + onClick: () => void; +} + +export function DialogButton(props: Props) { + return ( + props.onClick()} + > + + {props.loading && } + {props.text} + + + ); +} +DialogButton.defaultProps = { + textColor: 'var(--black)', + backgroundColor: 'var(--teal)', + type: 'button', + onClick: () => {}, +}; + +interface StyledButtonProps { + $textColor: string; + $backgroundColor: string; + disabled?: boolean; +} + +const StyledButton = styled.button` + color: ${(props: StyledButtonProps) => props.$textColor}; + background-color: ${(props: StyledButtonProps) => props.$backgroundColor}; + border-radius: 20px; + padding: 11px 22px; + font-size: 14px; + &:hover:not(:disabled) { + color: var(--white); + } + &:disabled span { + opacity: 0.7; + } + ${(props: StyledButtonProps) => + props.disabled && + css` + cursor: not-allowed; + `} +`; diff --git a/src/app/components/DummyField/Loadable.tsx b/src/app/components/DummyField/Loadable.tsx new file mode 100644 index 000000000..03cd40207 --- /dev/null +++ b/src/app/components/DummyField/Loadable.tsx @@ -0,0 +1,12 @@ +/** + * + * Asynchronously loads the component for DummyField + * + */ + +import { lazyLoad } from 'utils/loadable'; + +export const DummyField = lazyLoad( + () => import('./index'), + module => module.DummyField, +); diff --git a/src/app/components/DummyField/index.tsx b/src/app/components/DummyField/index.tsx new file mode 100644 index 000000000..42489d39b --- /dev/null +++ b/src/app/components/DummyField/index.tsx @@ -0,0 +1,29 @@ +/** + * + * DummyField + * + */ +import React from 'react'; +import styled from 'styled-components/macro'; + +interface Props { + children: React.ReactNode; +} + +export function DummyField(props: Props) { + return ( +
{props.children}
+ ); +} + +const Div = styled.div` + border-radius: 5px; + background-color: var(--secondary); + color: var(--white); + padding: 10px 14px; + font-size: 16px; + font-family: 'Work Sans', sans-serif; + font-size: 400; + letter-spacing: 0; + height: 48px; +`; diff --git a/src/app/components/FieldGroup/Loadable.tsx b/src/app/components/FieldGroup/Loadable.tsx new file mode 100644 index 000000000..710dcdeed --- /dev/null +++ b/src/app/components/FieldGroup/Loadable.tsx @@ -0,0 +1,12 @@ +/** + * + * Asynchronously loads the component for FieldGroup + * + */ + +import { lazyLoad } from 'utils/loadable'; + +export const FieldGroup = lazyLoad( + () => import('./index'), + module => module.FieldGroup, +); diff --git a/src/app/components/FieldGroup/index.tsx b/src/app/components/FieldGroup/index.tsx new file mode 100644 index 000000000..0ff2983f7 --- /dev/null +++ b/src/app/components/FieldGroup/index.tsx @@ -0,0 +1,43 @@ +/** + * + * FieldGroup + * + */ +import React from 'react'; +import styled from 'styled-components/macro'; +import { Text } from '@blueprintjs/core'; + +interface Props { + label: React.ReactNode; + children: React.ReactNode; + labelColor: string; +} + +export function FieldGroup(props: Props) { + return ( +
+ + {props.children} +
+ ); +} + +FieldGroup.defaultProps = { + labelColor: 'var(--dark-gray)', +}; + +const Div = styled.div``; + +const Content = styled.div``; + +interface LabelProps { + color: string; +} +const Label = styled(Text).attrs(_ => ({ + ellipsize: true, +}))` + color: ${(props: LabelProps) => props.color}; + font-size: 14px; +`; diff --git a/src/app/components/Footer/index.tsx b/src/app/components/Footer/index.tsx index 1839be7e4..74567e88a 100644 --- a/src/app/components/Footer/index.tsx +++ b/src/app/components/Footer/index.tsx @@ -3,24 +3,41 @@ * Footer * */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { Trans } from 'react-i18next'; +import { translations } from 'locales/i18n'; +import { LanguageToggle } from '../../components/LanguageToggle'; export function Footer() { + const [hasMatomo, setHasMatomo] = useState(false); + + useEffect(() => { + setHasMatomo(window.hasOwnProperty('Matomo')); + }, []); + return (