From f23cf20ae8946c3408a9343ea1fd44e480d5baac Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 27 Apr 2023 10:34:58 +0200 Subject: [PATCH] v2.0.0 (#182) * Fix self-transfer display in the history (Fix self-transfer display in the history #117) * Display an error when data can't be loaded (Display an error when data can't be loaded #114) * Multichain (UI for multi-chain deployment #168) * Gift cards v2 (Gift Cards V2 #167) * Temporarily remove WalletConnect v2 * Fix KYC banner styles (Issue with displaying KYC resync banner #177) --- netlify.toml | 42 +-- package.json | 4 +- public/manifest.json | 4 +- src/App.js | 6 + src/assets/dots.svg | 3 + src/assets/dropdown.svg | 4 +- src/assets/github.svg | 3 + src/assets/mirror.svg | 3 + src/assets/optimism.svg | 4 + src/assets/telegram.svg | 3 + src/assets/twitter.svg | 3 + src/components/Button/index.js | 17 +- src/components/Footer/index.js | 71 ++--- src/components/Header/index.js | 122 ++++---- src/components/HistoryItem/index.js | 81 ++--- src/components/IncreasedLimitsBanner/index.js | 34 ++- src/components/IncreasedLimitsModal/index.js | 6 +- src/components/LatestAction/index.js | 5 +- src/components/MoreDropdown/index.js | 67 ++++ src/components/MultilineInput/index.js | 15 +- .../MultitransferDetailsModal/index.js | 13 +- src/components/NetworkDropdown/index.js | 71 +++++ src/components/OptionButton/index.js | 4 + src/components/QRCodeReader/index.js | 55 ++-- src/components/RedeemGiftCardModal/index.js | 249 +++++++++++++++ src/components/SwapOptionsModal/index.js | 65 ---- src/components/ToastContainer/index.js | 3 + src/components/TransactionModal/index.js | 17 +- src/components/WalletDropdown/index.js | 11 +- src/components/WalletModal/index.js | 4 +- src/components/ZkAccountDropdown/index.js | 20 +- src/config/index.js | 89 ++++++ src/constants/index.js | 39 ++- src/containers/Header/index.js | 15 +- src/containers/IncreasedLimitsModal/index.js | 4 +- src/containers/PendingAction/index.js | 5 +- src/containers/RedeemGiftCardModal/index.js | 75 +++++ src/containers/SwapModal/index.js | 42 +-- src/containers/SwapOptionsModal/index.js | 23 -- src/containers/TransactionModal/index.js | 4 +- src/contexts/IncreasedLimitsContext/index.js | 16 +- src/contexts/ModalContext/index.js | 11 +- src/contexts/PoolContext/index.js | 29 ++ src/contexts/SupportIdContext/index.js | 2 +- src/contexts/TokenBalanceContext/index.js | 18 +- src/contexts/ZkAccountContext/index.js | 285 ++++++++++-------- src/contexts/ZkAccountContext/zp.js | 78 ++--- src/contexts/index.js | 19 +- src/fonts/Gilroy-ExtraBold.woff | Bin 0 -> 35332 bytes src/hooks/index.js | 2 + src/hooks/useFee.js | 5 +- src/hooks/usePrevious.js | 9 + src/pages/Deposit/index.js | 14 +- src/pages/History/index.js | 5 +- src/pages/Transfer/MultiTransfer/index.js | 3 +- src/pages/Transfer/index.js | 11 +- src/pages/Withdraw/index.js | 51 ++-- src/pages/index.js | 4 +- src/providers/Web3Provider.js | 32 +- src/utils/index.js | 10 + yarn.lock | 41 ++- 61 files changed, 1315 insertions(+), 635 deletions(-) create mode 100644 src/assets/dots.svg create mode 100644 src/assets/github.svg create mode 100644 src/assets/mirror.svg create mode 100644 src/assets/optimism.svg create mode 100644 src/assets/telegram.svg create mode 100644 src/assets/twitter.svg create mode 100644 src/components/MoreDropdown/index.js create mode 100644 src/components/NetworkDropdown/index.js create mode 100644 src/components/RedeemGiftCardModal/index.js delete mode 100644 src/components/SwapOptionsModal/index.js create mode 100644 src/config/index.js create mode 100644 src/containers/RedeemGiftCardModal/index.js delete mode 100644 src/containers/SwapOptionsModal/index.js create mode 100644 src/contexts/PoolContext/index.js create mode 100644 src/fonts/Gilroy-ExtraBold.woff create mode 100644 src/hooks/usePrevious.js diff --git a/netlify.toml b/netlify.toml index fe341729..22e901c8 100644 --- a/netlify.toml +++ b/netlify.toml @@ -23,51 +23,13 @@ base = '/opt/build/repo/public' [context.production.environment] - REACT_APP_EXPLORER_ADDRESS_TEMPLATE = "https://polygonscan.com/address/%s" - REACT_APP_EXPLORER_TX_TEMPLATE = "https://polygonscan.com/tx/%s" - REACT_APP_EXPLORER_URL = "https://polygonscan.com" - REACT_APP_NETWORK = "137" - REACT_APP_CONTRACT_ADDRESS = "0x72e6B59D4a90ab232e55D4BB7ed2dD17494D62fB" - REACT_APP_TOKEN_ADDRESS = "0xB0B195aEFA3650A6908f15CdaC7D92F8a5791B0B" - REACT_APP_RELAYER_URL = "https://relayer-mvp.zkbob.com" - REACT_APP_PROVER_URL = "https://remoteprover-mvp.zkbob.com/" - REACT_APP_RPC_URL = "https://polygon-rpc.com" - REACT_APP_ZEROPOOL_NETWORK = "polygon" - REACT_APP_BUCKET_URL = "https://r2.zkbob.com" - REACT_APP_SNARK_PARAMS_VERSION = "22022023" + REACT_APP_CONFIG="prod" REACT_APP_RESTRICTED_COUNTRIES = "AE" REACT_APP_LOCK_TIMEOUT = "900000" # REACT_APP_RESTRICTED_COUNTRIES = "BY,CU,IR,IQ,CI,LR,KP,RU,SD,SY,US,ZW" [context.staging.environment] - REACT_APP_EXPLORER_ADDRESS_TEMPLATE = "https://sepolia.etherscan.io/address/%s" - REACT_APP_EXPLORER_TX_TEMPLATE = "https://sepolia.etherscan.io/tx/%s" - REACT_APP_EXPLORER_URL = "https://sepolia.etherscan.io" - REACT_APP_NETWORK = "11155111" - REACT_APP_CONTRACT_ADDRESS = "0x3bd088C19960A8B5d72E4e01847791BD0DD1C9E6" - REACT_APP_TOKEN_ADDRESS = "0x2C74B18e2f84B78ac67428d0c7a9898515f0c46f" - REACT_APP_RELAYER_URL = "https://relayer.thgkjlr.website/" - REACT_APP_PROVER_URL = "https://prover-staging.thgkjlr.website/" - REACT_APP_RPC_URL = "https://rpc.sepolia.org" - REACT_APP_ZEROPOOL_NETWORK = "sepolia" - REACT_APP_BUCKET_URL = "https://r2-staging.zkbob.com" - REACT_APP_SNARK_PARAMS_VERSION = "20022023" + REACT_APP_CONFIG="dev" REACT_APP_RESTRICTED_COUNTRIES = "AE" - REACT_APP_KYC_STATUS_URL = "https://api-stage.knowyourcat.id/v1/%s/categories?category=BABTokenWeek" - REACT_APP_KYC_HOMEPAGE_URL = "https://stage.knowyourcat.id/address/%s/BABTokenWeek" REACT_APP_LOCK_TIMEOUT = "300000" # REACT_APP_RESTRICTED_COUNTRIES = "BY,CU,IR,IQ,CI,LR,KP,RU,SD,SY,US,ZW" - -[context.goerli.environment] - REACT_APP_EXPLORER_ADDRESS_TEMPLATE = "https://goerli.etherscan.io/address/%s" - REACT_APP_EXPLORER_TX_TEMPLATE = "https://goerli.etherscan.io/tx/%s" - REACT_APP_EXPLORER_URL = "https://goerli.etherscan.io" - REACT_APP_NETWORK = "5" - REACT_APP_CONTRACT_ADDRESS = "0x49661694a71B3Dab9F25E86D5df2809B170c56E6" - REACT_APP_TOKEN_ADDRESS = "0x97a4ab97028466FE67F18A6cd67559BAABE391b8" - REACT_APP_RELAYER_URL = "https://dev-relayer.thgkjlr.website/" - REACT_APP_RPC_URL = "https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161" - REACT_APP_ZEROPOOL_NETWORK = "goerli" - REACT_APP_BUCKET_URL = "https://r2-goerli.zkbob.com" - REACT_APP_RESTRICTED_COUNTRIES = "AE" - REACT_APP_LOCK_TIMEOUT = "300000" diff --git a/package.json b/package.json index 60231b48..cf536556 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zkbob-ui", - "version": "1.4.0", + "version": "2.0.0", "private": true, "dependencies": { "@dicebear/avatars": "^4.10.2", @@ -44,7 +44,7 @@ "wagmi": "^0.12.1", "web-vitals": "^1.0.1", "webpack": "^5.70.0", - "zkbob-client-js": "2.1.0" + "zkbob-client-js": "3.2.1" }, "scripts": { "start": "react-app-rewired start", diff --git a/public/manifest.json b/public/manifest.json index 1f2f141f..06827cc6 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "zkBob", + "name": "zkBob - Private Stable Transfers", "icons": [ { "src": "favicon.ico", diff --git a/src/App.js b/src/App.js index 671502e8..b145bd4e 100644 --- a/src/App.js +++ b/src/App.js @@ -12,6 +12,7 @@ import 'services'; import GilroyRegular from 'fonts/Gilroy-Regular.woff'; import GilroyMedium from 'fonts/Gilroy-Medium.woff'; import GilroyBold from 'fonts/Gilroy-Bold.woff'; +import GilroyExtraBold from 'fonts/Gilroy-ExtraBold.woff'; const GlobalStyle = createGlobalStyle` @font-face { @@ -29,6 +30,11 @@ const GlobalStyle = createGlobalStyle` src: url(${GilroyBold}) format('woff'); font-weight: 700; } + @font-face { + font-family: 'Gilroy'; + src: url(${GilroyExtraBold}) format('woff'); + font-weight: 800; + } body { margin: 0; font-family: 'Gilroy'; diff --git a/src/assets/dots.svg b/src/assets/dots.svg new file mode 100644 index 00000000..c72d3cc8 --- /dev/null +++ b/src/assets/dots.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/dropdown.svg b/src/assets/dropdown.svg index 69efc2bd..23702630 100644 --- a/src/assets/dropdown.svg +++ b/src/assets/dropdown.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/assets/github.svg b/src/assets/github.svg new file mode 100644 index 00000000..5ba67463 --- /dev/null +++ b/src/assets/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/mirror.svg b/src/assets/mirror.svg new file mode 100644 index 00000000..0d773651 --- /dev/null +++ b/src/assets/mirror.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/optimism.svg b/src/assets/optimism.svg new file mode 100644 index 00000000..b8cab80f --- /dev/null +++ b/src/assets/optimism.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/telegram.svg b/src/assets/telegram.svg new file mode 100644 index 00000000..f5e3d915 --- /dev/null +++ b/src/assets/telegram.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/twitter.svg b/src/assets/twitter.svg new file mode 100644 index 00000000..8b8bf4c1 --- /dev/null +++ b/src/assets/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Button/index.js b/src/components/Button/index.js index 9918d953..39eb351a 100644 --- a/src/components/Button/index.js +++ b/src/components/Button/index.js @@ -12,7 +12,7 @@ export default props => { return ( @@ -25,12 +25,12 @@ const Button = styled.button` props.theme.button.primary.background[props.disabled ? (props.$contrast ? 'contrast' : 'disabled') : 'default'] }; color: ${props => props.theme.button.primary.text.color[props.disabled && props.$contrast ? 'contrast' : 'default']}; - font-size: ${props => props.theme.button.primary.text.size[props.$small ? 'small' : 'default']}; - font-weight: ${props => props.theme.button.primary.text.weight[props.$small ? 'small' : 'default']}; - padding: ${props => props.$small ? '8px 16px' : '0'}; - height: ${props => props.$small ? '30px' : '60px'}; + font-size: ${props => props.theme.button.primary.text.size[props.small ? 'small' : 'default']}; + font-weight: ${props => props.theme.button.primary.text.weight[props.small ? 'small' : 'default']}; + padding: ${props => props.small ? '8px 16px' : '0'}; + height: ${props => props.small ? '36px' : '60px'}; box-sizing: border-box; - border-radius: 16px; + border-radius: ${props => props.small ? '18px' : '16px'}; border: 0; border-color: ${props => props.theme.button.primary.border.color}; border-style: solid; @@ -38,6 +38,11 @@ const Button = styled.button` display: flex; align-items: center; justify-content: center; + @media only screen and (max-width: 1000px) { + height: ${props => props.small ? '30px' : '60px'}; + padding: ${props => props.small ? '8px 12px' : '0'}; + border-radius: 16px; + } `; const TransparentButton = styled.button` diff --git a/src/components/Footer/index.js b/src/components/Footer/index.js index dbadc7d4..2df1662c 100644 --- a/src/components/Footer/index.js +++ b/src/components/Footer/index.js @@ -2,56 +2,40 @@ import React, { useContext } from 'react'; import styled from 'styled-components'; import zkBobLibPackage from 'zkbob-client-js/package.json'; +import { SupportIdContext, ZkAccountContext } from 'contexts'; + import Link from 'components/Link'; -import Button from 'components/Button'; -import { ModalContext, SupportIdContext, ZkAccountContext } from 'contexts'; +import { ReactComponent as TwitterIcon } from 'assets/twitter.svg'; +import { ReactComponent as TelegramIcon } from 'assets/telegram.svg'; +import { ReactComponent as MirrorIcon } from 'assets/mirror.svg'; +import { ReactComponent as GithubIcon } from 'assets/github.svg'; export default () => { - const { openSwapOptionsModal } = useContext(ModalContext); const { supportId } = useContext(SupportIdContext); const { relayerVersion } = useContext(ZkAccountContext); - const sections = [ - { - title: 'Resources', - links: [ - { name: 'Documentation', href: 'https://docs.zkbob.com/' }, - { name: 'FAQ', href: 'https://docs.zkbob.com/zkbob-overview/faq' }, - { name: 'Linktree', href: 'https://linktr.ee/zkbob' }, - { name: 'Dune Analytics', href: 'https://dune.com/maxaleks/zkbob' }, - ] - }, - { - title: 'BOB Stable Token', - links: [ - { - name: 'View contract', - href: `${process.env.REACT_APP_EXPLORER_URL}/token/${process.env.REACT_APP_TOKEN_ADDRESS}`, - }, - ], - components: [ - - ] - } + const resources = [ + { icon: TwitterIcon, href: 'https://twitter.com/zkBob_' }, + { icon: TelegramIcon, href: 'https://t.me/zkbob_news' }, + { icon: MirrorIcon, href: 'https://mirror.xyz/0x6132eB883e88CD4E007552b871A6444Bfc34E837' }, + { icon: GithubIcon, href: 'https://github.com/zkBob' }, ]; return ( - {/* © zkBob 2022 */} - {sections.map((column, index) => ( - - {column?.title} - {column?.links.map((link, index) => ( - - {link.name} - - ))} - {column?.components?.map(component => component)} - - ))} + + + bob.zkbob.com + + {resources.map((resource, index) => ( + + {React.createElement(resource.icon, {})} + + ))} + @@ -87,16 +71,15 @@ const InnerRow = styled.div` align-items: center; justify-content: center; flex-wrap: wrap; - margin: 10px 5px 0; & > * { - margin: 4px 10px 0; + margin: 7px 10px 0; } `; const Text = styled.span` font-size: 14px; - color: ${props => props.theme.text.color.secondary}; - font-weight: ${props => props.theme.text.weight.normal}; + color: #A7A2B8; + font-weight: ${props => props.theme.text.weight.bold}; line-height: 20px; text-align: center; `; @@ -106,3 +89,9 @@ const TextRow = styled.div` justify-content: center; flex-wrap: wrap; `; + +const CustomLink = styled(Link)` + color: #A7A2B8; + font-size: 14px; + font-weight: ${props => props.theme.text.weight.bold}; +`; diff --git a/src/components/Header/index.js b/src/components/Header/index.js index d66a49c5..be45013e 100644 --- a/src/components/Header/index.js +++ b/src/components/Header/index.js @@ -6,6 +6,8 @@ import Tooltip from 'components/Tooltip'; import { ZkAvatar } from 'components/ZkAccountIdentifier'; import WalletDropdown from 'components/WalletDropdown'; import ZkAccountDropdown from 'components/ZkAccountDropdown'; +import NetworkDropdown from 'components/NetworkDropdown'; +import MoreDropdown from 'components/MoreDropdown'; import SpinnerDefault from 'components/Spinner'; import Skeleton from 'components/Skeleton'; @@ -14,11 +16,13 @@ import { ReactComponent as RefreshIcon } from 'assets/refresh.svg'; import { ReactComponent as DropdownIconDefault } from 'assets/dropdown.svg'; import { ReactComponent as AccountIconDefault } from 'assets/account.svg'; import { ReactComponent as WalletIconDefault } from 'assets/wallet.svg'; +import { ReactComponent as DotsIcon } from 'assets/dots.svg'; import { shortAddress, formatNumber } from 'utils'; import { tokenSymbol } from 'utils/token'; import { NETWORKS, CONNECTORS_ICONS } from 'constants'; import { useWindowDimensions } from 'hooks'; +import config from 'config'; export default ({ openWalletModal, connector, isLoadingZkAccount, empty, @@ -26,9 +30,12 @@ export default ({ balance, poolBalance, zkAccountId, refresh, isLoadingBalance, openSwapModal, generateAddress, openChangePasswordModal, openSeedPhraseModal, isDemo, disconnect, isLoadingState, + switchToPool, currentPool, initializeGiftCard, }) => { const walletButtonRef = useRef(null); const zkAccountButtonRef = useRef(null); + const networkButtonRef = useRef(null); + const moreButtonRef = useRef(null); const { width } = useWindowDimensions(); if (empty) { @@ -46,14 +53,20 @@ export default ({ - - {NETWORKS[process.env.REACT_APP_NETWORK].icon && ( - - )} - - {NETWORKS[process.env.REACT_APP_NETWORK].name} - - + + + + + + + + Bridge {tokenSymbol()} + {account ? ( - + {connector && }
{shortAddress(account)}
@@ -75,14 +89,14 @@ export default ({ {formatNumber(balance)} {tokenSymbol()} - + )}
-
+
) : ( - )} - - Get {tokenSymbol()} - + + + + +
); @@ -177,24 +194,36 @@ const AccountSection = styled(Row)` &:first-child { margin-left: 0; } - @media only screen and (max-width: 370px) { + @media only screen and (max-width: 400px) { + margin-left: 7px; + } + @media only screen and (max-width: 380px) { margin-left: 5px; } } `; -const NetworkLabel = styled(Row)` +const DropdownButton = styled(Row)` background-color: ${props => props.theme.networkLabel.background}; color: ${props => props.theme.text.color.primary}; font-weight: ${props => props.theme.text.weight.normal}; - padding: 0 12px; - border-radius: 16px; - min-height: 30px; + padding: 0 8px; + border-radius: 18px; + min-height: 36px; box-sizing: border-box; + cursor: ${props => props.$refreshing ? 'not-allowed' : 'pointer'}; + @media only screen and (max-width: 1000px) { + min-height: 30px; + border-radius: 16px; + } `; -const AccountLabel = styled(NetworkLabel)` - cursor: ${props => props.$refreshing ? 'not-allowed' : 'pointer'}; +const NetworkDropdownButton = styled(DropdownButton)` + padding: 0 8px 0 10px; +`; + +const AccountDropdownButton = styled(NetworkDropdownButton)` + padding: 0 12px; overflow: hidden; border: 1px solid ${props => props.theme.button.primary.text.color.contrast}; &:hover { @@ -203,7 +232,7 @@ const AccountLabel = styled(NetworkLabel)` color: ${props => !props.$refreshing && props.theme.button.link.text.color}; } & path { - fill: ${props => !props.$refreshing && props.theme.button.link.text.color}; + stroke: ${props => !props.$refreshing && props.theme.button.link.text.color}; } } `; @@ -242,48 +271,33 @@ const Spinner = styled(SpinnerDefault)` const RefreshButtonContainer = styled(Row)` background-color: ${props => props.theme.networkLabel.background}; padding: 8px 12px; - border-radius: 16px; - height: 30px; + border-radius: 18px; + height: 36px; box-sizing: border-box; cursor: pointer; + @media only screen and (max-width: 1000px) { + height: 30px; + border-radius: 16px; + } `; -const GetBobButton = styled.button` - background: transparent; - border: 1px solid ${props => props.theme.button.link.text.color}; - font-size: 16px; - font-weight: 400; - cursor: pointer; - color: ${props => props.theme.button.link.text.color}; - underline: none; - text-decoration: none; - padding: 5px 12px; - border-radius: 16px; - height: 30px; - box-sizing: border-box; - white-space: nowrap; +const BridgeButton = styled(Button)` + background: ${props => props.theme.button.link.text.color}; @media only screen and (max-width: 1000px) { display: none; } `; const DropdownIcon = styled(DropdownIconDefault)` - margin-left: 8px; + margin-left: 7px; + @media only screen and (max-width: 1000px) { + display: ${props => props.$onlyDesktop ? 'none' : 'block'}; + } `; const NetworkIcon = styled.img` width: 18px; height: 18px; - margin-right: 5px; - @media only screen and (max-width: 1000px) { - margin-right: 0; - } -`; - -const NetworkName = styled.span` - @media only screen and (max-width: 1000px) { - display: none; - } `; const LargeButtonContent = styled.div` diff --git a/src/components/HistoryItem/index.js b/src/components/HistoryItem/index.js index 341c7215..a8dbe9b6 100644 --- a/src/components/HistoryItem/index.js +++ b/src/components/HistoryItem/index.js @@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react'; import styled from 'styled-components'; import { ethers } from 'ethers'; import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { HistoryTransactionType } from 'zkbob-client-js'; import Link from 'components/Link'; import Spinner from 'components/Spinner'; @@ -13,7 +14,8 @@ import { ZkAvatar } from 'components/ZkAccountIdentifier'; import { formatNumber, shortAddress } from 'utils'; import { tokenSymbol, tokenIcon } from 'utils/token'; import { useDateFromNow, useWindowDimensions } from 'hooks'; -import { HISTORY_ACTION_TYPES } from 'constants'; +import config from 'config'; +import { NETWORKS } from 'constants'; import { ReactComponent as DepositIcon } from 'assets/deposit.svg'; import { ReactComponent as WithdrawIcon } from 'assets/withdraw.svg'; @@ -22,51 +24,60 @@ import { ReactComponent as IncognitoAvatar } from 'assets/incognito-avatar.svg'; import { ReactComponent as InfoIconDefault } from 'assets/info.svg'; const { - DEPOSIT, - TRANSFER_IN, - TRANSFER_OUT, - WITHDRAWAL, - TRANSFER_SELF, - DIRECT_DEPOSIT, -} = HISTORY_ACTION_TYPES; + Deposit, + TransferIn, + TransferOut, + Withdrawal, + DirectDeposit, +} = HistoryTransactionType; const actions = { - [DEPOSIT]: { + [Deposit]: { name: 'Deposit', icon: DepositIcon, sign: '+', }, - [TRANSFER_IN]: { + [TransferIn]: { name: 'Transfer', icon: TransferIcon, sign: '+', }, - [TRANSFER_OUT]: { + [TransferOut]: { name: 'Transfer', icon: TransferIcon, sign: '-', }, - [WITHDRAWAL]: { + [Withdrawal]: { name: 'Withdrawal', icon: WithdrawIcon, sign: '-', }, - [TRANSFER_SELF]: { + [DirectDeposit]: { + name: 'Deposit', + icon: DepositIcon, + sign: '+', + }, + 5: { // old transfer self name: 'Transfer', icon: TransferIcon, sign: '', }, - [DIRECT_DEPOSIT]: { - name: 'Deposit', - icon: DepositIcon, - sign: '+', - } }; -const AddressLink = ({ action, isMobile }) => { - const address = action.type === DEPOSIT ? action.actions[0].from : action.actions[0].to; +function getSign(item) { + if (item.actions.length === 1 && item.actions[0].isLoopback) { + return ''; + } + return actions[item.type].sign; +} + +const AddressLink = ({ action, isMobile, currentChainId }) => { + const address = action.type === Deposit ? action.actions[0].from : action.actions[0].to; return ( - + {shortAddress(address, isMobile ? 10 : 22)} ); @@ -97,12 +108,13 @@ const Fee = ({ fee, highFee, isMobile }) => ( ); -export default ({ item, zkAccountId }) => { +export default ({ item, zkAccountId, currentPool }) => { const date = useDateFromNow(item.timestamp); const { width } = useWindowDimensions(); const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); const [isCopied, setIsCopied] = useState(false); const isMobile = width <= 500; + const currentChainId = config.pools[currentPool].chainId; const onCopy = useCallback((text, result) => { if (result) { @@ -124,7 +136,7 @@ export default ({ item, zkAccountId }) => { - {actions[item.type].sign}{' '} + {getSign(item)}{' '} {(() => { const total = item.actions.reduce((acc, curr) => acc.add(curr.amount), ethers.constants.Zero); return ( @@ -159,10 +171,10 @@ export default ({ item, zkAccountId }) => { - {item.type === DEPOSIT ? 'From' : 'To'} + {item.type === Deposit ? 'From' : 'To'} - {[DEPOSIT, WITHDRAWAL].includes(item.type) ? ( - + {[Deposit, Withdrawal].includes(item.type) ? ( + ) : ( item.actions.length === 1 ? ( { - {item.type === TRANSFER_OUT ? ( + {(item.type === TransferOut && !item.actions[0].isLoopback) ? ( ) : ( @@ -186,7 +198,7 @@ export default ({ item, zkAccountId }) => { {shortAddress( item.actions[0].to, - isMobile ? 10 : (item.type === DIRECT_DEPOSIT ? 16 : 22) + isMobile ? 10 : (item.type === DirectDeposit ? 16 : 22) )} @@ -195,7 +207,7 @@ export default ({ item, zkAccountId }) => { ) : ( - {item.type === TRANSFER_OUT ? ( + {item.type === TransferOut ? ( <> - {item.actions.length > 1 && item.type === TRANSFER_OUT && ( + {item.actions.length > 1 && item.type === TransferOut && ( {isMobile ? 'Multi' : 'Multitransfer'} )} - {item.type === DIRECT_DEPOSIT && ( + {item.type === DirectDeposit && ( {isMobile ? 'Direct' : 'Direct deposit'} )} {(item.txHash && item.txHash !== '0') ? ( - + View tx ) : ( @@ -244,9 +256,10 @@ export default ({ item, zkAccountId }) => {
{item.actions.length > 1 && ( ({ address: action.to, amount: action.amount }))} + transfers={item.actions.map(action => ({ address: action.to, ...action }))} isOpen={isDetailsModalOpen} onClose={() => setIsDetailsModalOpen(false)} + zkAccountId={zkAccountId} isSent={true} /> )} diff --git a/src/components/IncreasedLimitsBanner/index.js b/src/components/IncreasedLimitsBanner/index.js index 61aa9c0c..20f9d256 100644 --- a/src/components/IncreasedLimitsBanner/index.js +++ b/src/components/IncreasedLimitsBanner/index.js @@ -3,12 +3,12 @@ import styled from 'styled-components'; import DefaultLink from 'components/Link'; import DefaultButton from 'components/Button'; -import { ReactComponent as InfinityLoopIcon } from 'assets/infinity-loop.svg'; -import { ReactComponent as WargingIcon } from 'assets/warning.svg'; +import { ReactComponent as InfinityLoopIconDefault } from 'assets/infinity-loop.svg'; +import { ReactComponent as WargingIconDefault } from 'assets/warning.svg'; import { INCREASED_LIMITS_STATUSES } from 'constants'; -export default ({ status, account, openModal }) => { +export default ({ status, account, openModal, kycUrls }) => { let component; switch(status) { default: @@ -29,8 +29,10 @@ export default ({ status, account, openModal }) => { case INCREASED_LIMITS_STATUSES.RESYNC: component = <> - To restore increased deposit limits - - resync your BAB token + + To restore increased deposit limits - + resync your BAB token + ; break; } @@ -45,19 +47,21 @@ const Container = styled.div` display: flex; align-items: center; justify-content: center; - height: 36px; + min-height: 36px; width: 480px; max-width: 100%; border-radius: 10px; background: ${props => props.theme.color.yellow}; margin: 20px 0 -10px; + padding: 5px 10px; + box-sizing: border-box; `; const Text = styled.span` font-size: 14px; font-weight: ${props => props.theme.text.weight.bold}; color: ${props => props.theme.text.color.secondary}; - margin: 0 5px 0 8px; + margin-right: 5px; `; const Link = styled(DefaultLink)` @@ -67,3 +71,19 @@ const Link = styled(DefaultLink)` const Button = styled(DefaultButton)` font-weight: ${props => props.theme.text.weight.bold}; `; + +const Row = styled.div` + white-space: pre-wrap; /* CSS3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +`; + +const InfinityLoopIcon = styled(InfinityLoopIconDefault)` + margin-right: 8px; +`; + +const WargingIcon = styled(WargingIconDefault)` + margin-right: 8px; +`; diff --git a/src/components/IncreasedLimitsModal/index.js b/src/components/IncreasedLimitsModal/index.js index 09a12d6c..5a5832a4 100644 --- a/src/components/IncreasedLimitsModal/index.js +++ b/src/components/IncreasedLimitsModal/index.js @@ -5,9 +5,11 @@ import Button from 'components/Button'; import Link from 'components/Link'; import Modal from 'components/Modal'; +import config from 'config'; + const DOCS_URL = 'https://www.binance.com/en/support/faq/how-to-mint-binance-account-bound-bab-token-bacaf9595b52440ea2b023195ba4a09c'; -export default ({ isOpen, onClose, account, isWalletModalOpen, openWalletModal }) => { +export default ({ isOpen, onClose, account, isWalletModalOpen, openWalletModal, currentPool }) => { return ( {account ? ( - + Verify my BAB token ) : ( diff --git a/src/components/LatestAction/index.js b/src/components/LatestAction/index.js index e2cca747..c80220aa 100644 --- a/src/components/LatestAction/index.js +++ b/src/components/LatestAction/index.js @@ -9,8 +9,9 @@ import Tooltip from 'components/Tooltip'; import { shortAddress, formatNumber } from 'utils'; import { tokenSymbol, tokenIcon } from 'utils/token'; +import { NETWORKS } from 'constants'; -export default ({ type, shielded, actions, txHash }) => { +export default ({ type, shielded, actions, txHash, currentChainId }) => { const history = useHistory(); const location = useLocation(); return ( @@ -29,7 +30,7 @@ export default ({ type, shielded, actions, txHash }) => { })()} {' '}{tokenSymbol(shielded)} - + {shortAddress(txHash)} diff --git a/src/components/MoreDropdown/index.js b/src/components/MoreDropdown/index.js new file mode 100644 index 00000000..13800df2 --- /dev/null +++ b/src/components/MoreDropdown/index.js @@ -0,0 +1,67 @@ +import { useCallback } from 'react'; +import styled from 'styled-components'; + +import Dropdown from 'components/Dropdown'; +import OptionButton from 'components/OptionButton'; + +const links = [ + { name: 'Dune Analytics', href: 'https://dune.com/maxaleks/zkbob' }, + { name: 'Documentation', href: 'https://docs.zkbob.com/' }, + { name: 'Linktree', href: 'https://linktr.ee/zkbob' }, +]; + +const Content = ({ buttonRef, openSwapModal }) => { + const onClick = useCallback(() => { + openSwapModal(); + buttonRef.current.click(); + }, [buttonRef, openSwapModal]); + + return ( + + More about zkBob + + Bridge BOB + + {links.map((link, index) => + buttonRef.current.click()} + > + {link.name} + + )} + + ); +} + +export default ({ buttonRef, openSwapModal, children }) => ( + }> + {children} + +); + +const Container = styled.div` + display: flex; + flex-direction: column; + & > :last-child { + margin-bottom: 0; + } +`; + +const Title = styled.span` + font-size: 14px; + color: ${({ theme }) => theme.text.color.secondary}; + margin-bottom: 20px; +`; + +const SwapButton = styled(OptionButton)` + background: ${props => props.theme.button.link.text.color}; + color: ${props => props.theme.button.primary.text.color.default}; + border: 0; + display: none; + @media only screen and (max-width: 1000px) { + display: flex; + } +`; diff --git a/src/components/MultilineInput/index.js b/src/components/MultilineInput/index.js index f6c274d5..4ce33a1d 100644 --- a/src/components/MultilineInput/index.js +++ b/src/components/MultilineInput/index.js @@ -5,6 +5,7 @@ import Tooltip from 'components/Tooltip'; import QRCodeReader from 'components/QRCodeReader'; import { ReactComponent as InfoIconDefault } from 'assets/info.svg'; +import { ReactComponent as QrCodeIconDefault } from 'assets/qr-code.svg'; import useAutosizeTextArea from './hooks/useAutosizeTextArea'; @@ -38,7 +39,11 @@ export default ({ value, onChange, hint, placeholder, qrCode }) => { } - {qrCode && } + {qrCode && ( + + + + )} ); }; @@ -102,3 +107,11 @@ const InfoIcon = styled(InfoIconDefault)` } } `; + +const QrCodeIcon = styled(QrCodeIconDefault)` + position: absolute; + right: 22px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; +`; diff --git a/src/components/MultitransferDetailsModal/index.js b/src/components/MultitransferDetailsModal/index.js index 6ea9da6b..0b663275 100644 --- a/src/components/MultitransferDetailsModal/index.js +++ b/src/components/MultitransferDetailsModal/index.js @@ -5,13 +5,14 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'; import Modal from 'components/Modal'; import Tooltip from 'components/Tooltip'; +import { ZkAvatar } from 'components/ZkAccountIdentifier'; import { ReactComponent as IncognitoAvatar } from 'assets/incognito-avatar.svg'; import { tokenSymbol, tokenIcon } from 'utils/token'; import { formatNumber, shortAddress } from 'utils'; -const ListItem = ({ index, data }) => { +const ListItem = ({ index, data, zkAccountId }) => { const [isCopied, setIsCopied] = useState(false); const onCopy = useCallback((text, result) => { @@ -24,7 +25,11 @@ const ListItem = ({ index, data }) => { return ( {index + 1} - + {data.isLoopback ? ( + + ) : ( + + )} { ); } -export default ({ isOpen, onClose, onBack, transfers, isSent }) => { +export default ({ isOpen, onClose, onBack, transfers, isSent, zkAccountId }) => { return ( { {transfers.map((transfer, index) => ( - + ))} diff --git a/src/components/NetworkDropdown/index.js b/src/components/NetworkDropdown/index.js new file mode 100644 index 00000000..04e8a3d5 --- /dev/null +++ b/src/components/NetworkDropdown/index.js @@ -0,0 +1,71 @@ +import { useCallback } from 'react'; +import styled from 'styled-components'; + +import Dropdown from 'components/Dropdown'; +import OptionButton from 'components/OptionButton'; + +import { NETWORKS } from 'constants'; + +import config from 'config'; + + +const Content = ({ switchToPool, currentPool, buttonRef }) => { + const onSwitchPool = useCallback(poolId => { + buttonRef.current.click(); + switchToPool(poolId); + }, [switchToPool, buttonRef]); + + return ( + + Networks + {Object.values(config.pools).map((pool, index) => + onSwitchPool(Object.keys(config.pools)[index])} + disabled={currentPool === Object.keys(config.pools)[index]} + > + + + {NETWORKS[pool.chainId].name} + + + )} + + ); +}; + +export default ({ disabled, switchToPool, currentPool, buttonRef, children }) => ( + ( + + )} + > + {children} + +); + +const Container = styled.div` + display: flex; + flex-direction: column; + & > :last-child { + margin-bottom: 0; + } +`; + +const Row = styled.div` + display: flex; + align-items: center; +`; + +const Title = styled.span` + font-size: 14px; + color: ${({ theme }) => theme.text.color.secondary}; + margin-bottom: 20px; +`; + +const NetworkIcon = styled.img` + width: 18px; + height: 18px; + margin-right: 10px; +`; diff --git a/src/components/OptionButton/index.js b/src/components/OptionButton/index.js index 2e8e663a..b383c735 100644 --- a/src/components/OptionButton/index.js +++ b/src/components/OptionButton/index.js @@ -33,4 +33,8 @@ const ButtonLink = styled(Link)` color: ${({ theme }) => theme.text.color.primary}; font-weight: ${({ theme }) => theme.text.weight.normal}; opacity: ${props => props.disabled ? 0.5 : 1}; + &:disabled { + background: ${props => props.theme.color.white}; + cursor: not-allowed; + } `; diff --git a/src/components/QRCodeReader/index.js b/src/components/QRCodeReader/index.js index 383d67d1..dbfbc4a8 100644 --- a/src/components/QRCodeReader/index.js +++ b/src/components/QRCodeReader/index.js @@ -1,11 +1,11 @@ import React, { useState } from 'react'; +import { createPortal } from 'react-dom'; import styled from 'styled-components'; import QrReader from 'react-qr-scanner' -import { ReactComponent as QrCodeIconDefault } from 'assets/qr-code.svg'; import { ReactComponent as CrossIconDefault } from 'assets/cross.svg'; -export default ({ onResult }) => { +export default ({ onResult, children }) => { const [showScanner, setShowScanner] = useState(false); const handleScan = data => { @@ -16,35 +16,36 @@ export default ({ onResult }) => { return ( <> - { e.stopPropagation(); setShowScanner(true); }} /> - {showScanner && ( - e.stopPropagation()}> - setShowScanner(false)} /> - console.log(error)} - style={{ width: 300 }} - /> - - )} + {React.cloneElement(children, { + onClick: e => { + e.stopPropagation(); + setShowScanner(true); + } + })} + {createPortal(( + <> + {showScanner && ( + e.stopPropagation()}> + setShowScanner(false)} /> + console.log(error)} + style={{ width: 300 }} + /> + + )} + + ), document.getElementById('root'))} ); }; -const QrCodeIcon = styled(QrCodeIconDefault)` - position: absolute; - right: 22px; - top: 50%; - transform: translateY(-50%); - cursor: pointer; -`; - const CrossIcon = styled(CrossIconDefault)` position: absolute; top: 20px; diff --git a/src/components/RedeemGiftCardModal/index.js b/src/components/RedeemGiftCardModal/index.js new file mode 100644 index 00000000..9ee59b38 --- /dev/null +++ b/src/components/RedeemGiftCardModal/index.js @@ -0,0 +1,249 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import styled from 'styled-components'; +import { BigNumber } from 'ethers'; + +import ButtonDefault from 'components/Button'; +import Modal from 'components/Modal'; +import Spinner from 'components/Spinner'; + +import { formatNumber } from 'utils'; + +import { ReactComponent as BobTokenDefault } from 'assets/bob.svg'; +import { ReactComponent as CheckIconDefault } from 'assets/check-circle.svg'; +import { ReactComponent as CrossIconDefault } from 'assets/cross-circle.svg'; + +import config from 'config'; +import { NETWORKS } from 'constants'; + +const CREATE_ACCOUNT = 1; +const SWITCH_NETWORK = 2; +const START = 3; +const IN_PROGRESS = 4; +const COMPLETED = 5; +const FAILED = 6; + +const texts = [ + <>Transferring tokens

to your account, + <>Making magic, + <>Just a few seconds more,
almost done, +]; + +const gradient = 'linear-gradient(150deg, rgba(251, 237, 206, 0.8) 10%, rgba(250, 243, 230, 0.5) 50%, rgba(251, 237, 206, 0.8) 100%)'; + +const CreateAccountScreen = ({ openCreateAccount }) => ( + <> + + Before you can redeem
a gift card +
+ + To redeem a gift card you need a zkBob zkAccount! Create an account,{' '} + or login into an existing account to claim your BOB tokens. + + + +); + +const SwitchNetworkScreen = ({ giftCard, redeem }) => ( + <> + + We need to switch the network + + + Gift card available only on {NETWORKS[config.pools[giftCard.poolAlias].chainId].name}.{' '} + To redeem it we need to change the network + + + +); + +const StartScreen = ({ giftCard, redeem, isLoadingZkAccount }) => ( + <> + You're Lucky! + + On this gift card you will find + + + + {formatNumber(giftCard?.balance || BigNumber.from('0'))} + + + +); + +const InProgressScreen = () => { + const [text, setText] = useState(texts[0]); + + useEffect(() => { + let counter = 0; + const intervalId = setInterval(() => { + counter++; + const index = counter % texts.length; + setText(texts[index]); + }, 5000); // 5 seconds + return () => clearInterval(intervalId); + }, []); + + return ( + <> + {text} + + + ); +} + +const CompletedScreen = () => ( + <> + + Your BOB tokens were claimed.
+ Check your account balance. +
+ + +); + +const FailedScreen = () => ( + <> + + The tokens have already
been claimed +
+ + +); + +export default ({ + isOpen, onClose, giftCard, redeemGiftCard, + zkAccount, isLoadingZkAccount, + setUpAccount, isNewUser, currentPool, +}) => { + const [step, setStep] = useState(START); + + const redeem = useCallback(async () => { + setStep(IN_PROGRESS); + try { + await redeemGiftCard(); + setStep(COMPLETED); + } catch (error) { + setStep(FAILED); + } + }, [redeemGiftCard]); + + const checkNetworkAndRedeem = useCallback(() => { + if (!isNewUser && currentPool !== giftCard?.poolAlias) { + setStep(SWITCH_NETWORK); + } else { + redeem(); + } + }, [isNewUser, currentPool, giftCard, redeem]); + + useEffect(() => { + if (isOpen && !zkAccount && !isLoadingZkAccount) { + setStep(CREATE_ACCOUNT); + } + }, [isOpen, zkAccount, isLoadingZkAccount]); + + const openCreateAccount = useCallback(() => { + setUpAccount(); + setStep(START); + }, [setUpAccount]); + + const clearStateAndClose = useCallback(() => { + setStep(START); + onClose(); + }, [onClose]); + + return ( + + {(() => { + switch(step) { + case CREATE_ACCOUNT: + return ; + case SWITCH_NETWORK: + return ; + default: + case START: + return ; + case IN_PROGRESS: + return ; + case COMPLETED: + return ; + case FAILED: + return ; + } + })()} + + ); +}; + +const Title = styled.span` + font-size: 24px; + line-height: 32px; + color: ${({ theme }) => theme.text.color.primary}; + font-weight: 600; + text-align: center; + margin-bottom: 8px; + margin-top: -20px; +`; + +const Description = styled.span` + font-size: 16px; + line-height: 24px; + color: ${({ theme }) => theme.text.color.primary}; + text-align: center; +`; + +const BalanceContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; +`; + +const Amount = styled.span` + font-size: 80px; + line-height: 98px; + font-weight: 800; + background: linear-gradient(115.84deg, #6D5CFF 9.33%, #E86EFF 60.78%, #FFD66E 97.92%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; +`; + +const BobToken = styled(BobTokenDefault)` + width: 60px; + height: 60px; + margin-right: 10px; +`; + +const StatusTitle = styled.span` + font-size: 20px; + line-height: 28px; + color: ${({ theme }) => theme.text.color.primary}; + font-weight: ${({ theme }) => theme.text.weight.bold}; + margin-bottom: 24px; + margin-top: -24px; + text-align: center; +`; + +const CheckIcon = styled(CheckIconDefault)` + margin-bottom: 16px; +`; + +const CrossIcon = styled(CrossIconDefault)` + margin-bottom: 16px; +`; + +const Button = styled(ButtonDefault)` + width: 100%; + margin-top: 20px; +`; diff --git a/src/components/SwapOptionsModal/index.js b/src/components/SwapOptionsModal/index.js deleted file mode 100644 index 60def78c..00000000 --- a/src/components/SwapOptionsModal/index.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -import Modal from 'components/Modal'; -import Link from 'components/Link'; - -import { ReactComponent as LinkIcon } from 'assets/external-link.svg'; - -export default ({ isOpen, onClose, openSwapModal }) => { - return ( - - - Swap using Li.Fi - - - Other options - - - - ); -}; - -const SwapOption = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - background-color: ${({ theme }) => theme.walletConnectorOption.background.default}; - border: 1px solid ${({ theme }) => theme.walletConnectorOption.border.default}; - border-radius: 16px; - width: 100%; - height: 60px; - padding: 0 24px; - margin-bottom: 16px; - box-sizing: border-box; - cursor: pointer; - &:hover { - background-color: ${({ theme }) => theme.walletConnectorOption.background.hover}; - border: 1px solid ${({ theme }) => theme.walletConnectorOption.border.hover}; - } -`; - -const SwapOptionLink = styled(Link)` - display: flex; - align-items: center; - justify-content: space-between; - background-color: ${({ theme }) => theme.walletConnectorOption.background.default}; - border: 1px solid ${({ theme }) => theme.walletConnectorOption.border.default}; - border-radius: 16px; - width: 100%; - height: 60px; - padding: 0 24px; - margin-bottom: 16px; - box-sizing: border-box; - cursor: pointer; - &:hover { - background-color: ${({ theme }) => theme.walletConnectorOption.background.hover}; - border: 1px solid ${({ theme }) => theme.walletConnectorOption.border.hover}; - } -`; - -const SwapOptionName = styled.span` - font-size: 16px; - color: ${({ theme }) => theme.text.color.primary}; - font-weight: ${({ theme }) => theme.text.weight.normal}; -`; diff --git a/src/components/ToastContainer/index.js b/src/components/ToastContainer/index.js index d0a95c09..d39c6ed9 100644 --- a/src/components/ToastContainer/index.js +++ b/src/components/ToastContainer/index.js @@ -7,6 +7,7 @@ injectStyle(); export default () => @@ -14,5 +15,7 @@ const ToastContainerStyled = styled(ToastContainer)` .Toastify__toast { border-radius: 16px; color: ${props => props.theme.text.color.secondary}; + font-size: 14px; + line-height: 20px; } `; diff --git a/src/components/TransactionModal/index.js b/src/components/TransactionModal/index.js index 11e8295b..574aa632 100644 --- a/src/components/TransactionModal/index.js +++ b/src/components/TransactionModal/index.js @@ -12,6 +12,7 @@ import { ReactComponent as CrossIconDefault } from 'assets/cross-circle.svg'; import { tokenSymbol } from 'utils/token'; import { formatNumber } from 'utils'; +import config from 'config'; const titles = { [TX_STATUSES.APPROVE_TOKENS]: 'Please approve tokens', @@ -32,23 +33,23 @@ const titles = { }; const descriptions = { - [TX_STATUSES.DEPOSITED]: amount => ( + [TX_STATUSES.DEPOSITED]: ({ amount }) => ( Your {formatNumber(amount, 18)} {tokenSymbol()} deposit to the zero knowledge pool is in progress.

To increase the level of privacy, consider keeping the tokens in the zero knowledge pool for some time before withdrawal.
), - [TX_STATUSES.TRANSFERRED]: amount => ( + [TX_STATUSES.TRANSFERRED]: ({ amount }) => ( Your {formatNumber(amount, 18)} {tokenSymbol()} transfer within the zero knowledge pool is in progress. ), - [TX_STATUSES.TRANSFERRED_MULTI]: amount => ( + [TX_STATUSES.TRANSFERRED_MULTI]: ({ amount }) => ( Your {formatNumber(amount, 18)} {tokenSymbol()} multitransfer within the zero knowledge pool is in progress. ), - [TX_STATUSES.WITHDRAWN]: amount => ( + [TX_STATUSES.WITHDRAWN]: ({ amount }) => ( Your {formatNumber(amount, 18)} {tokenSymbol()} withdrawal from the zero knowledge pool is in progress. @@ -70,10 +71,10 @@ const descriptions = { Because of this, you can't withdraw funds to this address. ), - [TX_STATUSES.WRONG_NETWORK]: () => ( + [TX_STATUSES.WRONG_NETWORK]: ({ currentPool }) => ( Failed to switch the network.{' '} - Please connect your wallet to {NETWORKS[process.env.REACT_APP_NETWORK].name} and try again. + Please connect your wallet to {NETWORKS[config.pools[currentPool].chainId].name} and try again. ), }; @@ -94,7 +95,7 @@ const SUSPICIOUS_ACCOUNT_STATUSES = [ TX_STATUSES.SUSPICIOUS_ACCOUNT_WITHDRAWAL, ]; -export default ({ isOpen, onClose, status, amount, error, supportId }) => { +export default ({ isOpen, onClose, status, amount, error, supportId, currentPool }) => { return ( { else return ; })()} {descriptions[status] && ( - {descriptions[status](amount)} + {descriptions[status]({ amount, currentPool })} )} {(status === TX_STATUSES.REJECTED && error) && ( {error} diff --git a/src/components/WalletDropdown/index.js b/src/components/WalletDropdown/index.js index 3f94c973..ba41ebd8 100644 --- a/src/components/WalletDropdown/index.js +++ b/src/components/WalletDropdown/index.js @@ -13,10 +13,13 @@ import { ReactComponent as CheckIcon } from 'assets/check.svg'; import { formatNumber } from 'utils'; import { tokenIcon, tokenSymbol } from 'utils/token'; -import { CONNECTORS_ICONS } from 'constants'; +import { CONNECTORS_ICONS, NETWORKS } from 'constants'; -const Content = ({ address, balance, connector, changeWallet, disconnect, buttonRef }) => { +const Content = ({ + address, balance, connector, changeWallet, + disconnect, buttonRef, currentChainId, +}) => { const [isCopied, setIsCopied] = useState(false); const onCopy = useCallback((text, result) => { @@ -59,7 +62,7 @@ const Content = ({ address, balance, connector, changeWallet, disconnect, button View in Explorer @@ -72,6 +75,7 @@ const Content = ({ address, balance, connector, changeWallet, disconnect, button export default ({ address, balance, connector, changeWallet, disconnect, buttonRef, children, disabled, + currentChainId, }) => ( )} > diff --git a/src/components/WalletModal/index.js b/src/components/WalletModal/index.js index 521f79fc..5e3d77a2 100644 --- a/src/components/WalletModal/index.js +++ b/src/components/WalletModal/index.js @@ -9,8 +9,8 @@ import { tokenSymbol } from 'utils/token'; import { CONNECTORS_ICONS } from 'constants'; const getConnectorName = connector => { - if (connector.name === 'WalletConnectLegacy') return 'WalletConnect v1'; - if (connector.name === 'WalletConnect') return 'WalletConnect v2'; + if (connector.name === 'WalletConnectLegacy') return 'WalletConnect'; + // if (connector.name === 'WalletConnect') return 'WalletConnect v2'; return connector.name; } diff --git a/src/components/ZkAccountDropdown/index.js b/src/components/ZkAccountDropdown/index.js index b5aa6e0d..07e2aafc 100644 --- a/src/components/ZkAccountDropdown/index.js +++ b/src/components/ZkAccountDropdown/index.js @@ -8,6 +8,7 @@ import Tooltip from 'components/Tooltip'; import OptionButton from 'components/OptionButton'; import Button from 'components/Button'; import PrivateAddress from 'components/PrivateAddress'; +import QRCodeReader from 'components/QRCodeReader'; import { ReactComponent as BackIconDefault } from 'assets/back.svg'; @@ -18,7 +19,7 @@ import { tokenIcon, tokenSymbol } from 'utils/token'; const Content = ({ balance, generateAddress, switchAccount, isDemo, changePassword, logout, buttonRef, showSeedPhrase, - isLoadingState, + isLoadingState, initializeGiftCard, }) => { const [privateAddress, setPrivateAddress] = useState(null); const [showQRCode, setShowQRCode] = useState(false); @@ -37,6 +38,18 @@ const Content = ({ setShowQRCode(false); }, []); + const initGiftCard = useCallback(async result => { + try { + const paramsString = result.split('?')[1]; + const queryParams = new URLSearchParams(paramsString); + const code = queryParams.get('gift-code'); + await initializeGiftCard(code); + buttonRef.current.click(); + } catch (error) { + console.log(error); + } + }, [initializeGiftCard, buttonRef]); + const handleOptionClick = useCallback(action => { buttonRef.current.click(); action(); @@ -95,6 +108,9 @@ const Content = ({ You create a new address each time you connect.{' '} Receive tokens to this address or a previously generated address. + + Redeem gift card + {options.map((item, index) => ( )} > diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 00000000..f00c4a3b --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,89 @@ +const config = { + prod: { + defaultPool: 'BOB-polygon', + pools: { + 'BOB-polygon': { + chainId: 137, + poolAddress: '0x72e6B59D4a90ab232e55D4BB7ed2dD17494D62fB', + tokenAddress: '0xB0B195aEFA3650A6908f15CdaC7D92F8a5791B0B', + relayerUrls: ['https://relayer-mvp.zkbob.com'], + delegatedProverUrls: ['https://remoteprover-mvp.zkbob.com/'], + coldStorageConfigPath: 'https://r2.zkbob.com/coldstorage/coldstorage.cfg', + kycUrls: { + status: 'https://api.knowyourcat.id/v1/%s/categories?category=BABTokenWeek', + homepage: 'https://knowyourcat.id/address/%s/BABTokenWeek', + }, + }, + 'BOB-optimism': { + chainId: 10, + poolAddress: '0x1CA8C2B9B20E18e86d5b9a72370fC6c91814c97C', + tokenAddress: '0xB0B195aEFA3650A6908f15CdaC7D92F8a5791B0B', + relayerUrls: ['https://relayer-optimism.zkbob.com/'], + delegatedProverUrls: [], + coldStorageConfigPath: '', + }, + }, + chains: { + '137': { + rpcUrls: ['https://polygon-rpc.com'], + }, + '10': { + rpcUrls: ['https://opt-mainnet.g.alchemy.com/v2/demo', 'https://mainnet.optimism.io'] + }, + }, + snarkParams: { + transferParamsUrl: 'https://r2.zkbob.com/transfer_params_22022023.bin', + transferVkUrl: 'https://r2.zkbob.com/transfer_verification_key_22022023.json' + }, + }, + dev: { + defaultPool: 'BOB-sepolia', + pools: { + 'BOB-sepolia': { + chainId: 11155111, + poolAddress: '0x3bd088C19960A8B5d72E4e01847791BD0DD1C9E6', + tokenAddress: '0x2C74B18e2f84B78ac67428d0c7a9898515f0c46f', + relayerUrls: ['https://relayer.thgkjlr.website/'], + delegatedProverUrls: ['https://prover-staging.thgkjlr.website/'], + coldStorageConfigPath: 'https://r2-staging.zkbob.com/coldstorage/coldstorage.cfg', + kycUrls: { + status: 'https://api-stage.knowyourcat.id/v1/%s/categories?category=BABTokenWeek', + homepage: 'https://stage.knowyourcat.id/address/%s/BABTokenWeek', + }, + }, + 'BOB-goerli': { + chainId: 5, + poolAddress: '0x49661694a71B3Dab9F25E86D5df2809B170c56E6', + tokenAddress: '0x97a4ab97028466FE67F18A6cd67559BAABE391b8', + relayerUrls: ['https://dev-relayer.thgkjlr.website/'], + delegatedProverUrls: [], + coldStorageConfigPath: '' + }, + 'BOB-op-goerli': { + chainId: 420, + poolAddress:'0x55B81b0730399974Ccad8AC858e766Cf54126596', + tokenAddress:'0x0fA7E69b9344D6434Bd6b79c5950bb5234245a5F', + relayerUrls:['https://gop-relayer.thgkjlr.website'], + delegatedProverUrls: [], + coldStorageConfigPath: '' + } + }, + chains: { + '11155111': { + rpcUrls: ['https://rpc.sepolia.org'], + }, + '5': { + rpcUrls: ['https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'] + }, + '420': { + rpcUrls: ['https://goerli.optimism.io'] + } + }, + snarkParams: { + transferParamsUrl: 'https://r2-staging.zkbob.com/transfer_params_20022023.bin', + transferVkUrl: 'https://r2-staging.zkbob.com/transfer_verification_key_20022023.json' + }, + } +}; + +export default config[process.env.REACT_APP_CONFIG]; diff --git a/src/constants/index.js b/src/constants/index.js index ba1733ed..09f087f7 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -18,29 +18,48 @@ export const TX_STATUSES = { export const NETWORKS = { 11155111: { name: 'Sepolia', - icon: require('assets/ethereum.svg').default, + icon: require('assets/polygon.svg').default, + blockExplorerUrls: { + address: 'https://sepolia.etherscan.io/address/%s', + tx: 'https://sepolia.etherscan.io/tx/%s', + }, }, 137: { name: 'Polygon', icon: require('assets/polygon.svg').default, + blockExplorerUrls: { + address: 'https://polygonscan.com/address/%s', + tx: 'https://polygonscan.com/tx/%s', + }, }, 5: { name: 'Goerli', icon: require('assets/ethereum.svg').default, + blockExplorerUrls: { + address: 'https://goerli.etherscan.io/address/%s', + tx: 'https://goerli.etherscan.io/tx/%s', + }, + }, + 420: { + name: 'Goerli Optimism', + icon: require('assets/optimism.svg').default, + blockExplorerUrls: { + address: 'https://goerli-optimism.etherscan.io/address/%s', + tx: 'https://goerli-optimism.etherscan.io/tx/%s', + }, + }, + 10: { + name: 'Optimism', + icon: require('assets/optimism.svg').default, + blockExplorerUrls: { + address: 'https://optimistic.etherscan.io/address/%s', + tx: 'https://optimistic.etherscan.io/tx/%s', + }, }, }; export const TOKEN_SYMBOL = process.env.REACT_APP_TOKEN_SYMBOL || 'BOB'; -export const HISTORY_ACTION_TYPES = { - DEPOSIT: 1, - TRANSFER_IN: 2, - TRANSFER_OUT: 3, - WITHDRAWAL: 4, - TRANSFER_SELF: 5, - DIRECT_DEPOSIT: 6, -}; - export const CONNECTORS_ICONS = { 'MetaMask': require('assets/metamask.svg').default, 'WalletConnect': require('assets/walletconnect.svg').default, diff --git a/src/containers/Header/index.js b/src/containers/Header/index.js index 1442782b..19f2917d 100644 --- a/src/containers/Header/index.js +++ b/src/containers/Header/index.js @@ -2,7 +2,10 @@ import React, { useContext, useCallback } from 'react'; import { useAccount, useDisconnect } from 'wagmi'; import Header from 'components/Header'; -import { ZkAccountContext, ModalContext, TokenBalanceContext } from 'contexts'; +import { + ZkAccountContext, ModalContext, + TokenBalanceContext, PoolContext, +} from 'contexts'; export default ({ empty }) => { const { address, connector } = useAccount(); @@ -11,13 +14,14 @@ export default ({ empty }) => { const { zkAccount, isLoadingZkAccount, balance: poolBalance, zkAccountId, updatePoolData, generateAddress, isDemo, - isLoadingState, + isLoadingState, switchToPool, initializeGiftCard, } = useContext(ZkAccountContext); const { openWalletModal, openSeedPhraseModal, - openAccountSetUpModal, openSwapOptionsModal, + openAccountSetUpModal, openSwapModal, openChangePasswordModal, openConfirmLogoutModal, } = useContext(ModalContext); + const { currentPool } = useContext(PoolContext); const refresh = useCallback(e => { e.stopPropagation(); @@ -30,7 +34,7 @@ export default ({ empty }) => {
{ openSeedPhraseModal={openSeedPhraseModal} isDemo={isDemo} disconnect={disconnect} + switchToPool={switchToPool} + currentPool={currentPool} + initializeGiftCard={initializeGiftCard} /> ); diff --git a/src/containers/IncreasedLimitsModal/index.js b/src/containers/IncreasedLimitsModal/index.js index 26ef3fc8..d5162786 100644 --- a/src/containers/IncreasedLimitsModal/index.js +++ b/src/containers/IncreasedLimitsModal/index.js @@ -3,7 +3,7 @@ import { useAccount } from 'wagmi' import IncreasedLimitsModal from 'components/IncreasedLimitsModal'; -import { ModalContext, IncreasedLimitsContext, ZkAccountContext } from 'contexts'; +import { ModalContext, IncreasedLimitsContext, ZkAccountContext, PoolContext } from 'contexts'; import { INCREASED_LIMITS_STATUSES } from 'constants'; @@ -15,6 +15,7 @@ export default () => { } = useContext(ModalContext); const { status, updateStatus } = useContext(IncreasedLimitsContext); const { updateLimits } = useContext(ZkAccountContext); + const { currentPool } = useContext(PoolContext); useEffect(() => { if (isIncreasedLimitsModalOpen && account) { @@ -41,6 +42,7 @@ export default () => { isWalletModalOpen={isWalletModalOpen} openWalletModal={openWalletModal} account={account} + currentPool={currentPool} /> ); } diff --git a/src/containers/PendingAction/index.js b/src/containers/PendingAction/index.js index 889245fc..31ade42a 100644 --- a/src/containers/PendingAction/index.js +++ b/src/containers/PendingAction/index.js @@ -4,10 +4,11 @@ import styled from 'styled-components'; import Card from 'components/Card'; import HistoryItem from 'components/HistoryItem'; -import { ZkAccountContext } from 'contexts'; +import { PoolContext, ZkAccountContext } from 'contexts'; export default () => { const { pendingActions } = useContext(ZkAccountContext); + const { currentPool } = useContext(PoolContext); const multi = pendingActions.length > 1; return ( { {pendingActions.map((action, index) => - + )} diff --git a/src/containers/RedeemGiftCardModal/index.js b/src/containers/RedeemGiftCardModal/index.js new file mode 100644 index 00000000..406c6d99 --- /dev/null +++ b/src/containers/RedeemGiftCardModal/index.js @@ -0,0 +1,75 @@ +import { useContext, useEffect, useCallback, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import { ModalContext, PoolContext, ZkAccountContext } from 'contexts'; +import RedeemGiftCardModal from 'components/RedeemGiftCardModal'; + +export default () => { + const { currentPool } = useContext(PoolContext); + const { + giftCard, initializeGiftCard, deleteGiftCard, + redeemGiftCard, switchToPool, zkAccount, isLoadingZkAccount, + } = useContext(ZkAccountContext); + const { + isRedeemGiftCardModalOpen, + openRedeemGiftCardModal, + closeRedeemGiftCardModal, + isPasswordModalOpen, + isAccountSetUpModalOpen, + openAccountSetUpModal, + isTermsModalOpen, + } = useContext(ModalContext); + const history = useHistory(); + const location = useLocation(); + const [isNewUser, setIsNewUser] = useState(false); + + useEffect(() => { + if (giftCard && !isPasswordModalOpen && !isAccountSetUpModalOpen && !isTermsModalOpen) { + openRedeemGiftCardModal(); + } + }, [ + giftCard, openRedeemGiftCardModal, isPasswordModalOpen, + isAccountSetUpModalOpen, isTermsModalOpen, + ]); + + useEffect(() => { + async function init() { + if (!window.localStorage.getItem('seed')) { + setIsNewUser(true); + } + const result = await initializeGiftCard(code); + if (result) { + queryParams.delete('gift-code'); + history.replace({ search: queryParams.toString() }); + } + } + const queryParams = new URLSearchParams(location.search); + const code = queryParams.get('gift-code'); + if (code) init(); + }, [history, location, initializeGiftCard]); + + const onClose = useCallback(() => { + deleteGiftCard(); + closeRedeemGiftCardModal(); + }, [deleteGiftCard, closeRedeemGiftCardModal]); + + const setUpAccount = useCallback(() => { + closeRedeemGiftCardModal(); + openAccountSetUpModal(); + }, [closeRedeemGiftCardModal, openAccountSetUpModal]); + + return ( + + ); +} diff --git a/src/containers/SwapModal/index.js b/src/containers/SwapModal/index.js index 1962f66e..5a6dcad8 100644 --- a/src/containers/SwapModal/index.js +++ b/src/containers/SwapModal/index.js @@ -8,34 +8,25 @@ import Modal from 'components/Modal'; import Button from 'components/Button'; export default () => { - const { - isSwapModalOpen, closeSwapModal, openSwapOptionsModal, - } = useContext(ModalContext); + const { isSwapModalOpen, closeSwapModal } = useContext(ModalContext); const widgetEvents = useWidgetEvents(); const [isInProgress, setIsInProgress] = useState(false); - const [nextAction, setNextAction] = useState(null); + const [isConfirmationShown, setIsConfirmationShown] = useState(false); - const close = useCallback(nextAction => { - closeSwapModal(); - if (nextAction === 'back') { - openSwapOptionsModal(); - } - }, [closeSwapModal, openSwapOptionsModal]); - - const tryToClose = useCallback(nextAction => { + const tryToClose = useCallback(() => { if (isInProgress) { - setNextAction(nextAction); + setIsConfirmationShown(true); return; } - close(nextAction); - }, [isInProgress, close]); + closeSwapModal(); + }, [isInProgress, closeSwapModal]); const confirm = useCallback(() => { - close(nextAction); - setNextAction(null); - }, [nextAction, close]); + closeSwapModal(); + setIsConfirmationShown(false); + }, [closeSwapModal]); - const reject = () => setNextAction(null); + const reject = () => setIsConfirmationShown(false); useEffect(() => { widgetEvents.on(WidgetEvent.RouteExecutionStarted, () => setIsInProgress(true)); @@ -47,22 +38,21 @@ export default () => { return ( tryToClose('close')} - onBack={nextAction ? null : () => tryToClose('back')} + onClose={isConfirmationShown ? null : () => tryToClose()} width={480} style={{ padding: '26px 0 0' }} - title={!!nextAction ? 'The swap is in progress' : null} + title={isConfirmationShown ? 'The swap is in progress' : null} > - {isInProgress && nextAction && ( + {isInProgress && isConfirmationShown && ( You can close this window and return later to view the results. Do you want to close the window? - No - Yes + No + Yes )} - + diff --git a/src/containers/SwapOptionsModal/index.js b/src/containers/SwapOptionsModal/index.js deleted file mode 100644 index 5307b411..00000000 --- a/src/containers/SwapOptionsModal/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import { useContext, useCallback } from 'react'; - -import { ModalContext } from 'contexts'; -import SwapOptionsModal from 'components/SwapOptionsModal'; - -export default () => { - const { - isSwapOptionsModalOpen, closeSwapOptionsModal, openSwapModal, - } = useContext(ModalContext); - - const onOpenSwapModal = useCallback(() => { - closeSwapOptionsModal(); - openSwapModal(); - }, [openSwapModal, closeSwapOptionsModal]); - - return ( - - ); -} diff --git a/src/containers/TransactionModal/index.js b/src/containers/TransactionModal/index.js index 30694cb5..d18b1496 100644 --- a/src/containers/TransactionModal/index.js +++ b/src/containers/TransactionModal/index.js @@ -1,12 +1,13 @@ import { useContext } from 'react'; -import { TransactionModalContext, SupportIdContext } from 'contexts'; +import { TransactionModalContext, SupportIdContext, PoolContext } from 'contexts'; import TransactionModal from 'components/TransactionModal'; export default () => { const { txStatus, isTxModalOpen, closeTxModal, txAmount, txError, } = useContext(TransactionModalContext); + const { currentPool } = useContext(PoolContext); const { supportId } = useContext(SupportIdContext); return ( { amount={txAmount} error={txError} supportId={supportId} + currentPool={currentPool} /> ); } diff --git a/src/contexts/IncreasedLimitsContext/index.js b/src/contexts/IncreasedLimitsContext/index.js index 928b1a2d..41a0343d 100644 --- a/src/contexts/IncreasedLimitsContext/index.js +++ b/src/contexts/IncreasedLimitsContext/index.js @@ -1,8 +1,10 @@ -import { createContext, useState, useEffect, useCallback } from 'react'; +import { createContext, useState, useEffect, useCallback, useContext } from 'react'; import { useAccount } from 'wagmi'; import * as Sentry from '@sentry/react'; import { INCREASED_LIMITS_STATUSES } from 'constants'; +import { PoolContext } from 'contexts'; +import config from 'config'; const DAY = 86400; // in seconds @@ -11,11 +13,13 @@ const IncreasedLimitsContext = createContext({}); export default IncreasedLimitsContext; export const IncreasedLimitsContextProvider = ({ children }) => { + const { currentPool } = useContext(PoolContext); const { address } = useAccount(); const [status, setStatus] = useState(null); const updateStatus = useCallback(async () => { - if (!process.env.REACT_APP_KYC_STATUS_URL) { + const { chainId, kycUrls } = config.pools[currentPool]; + if (!kycUrls) { setStatus(null); return; } @@ -26,13 +30,11 @@ export const IncreasedLimitsContextProvider = ({ children }) => { } try { const data = await ( - await fetch(process.env.REACT_APP_KYC_STATUS_URL.replace('%s', address)) + await fetch(kycUrls.status.replace('%s', address)) ).json(); const provider = data.data.providers.find(p => p.symbol === 'BABT'); if (provider.result) { - const syncData = provider.sync.byChainIds.find( - c => c.chainId === Number(process.env.REACT_APP_NETWORK) - ); + const syncData = provider.sync.byChainIds.find(c => c.chainId === chainId); if (syncData.syncTimestamp === 0) { status = INCREASED_LIMITS_STATUSES.INACTIVE; } else if ((+new Date() / 1000) - syncData.syncTimestamp > 7 * DAY) { @@ -46,7 +48,7 @@ export const IncreasedLimitsContextProvider = ({ children }) => { Sentry.captureException(error, { tags: { method: 'IncreasedLimitsContext.check' } }); } setStatus(status); - }, [address]); + }, [address, currentPool]); useEffect(() => { updateStatus(); diff --git a/src/contexts/ModalContext/index.js b/src/contexts/ModalContext/index.js index 5d89ff02..df66fc30 100644 --- a/src/contexts/ModalContext/index.js +++ b/src/contexts/ModalContext/index.js @@ -29,10 +29,6 @@ export const ModalContextProvider = ({ children }) => { const openSwapModal = () => setIsSwapModalOpen(true); const closeSwapModal = () => setIsSwapModalOpen(false); - const [isSwapOptionsModalOpen, setIsSwapOptionsModalOpen] = useState(false); - const openSwapOptionsModal = () => setIsSwapOptionsModalOpen(true); - const closeSwapOptionsModal = () => setIsSwapOptionsModalOpen(false); - const [isConfirmLogoutModalOpen, setIsConfirmLogoutModalOpen] = useState(false); const openConfirmLogoutModal = () => setIsConfirmLogoutModalOpen(true); const closeConfirmLogoutModal = () => setIsConfirmLogoutModalOpen(false); @@ -45,12 +41,15 @@ export const ModalContextProvider = ({ children }) => { const openIncreasedLimitsModal = () => setIsIncreasedLimitsModalOpen(true); const closeIncreasedLimitsModal = () => setIsIncreasedLimitsModalOpen(false); + const [isRedeemGiftCardModalOpen, setIsRedeemGiftCardModalOpen] = useState(false); + const openRedeemGiftCardModal = () => setIsRedeemGiftCardModalOpen(true); + const closeRedeemGiftCardModal = () => setIsRedeemGiftCardModalOpen(false); + const closeAllModals = () => { closeWalletModal(); closeAccountSetUpModal(); closeChangePasswordModal(); closeSwapModal(); - closeSwapOptionsModal(); closeConfirmLogoutModal(); closeSeedPhraseModal(); closeIncreasedLimitsModal(); @@ -65,10 +64,10 @@ export const ModalContextProvider = ({ children }) => { isChangePasswordModalOpen, openChangePasswordModal, closeChangePasswordModal, isTermsModalOpen, openTermsModal, closeTermsModal, isSwapModalOpen, openSwapModal, closeSwapModal, - isSwapOptionsModalOpen, openSwapOptionsModal, closeSwapOptionsModal, isConfirmLogoutModalOpen, openConfirmLogoutModal, closeConfirmLogoutModal, isSeedPhraseModalOpen, openSeedPhraseModal, closeSeedPhraseModal, isIncreasedLimitsModalOpen, openIncreasedLimitsModal, closeIncreasedLimitsModal, + isRedeemGiftCardModalOpen, openRedeemGiftCardModal, closeRedeemGiftCardModal, closeAllModals, }} > diff --git a/src/contexts/PoolContext/index.js b/src/contexts/PoolContext/index.js new file mode 100644 index 00000000..7d6314d2 --- /dev/null +++ b/src/contexts/PoolContext/index.js @@ -0,0 +1,29 @@ +import React, { createContext, useState, useCallback } from 'react'; +import * as Sentry from '@sentry/react'; + +import config from 'config'; + +const PoolContext = createContext({ currentPool: null }); + +export default PoolContext; + +export const PoolContextProvider = ({ children }) => { + const [currentPool, setPool] = useState(() => { + const poolId = window.localStorage.getItem('pool'); + return !!config.pools[poolId] ? poolId : config.defaultPool; + }); + + const setCurrentPool = useCallback(poolId => { + setPool(poolId); + localStorage.setItem('pool', poolId); + Sentry.configureScope(scope => { + scope.setTag('pool_id', poolId); + }); + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/contexts/SupportIdContext/index.js b/src/contexts/SupportIdContext/index.js index e14f3de2..a3bb9bd8 100644 --- a/src/contexts/SupportIdContext/index.js +++ b/src/contexts/SupportIdContext/index.js @@ -1,5 +1,5 @@ import { createContext, useState, useEffect, useCallback } from 'react'; -import * as Sentry from "@sentry/react"; +import * as Sentry from '@sentry/react'; import { v4 as uuidv4 } from 'uuid'; const SupportIdContext = createContext({ supportId: null }); diff --git a/src/contexts/TokenBalanceContext/index.js b/src/contexts/TokenBalanceContext/index.js index e0a0f676..5735aba4 100644 --- a/src/contexts/TokenBalanceContext/index.js +++ b/src/contexts/TokenBalanceContext/index.js @@ -1,9 +1,13 @@ -import React, { createContext, useState, useEffect, useCallback } from 'react'; +import React, { createContext, useState, useEffect, useCallback, useContext } from 'react'; import { ethers } from 'ethers'; import { useContract, useAccount, useProvider } from 'wagmi'; import * as Sentry from '@sentry/react'; -const TOKEN_ADDRESS = process.env.REACT_APP_TOKEN_ADDRESS; +import { PoolContext } from 'contexts'; + +import { showLoadingError } from 'utils'; +import config from 'config'; + const TOKEN_ABI = ['function balanceOf(address) pure returns (uint256)']; const TokenBalanceContext = createContext({ balance: null }); @@ -12,8 +16,13 @@ export default TokenBalanceContext; export const TokenBalanceContextProvider = ({ children }) => { const { address: account } = useAccount(); - const provider = useProvider(); - const token = useContract({ address: TOKEN_ADDRESS, abi: TOKEN_ABI, signerOrProvider: provider }); + const { currentPool } = useContext(PoolContext); + const provider = useProvider({ chainId: config.pools[currentPool].chainId }); + const token = useContract({ + address: config.pools[currentPool].tokenAddress, + abi: TOKEN_ABI, + signerOrProvider: provider + }); const [balance, setBalance] = useState(ethers.constants.Zero); const [isLoadingBalance, setIsLoadingBalance] = useState(false); @@ -26,6 +35,7 @@ export const TokenBalanceContextProvider = ({ children }) => { } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'TokenBalanceContext.updateBalance' } }); + showLoadingError('wallet balance'); } } setBalance(balance); diff --git a/src/contexts/ZkAccountContext/index.js b/src/contexts/ZkAccountContext/index.js index 988c0b74..a77edc5e 100644 --- a/src/contexts/ZkAccountContext/index.js +++ b/src/contexts/ZkAccountContext/index.js @@ -5,25 +5,24 @@ import AES from 'crypto-js/aes'; import Utf8 from 'crypto-js/enc-utf8'; import * as Sentry from "@sentry/react"; import { - TxType, TxDepositDeadlineExpiredError, InitState, + TxType, TxDepositDeadlineExpiredError, HistoryRecordState, HistoryTransactionType, - relayerFee, currentLimits, fetchVersion, - ServiceType, } from 'zkbob-client-js'; +import { ProverMode } from 'zkbob-client-js/lib/config'; import { - TransactionModalContext, ModalContext, + TransactionModalContext, ModalContext, PoolContext, TokenBalanceContext, SupportIdContext, } from 'contexts'; import { TX_STATUSES } from 'constants'; +import { showLoadingError } from 'utils'; +import { usePrevious } from 'hooks'; +import config from 'config'; import zp from './zp.js'; import { aggregateFees, splitDirectDeposits } from './utils.js'; -const TOKEN_ADDRESS = process.env.REACT_APP_TOKEN_ADDRESS; -const RELAYER_URL = process.env.REACT_APP_RELAYER_URL; - const ZkAccountContext = createContext({ zkAccount: null }); const defaultLimits = { @@ -37,18 +36,22 @@ const defaultLimits = { export default ZkAccountContext; export const ZkAccountContextProvider = ({ children }) => { + const { currentPool, setCurrentPool } = useContext(PoolContext); + const previousPool = usePrevious(currentPool); const { address: account } = useAccount(); - const { data: signer } = useSigner(); - const { chain, chains } = useNetwork(); + const { chain } = useNetwork(); + const currentChainId = config.pools[currentPool].chainId; + const { data: signer } = useSigner({ chainId: currentChainId }); const { switchNetworkAsync } = useSwitchNetwork({ - chainId: chains[0]?.id, + chainId: currentChainId, throwForSwitchChainNotSupported: true, }); const { openTxModal, setTxStatus, setTxAmount, setTxError } = useContext(TransactionModalContext); const { openPasswordModal, closePasswordModal, closeAllModals } = useContext(ModalContext); const { updateBalance: updateTokenBalance } = useContext(TokenBalanceContext); const { supportId, updateSupportId } = useContext(SupportIdContext); - const [zkAccount, setZkAccount] = useState(null); + const [zkClient, setZkClient] = useState(null); + const [zkAccount, setZkAccount] = useState(false); const [zkAccountId, setZkAccountId] = useState(null); const [balance, setBalance] = useState(ethers.constants.Zero); const [history, setHistory] = useState(null); @@ -64,29 +67,43 @@ export const ZkAccountContextProvider = ({ children }) => { const [loadingPercentage, setLoadingPercentage] = useState(null); const [relayerVersion, setRelayerVersion] = useState(null); const [isDemo, setIsDemo] = useState(false); - const [fee, setFee] = useState(null); + const [giftCard, setGiftCard] = useState(null); - const updateLoadingStatus = status => { - let loadingPercentage = null; - if (status.state === InitState.DownloadingParams) { - const { loaded, total } = status.download; - loadingPercentage = Math.round(loaded / total * 100); + useEffect(() => { + if (zkClient || !supportId || !currentPool) return; + async function create() { + try { + const zkClient = await zp.createClient(currentPool, supportId); + setZkClient(zkClient); + } catch (error) { + console.error(error); + Sentry.captureException(error, { tags: { method: 'ZkAccountContext.createZkClient' } }); + showLoadingError('zk-client'); + } } - setLoadingPercentage(loadingPercentage); - }; + create(); + }, [zkClient, currentPool, supportId]); + + const switchToPool = useCallback(async poolId => { + if (!zkClient) return; + await zkClient.switchToPool(poolId); + setCurrentPool(poolId); + }, [zkClient, setCurrentPool]); const loadZkAccount = useCallback(async (secretKey, birthIndex, useDelegatedProver = false) => { - let zkAccount = null; + let zkAccount = false; let zkAccountId = null; - if (secretKey) { + if (zkClient && secretKey) { setBalance(ethers.constants.Zero); setHistory(null); setIsLoadingZkAccount(true); try { - zkAccount = await zp.createAccount(secretKey, updateLoadingStatus, birthIndex, supportId, useDelegatedProver); + await zp.createAccount(zkClient, secretKey, birthIndex, useDelegatedProver); + zkAccount = true; } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.loadZkAccount' } }); + showLoadingError('zkAccount'); } zkAccountId = ethers.utils.id(secretKey); } @@ -94,35 +111,35 @@ export const ZkAccountContextProvider = ({ children }) => { setZkAccountId(zkAccountId); setIsLoadingZkAccount(false); setLoadingPercentage(0); - }, [supportId]); + }, [zkClient]); - const fromShieldedAmount = useCallback(shieldedAmount => { - if (!zkAccount) { - return BigNumber.from(shieldedAmount).mul(1e9); - } - const wei = zkAccount.shieldedAmountToWei(TOKEN_ADDRESS, shieldedAmount); + const fromShieldedAmount = useCallback(async shieldedAmount => { + if (!zkClient) return BigNumber.from(0); + const wei = await zkClient.shieldedAmountToWei(shieldedAmount); return BigNumber.from(wei); - }, [zkAccount]); + }, [zkClient]); const toShieldedAmount = useCallback(wei => { - return zkAccount.weiToShieldedAmount(TOKEN_ADDRESS, wei.toBigInt()); - }, [zkAccount]); + if (!zkClient) return BigInt(0); + return zkClient.weiToShieldedAmount(wei.toBigInt()); + }, [zkClient]); const updateBalance = useCallback(async () => { let balance = ethers.constants.Zero; if (zkAccount) { setIsLoadingState(true); try { - balance = await zkAccount.getTotalBalance(TOKEN_ADDRESS); - balance = fromShieldedAmount(balance); + balance = await zkClient.getTotalBalance(); + balance = await fromShieldedAmount(balance); } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.updateBalance' } }); + showLoadingError('zkAccount balance'); } } setBalance(balance); setIsLoadingState(false); - }, [zkAccount, fromShieldedAmount]); + }, [zkAccount, zkClient, fromShieldedAmount]); const updateHistory = useCallback(async () => { let history = []; @@ -130,24 +147,32 @@ export const ZkAccountContextProvider = ({ children }) => { let pendingActions = []; let atomicTxFee = ethers.constants.Zero; if (zkAccount) { + if (currentPool !== previousPool) { + setHistory([]); + setIsPending(false); + setPendingActions([]); + } setIsLoadingHistory(true); try { [history, atomicTxFee] = await Promise.all([ - zkAccount.getAllHistory(TOKEN_ADDRESS), - zkAccount.atomicTxFee(TOKEN_ADDRESS), + zkClient.getAllHistory(), + zkClient.atomicTxFee(), ]); - history = history.map(item => ({ + history = await Promise.all(history.map(async item => ({ ...item, failed: [HistoryRecordState.RejectedByRelayer, HistoryRecordState.RejectedByPool].includes(item.state), - actions: item.actions.map(action => ({ ...action, amount: fromShieldedAmount(action.amount) })), - fee: fromShieldedAmount(item.fee), - })); + actions: await Promise.all(item.actions.map(async action => ({ + ...action, + amount: await fromShieldedAmount(action.amount) + }))), + fee: await fromShieldedAmount(item.fee), + }))); history = splitDirectDeposits(history); history = aggregateFees(history); - history = history.map(item => ({ + history = await Promise.all(history.map(async item => ({ ...item, - highFee: item.fee.gt(fromShieldedAmount(atomicTxFee)), - })); + highFee: item.fee.gt(await fromShieldedAmount(atomicTxFee)), + }))); history = history.reverse(); pendingActions = history.filter(item => item.state === HistoryRecordState.Pending && item.type !== HistoryTransactionType.TransferIn @@ -156,97 +181,89 @@ export const ZkAccountContextProvider = ({ children }) => { } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.updateHistory' } }); + showLoadingError('history'); } } setHistory(history); setPendingActions(pendingActions); setIsPending(isPending); setIsLoadingHistory(false); - }, [zkAccount, fromShieldedAmount]); + }, [zkAccount, zkClient, fromShieldedAmount, currentPool, previousPool]); const updateLimits = useCallback(async () => { - if (!supportId) return; + if (!zkClient) return; setIsLoadingLimits(true); let limits = defaultLimits; try { - let data; - if (zkAccount) { - data = await zkAccount.getLimits(TOKEN_ADDRESS, account); - } else { - const res = await currentLimits(RELAYER_URL, supportId, account); - data = { deposit: { components: { ...res.deposit } }, withdraw: { components: { ...res.withdraw } } }; - } + const data = await zkClient.getLimits(account); limits = { - singleDepositLimit: fromShieldedAmount(BigInt(data.deposit.components.singleOperation)), + singleDepositLimit: await fromShieldedAmount(BigInt(data.deposit.components.singleOperation)), dailyDepositLimitPerAddress: { - total: fromShieldedAmount(BigInt(data.deposit.components.dailyForAddress.total)), - available: fromShieldedAmount(BigInt(data.deposit.components.dailyForAddress.available)) + total: await fromShieldedAmount(BigInt(data.deposit.components.dailyForAddress.total)), + available: await fromShieldedAmount(BigInt(data.deposit.components.dailyForAddress.available)) }, dailyDepositLimit: { - total: fromShieldedAmount(BigInt(data.deposit.components.dailyForAll.total)), - available: fromShieldedAmount(BigInt(data.deposit.components.dailyForAll.available)) + total: await fromShieldedAmount(BigInt(data.deposit.components.dailyForAll.total)), + available: await fromShieldedAmount(BigInt(data.deposit.components.dailyForAll.available)) }, dailyWithdrawalLimit: { - total: fromShieldedAmount(BigInt(data.withdraw.components.dailyForAll.total)), - available: fromShieldedAmount(BigInt(data.withdraw.components.dailyForAll.available)) + total: await fromShieldedAmount(BigInt(data.withdraw.components.dailyForAll.total)), + available: await fromShieldedAmount(BigInt(data.withdraw.components.dailyForAll.available)) }, poolSizeLimit: { - total: fromShieldedAmount(BigInt(data.deposit.components.poolLimit.total)), - available: fromShieldedAmount(BigInt(data.deposit.components.poolLimit.available)) + total: await fromShieldedAmount(BigInt(data.deposit.components.poolLimit.total)), + available: await fromShieldedAmount(BigInt(data.deposit.components.poolLimit.available)) }, }; } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.updateLimits' } }); + showLoadingError('limits'); } setLimits(limits); setIsLoadingLimits(false); - }, [zkAccount, account, fromShieldedAmount, supportId]); + }, [zkClient, account, fromShieldedAmount]); const updateMaxTransferable = useCallback(async () => { let maxTransferable = ethers.constants.Zero; if (zkAccount) { try { - const max = await zkAccount.calcMaxAvailableTransfer(TOKEN_ADDRESS, false); - maxTransferable = fromShieldedAmount(max); + const max = await zkClient.calcMaxAvailableTransfer(false); + maxTransferable = await fromShieldedAmount(max); } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.updateMaxTransferable' } }); } } setMaxTransferable(maxTransferable); - }, [zkAccount, fromShieldedAmount]); + }, [zkAccount, zkClient, fromShieldedAmount]); const loadMinTxAmount = useCallback(async () => { let minTxAmount = ethers.constants.Zero; if (zkAccount) { try { - minTxAmount = await zkAccount.minTxAmount(TOKEN_ADDRESS); - minTxAmount = fromShieldedAmount(minTxAmount); + minTxAmount = await zkClient.minTxAmount(); + minTxAmount = await fromShieldedAmount(minTxAmount); } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.loadMinTxAmount' } }); } } setMinTxAmount(minTxAmount); - }, [zkAccount, fromShieldedAmount]); + }, [zkAccount, zkClient, fromShieldedAmount]); const loadRelayerVersion = useCallback(async () => { + if (!zkClient) return; let version = null; try { - let data; - if (zkAccount) { - data = await zkAccount.getRelayerVersion(TOKEN_ADDRESS); - } else { - data = await fetchVersion(RELAYER_URL, ServiceType.Relayer); - } + const data = await zkClient.getRelayerVersion(); version = data.ref; } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.loadRelayerVersion' } }); } setRelayerVersion(version); - }, [zkAccount]); + }, [zkClient]); const updatePoolData = useCallback(() => Promise.all([ updateBalance(), @@ -258,7 +275,7 @@ export const ZkAccountContextProvider = ({ children }) => { openTxModal(); setTxAmount(amount); try { - if (chain.id !== chains[0].id) { + if (chain.id !== currentChainId) { setTxStatus(TX_STATUSES.SWITCH_NETWORK); try { await switchNetworkAsync(); @@ -269,9 +286,9 @@ export const ZkAccountContextProvider = ({ children }) => { return; } } - const shieldedAmount = toShieldedAmount(amount); - const { totalPerTx: fee } = await zkAccount.feeEstimate(TOKEN_ADDRESS, [shieldedAmount], TxType.Deposit, false); - await zp.deposit(signer, zkAccount, shieldedAmount, fee, setTxStatus); + const shieldedAmount = await toShieldedAmount(amount); + const { totalPerTx: fee } = await zkClient.feeEstimate([shieldedAmount], TxType.Deposit, false); + await zp.deposit(signer, zkClient, shieldedAmount, fee, setTxStatus); updatePoolData(); setTimeout(updateTokenBalance, 5000); } catch (error) { @@ -287,18 +304,18 @@ export const ZkAccountContextProvider = ({ children }) => { } } }, [ - zkAccount, updatePoolData, signer, openTxModal, setTxAmount, + zkClient, updatePoolData, signer, openTxModal, setTxAmount, setTxStatus, updateTokenBalance, toShieldedAmount, setTxError, - chain, chains, switchNetworkAsync, + chain, switchNetworkAsync, currentChainId, ]); const transfer = useCallback(async (to, amount) => { openTxModal(); try { setTxAmount(amount); - const shieldedAmount = toShieldedAmount(amount); - const { totalPerTx: fee } = await zkAccount.feeEstimate(TOKEN_ADDRESS, [shieldedAmount], TxType.Transfer, false); - await zp.transfer(zkAccount, [{ destination: to, amountGwei: shieldedAmount }], fee, setTxStatus); + const shieldedAmount = await toShieldedAmount(amount); + const { totalPerTx: fee } = await zkClient.feeEstimate([shieldedAmount], TxType.Transfer, false); + await zp.transfer(zkClient, [{ destination: to, amountGwei: shieldedAmount }], fee, setTxStatus); updatePoolData(); } catch (error) { console.error(error); @@ -307,7 +324,7 @@ export const ZkAccountContextProvider = ({ children }) => { setTxStatus(TX_STATUSES.REJECTED); } }, [ - zkAccount, updatePoolData, openTxModal, setTxError, + zkClient, updatePoolData, openTxModal, setTxError, setTxStatus, toShieldedAmount, setTxAmount, ]); @@ -315,13 +332,13 @@ export const ZkAccountContextProvider = ({ children }) => { openTxModal(); try { setTxAmount(data.reduce((acc, curr) => acc.add(curr.amount), ethers.constants.Zero)); - const transfers = data.map(({ address, amount }) => ({ + const transfers = await Promise.all(data.map(async ({ address, amount }) => ({ destination: address, - amountGwei: toShieldedAmount(amount) - })); + amountGwei: await toShieldedAmount(amount) + }))); const shieldedAmounts = transfers.map(tr => tr.amountGwei); - const { totalPerTx: fee } = await zkAccount.feeEstimate(TOKEN_ADDRESS, shieldedAmounts, TxType.Transfer, false); - await zp.transfer(zkAccount, transfers, fee, setTxStatus, true); + const { totalPerTx: fee } = await zkClient.feeEstimate(shieldedAmounts, TxType.Transfer, false); + await zp.transfer(zkClient, transfers, fee, setTxStatus, true); updatePoolData(); } catch (error) { console.error(error); @@ -330,7 +347,7 @@ export const ZkAccountContextProvider = ({ children }) => { setTxStatus(TX_STATUSES.REJECTED); } }, [ - zkAccount, updatePoolData, openTxModal, setTxError, + zkClient, updatePoolData, openTxModal, setTxError, setTxStatus, toShieldedAmount, setTxAmount, ]); @@ -338,9 +355,9 @@ export const ZkAccountContextProvider = ({ children }) => { openTxModal(); setTxAmount(amount); try { - const shieldedAmount = toShieldedAmount(amount); - const { totalPerTx: fee } = await zkAccount.feeEstimate(TOKEN_ADDRESS, [shieldedAmount], TxType.Withdraw, false); - await zp.withdraw(zkAccount, to, shieldedAmount, fee, setTxStatus); + const shieldedAmount = await toShieldedAmount(amount); + const { totalPerTx: fee } = await zkClient.feeEstimate([shieldedAmount], TxType.Withdraw, false); + await zp.withdraw(zkClient, to, shieldedAmount, fee, setTxStatus); updatePoolData(); setTimeout(updateTokenBalance, 5000); } catch (error) { @@ -354,41 +371,69 @@ export const ZkAccountContextProvider = ({ children }) => { } } }, [ - zkAccount, updatePoolData, openTxModal, setTxAmount, setTxError, + zkClient, updatePoolData, openTxModal, setTxAmount, setTxError, setTxStatus, updateTokenBalance, toShieldedAmount, ]); const generateAddress = useCallback(() => { if (!zkAccount) return; - return zkAccount.generateAddress(TOKEN_ADDRESS); - }, [zkAccount]); + return zkClient.generateAddress(); + }, [zkAccount, zkClient]); const verifyShieldedAddress = useCallback(address => { - if (!zkAccount) return false; - return zkAccount.verifyShieldedAddress(address); - }, [zkAccount]); + if (!zkClient) return false; + return zkClient.verifyShieldedAddress(address); + }, [zkClient]); const estimateFee = useCallback(async (amounts, txType) => { - if (!supportId) return null; + if (!zkClient) return null; try { if (!zkAccount) { - let atomicFee = fee; - if (!atomicFee) { - atomicFee = await relayerFee(RELAYER_URL, supportId); - atomicFee = fromShieldedAmount(atomicFee); - setFee(atomicFee); - } - return { fee: atomicFee, numberOfTxs: 1, insufficientFunds: false }; + let atomicTxFee = await zkClient.atomicTxFee(); + atomicTxFee = await fromShieldedAmount(atomicTxFee); + return { fee: atomicTxFee, numberOfTxs: 1, insufficientFunds: false }; } - const shieldedAmounts = amounts.map(amount => toShieldedAmount(amount)); - const { total, txCnt, insufficientFunds } = await zkAccount.feeEstimate(TOKEN_ADDRESS, shieldedAmounts, txType, false); - return { fee: fromShieldedAmount(total), numberOfTxs: txCnt, insufficientFunds }; + const shieldedAmounts = await Promise.all(amounts.map(async amount => await toShieldedAmount(amount))); + const { total, txCnt, insufficientFunds } = await zkClient.feeEstimate(shieldedAmounts, txType, false); + return { fee: await fromShieldedAmount(total), numberOfTxs: txCnt, insufficientFunds }; } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.estimateFee' } }); return null; } - }, [zkAccount, toShieldedAmount, fromShieldedAmount, supportId, fee]); + }, [zkClient, toShieldedAmount, fromShieldedAmount, zkAccount]); + + const initializeGiftCard = useCallback(async code => { + if (!zkClient) return false; + const parsed = await zkClient.giftCardFromCode(code); + parsed.balance = await fromShieldedAmount(parsed.balance); + setGiftCard(parsed); + return true; + }, [zkClient, fromShieldedAmount]); + + const deleteGiftCard = () => setGiftCard(null); + + const redeemGiftCard = useCallback(async () => { + try { + const targetPool = giftCard.poolAlias; + if (currentPool !== targetPool) { + await switchToPool(targetPool); + } + const proverExists = config.pools[targetPool].delegatedProverUrls.length > 0; + const jobId = await zkClient.redeemGiftCard({ + sk: giftCard.sk, + pool: targetPool, + birthindex: Number(giftCard.birthIndex), + proverMode: proverExists ? ProverMode.Delegated : ProverMode.Local, + }); + await zkClient.waitJobTxHash(jobId); + deleteGiftCard(); + } catch (error) { + console.error(error); + Sentry.captureException(error, { tags: { method: 'ZkAccountContext.redeemGiftCard' } }); + throw error; + } + }, [zkClient, giftCard, switchToPool, currentPool]); const decryptMnemonic = password => { const cipherText = window.localStorage.getItem('seed'); @@ -438,11 +483,11 @@ export const ZkAccountContextProvider = ({ children }) => { const removeZkAccountMnemonic = useCallback(async () => { if (zkAccount) { - await zkAccount.cleanState(TOKEN_ADDRESS); + await zkClient.logout(); } window.localStorage.removeItem('seed'); clearState(); - }, [zkAccount, clearState]); + }, [zkAccount, zkClient, clearState]); const lockAccount = useCallback(() => { clearState(); @@ -453,15 +498,15 @@ export const ZkAccountContextProvider = ({ children }) => { useEffect(() => { updatePoolData(); - }, [updatePoolData]); + }, [updatePoolData, currentPool]); useEffect(() => { loadMinTxAmount(); - }, [loadMinTxAmount]); + }, [loadMinTxAmount, currentPool]); useEffect(() => { updateMaxTransferable(); - }, [updateMaxTransferable, balance]); + }, [updateMaxTransferable, balance, currentPool]); useEffect(() => { if (isPending) { @@ -483,10 +528,7 @@ export const ZkAccountContextProvider = ({ children }) => { useEffect(() => { const seed = window.localStorage.getItem('seed'); - const demoCode = (new URLSearchParams(window.location.search)).get('code'); - if (demoCode) { - setIsDemo(true); - } else if (seed && !zkAccount) { + if (seed && !zkAccount) { openPasswordModal(); } }, []); // eslint-disable-line react-hooks/exhaustive-deps @@ -509,6 +551,7 @@ export const ZkAccountContextProvider = ({ children }) => { removeZkAccountMnemonic, updatePoolData, minTxAmount, loadingPercentage, estimateFee, maxTransferable, isLoadingLimits, limits, changePassword, verifyPassword, verifyShieldedAddress, decryptMnemonic, relayerVersion, isDemo, updateLimits, lockAccount, + switchToPool, giftCard, initializeGiftCard, deleteGiftCard, redeemGiftCard, }} > {children} diff --git a/src/contexts/ZkAccountContext/zp.js b/src/contexts/ZkAccountContext/zp.js index fd44f577..02aa1c12 100644 --- a/src/contexts/ZkAccountContext/zp.js +++ b/src/contexts/ZkAccountContext/zp.js @@ -1,88 +1,72 @@ import { Contract, ethers } from 'ethers'; -import { init as initZkBob, ZkBobClient } from 'zkbob-client-js'; +import { ZkBobClient } from 'zkbob-client-js'; import { deriveSpendingKeyZkBob } from 'zkbob-client-js/lib/utils'; import { ProverMode } from 'zkbob-client-js/lib/config'; -import { EvmNetwork } from 'zkbob-client-js/lib/networks/evm'; import { TX_STATUSES } from 'constants'; import { createPermitSignature } from 'utils/token'; +import config from 'config'; -const POOL_ADDRESS = process.env.REACT_APP_CONTRACT_ADDRESS; -const TOKEN_ADDRESS = process.env.REACT_APP_TOKEN_ADDRESS; -const RELAYER_URL = process.env.REACT_APP_RELAYER_URL; -const RPC_URL = process.env.REACT_APP_RPC_URL; -const BUCKET_URL = process.env.REACT_APP_BUCKET_URL; -const PROVER_URL = process.env.REACT_APP_PROVER_URL; -const SNARK_PARAMS_VERSION = process.env.REACT_APP_SNARK_PARAMS_VERSION; - -const snarkParams = { - transferParamsUrl: `${BUCKET_URL}/transfer_params_${SNARK_PARAMS_VERSION}.bin`, - treeParamsUrl: `${BUCKET_URL}/tree_params_${SNARK_PARAMS_VERSION}.bin`, - transferVkUrl: `${BUCKET_URL}/transfer_verification_key_${SNARK_PARAMS_VERSION}.json`, - treeVkUrl: `${BUCKET_URL}/tree_verification_key_${SNARK_PARAMS_VERSION}.json`, +const createClient = (currentPool, supportId) => { + return ZkBobClient.create({ + pools: config.pools, + chains: config.chains, + snarkParams: config.snarkParams, + supportId, + }, currentPool); }; -const createAccount = async (secretKey, statusCallback, birthIndex, supportId, useDelegatedProver) => { - const ctx = await initZkBob(snarkParams, RELAYER_URL, statusCallback); - const network = process.env.REACT_APP_ZEROPOOL_NETWORK; +const createAccount = async (zkClient, secretKey, birthIndex, useDelegatedProver) => { let sk = ethers.utils.isValidMnemonic(secretKey) - ? deriveSpendingKeyZkBob(secretKey, network) + ? deriveSpendingKeyZkBob(secretKey) : ethers.utils.arrayify(secretKey); - const tokens = { - [TOKEN_ADDRESS]: { - poolAddress: POOL_ADDRESS, - relayerUrl: RELAYER_URL, - coldStorageConfigPath: `${BUCKET_URL}/coldstorage/coldstorage.cfg`, - birthindex: birthIndex, - proverMode: (useDelegatedProver && !!PROVER_URL) ? ProverMode.Delegated : ProverMode.Local, - delegatedProverUrl: PROVER_URL, - } - }; - return ZkBobClient.create({ + const currentPool = zkClient.currentPool(); + const proverExists = config.pools[currentPool].delegatedProverUrls.length > 0; + return zkClient.login({ sk, - tokens, - worker: ctx.worker, - networkName: network, - network: new EvmNetwork(RPC_URL), - supportId, + pool: currentPool, + birthindex: birthIndex, + proverMode: (useDelegatedProver && proverExists) ? ProverMode.Delegated : ProverMode.Local, }); }; -const deposit = async (signer, account, amount, fee, setTxStatus) => { +const deposit = async (signer, zkClient, amount, fee, setTxStatus) => { const tokenABI = [ 'function name() view returns (string)', 'function nonces(address) view returns (uint256)', ]; - const token = new Contract(TOKEN_ADDRESS, tokenABI, signer); + const currentPool = zkClient.currentPool(); + const { tokenAddress, poolAddress } = config.pools[currentPool] + const token = new Contract(tokenAddress, tokenABI, signer); setTxStatus(TX_STATUSES.GENERATING_PROOF); const signFunction = async (deadline, value, salt) => { setTxStatus(TX_STATUSES.SIGN_MESSAGE); - const signature = await createPermitSignature(token, signer, POOL_ADDRESS, value, deadline, salt); + const signature = await createPermitSignature(token, signer, poolAddress, value, deadline, salt); setTxStatus(TX_STATUSES.GENERATING_PROOF); return signature; }; const myAddress = await signer.getAddress(); - const jobId = await account.depositPermittable(TOKEN_ADDRESS, amount, signFunction, myAddress, fee); + const jobId = await zkClient.depositPermittable(amount, signFunction, myAddress, fee); setTxStatus(TX_STATUSES.WAITING_FOR_RELAYER); - await account.waitJobTxHash(TOKEN_ADDRESS, jobId); + await zkClient.waitJobTxHash(jobId); setTxStatus(TX_STATUSES.DEPOSITED); }; -const transfer = async (account, transfers, fee, setTxStatus, isMulti) => { +const transfer = async (zkClient, transfers, fee, setTxStatus, isMulti) => { setTxStatus(TX_STATUSES.GENERATING_PROOF); - const jobIds = await account.transferMulti(TOKEN_ADDRESS, transfers, fee); + const jobIds = await zkClient.transferMulti(transfers, fee); setTxStatus(TX_STATUSES.WAITING_FOR_RELAYER); - await account.waitJobsTxHashes(TOKEN_ADDRESS, jobIds); + await zkClient.waitJobsTxHashes(jobIds); setTxStatus(TX_STATUSES[isMulti ? 'TRANSFERRED_MULTI' : 'TRANSFERRED']); }; -const withdraw = async (account, to, amount, fee, setTxStatus) => { +const withdraw = async (zkClient, to, amount, fee, setTxStatus) => { setTxStatus(TX_STATUSES.GENERATING_PROOF); - const jobIds = await account.withdrawMulti(TOKEN_ADDRESS, to, amount, fee); + const jobIds = await zkClient.withdrawMulti(to, amount, fee); setTxStatus(TX_STATUSES.WAITING_FOR_RELAYER); - await account.waitJobsTxHashes(TOKEN_ADDRESS, jobIds); + await zkClient.waitJobsTxHashes(jobIds); setTxStatus(TX_STATUSES.WITHDRAWN); }; -const zp = { createAccount, deposit, transfer, withdraw }; +const zp = { createClient, createAccount, deposit, transfer, withdraw }; export default zp; diff --git a/src/contexts/index.js b/src/contexts/index.js index 7a8b3f95..8b632f6e 100644 --- a/src/contexts/index.js +++ b/src/contexts/index.js @@ -6,18 +6,21 @@ import TransactionModalContext, { TransactionModalContextProvider } from 'contex import ModalContext, { ModalContextProvider } from 'contexts/ModalContext'; import SupportIdContext, { SupportIdContextProvider } from 'contexts/SupportIdContext'; import IncreasedLimitsContext, { IncreasedLimitsContextProvider } from 'contexts/IncreasedLimitsContext'; +import PoolContext, { PoolContextProvider } from 'contexts/PoolContext'; const ContextsProvider = ({ children }) => ( - - - - {children} - - - + + + + + {children} + + + + @@ -26,5 +29,5 @@ const ContextsProvider = ({ children }) => ( export default ContextsProvider; export { ZkAccountContext, TokenBalanceContext, TransactionModalContext, - ModalContext, SupportIdContext, IncreasedLimitsContext, + ModalContext, SupportIdContext, IncreasedLimitsContext, PoolContext, }; diff --git a/src/fonts/Gilroy-ExtraBold.woff b/src/fonts/Gilroy-ExtraBold.woff new file mode 100644 index 0000000000000000000000000000000000000000..c83a59d34be9c736ffb16f78231a4b88f3061c95 GIT binary patch literal 35332 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
Lq z>e&;ghV~2g#HrE69O7M(x|kCCip0Jmv9HLnmAOBoPQ|$f7*a*=d*ybyQ|^IY-w&<* z5$NDQfkyqjyePkuSD{1y4H|O@y|`N^L-U=d3v`k04UP9uU8=`Ja-RlSeJ-@KHF^m& z(JS?8-JlyG&+b4k`_PvM^mb)|Mo%9WpvphdjAJxmwnXT_{pZgliFPr<=`gLud6=loyu{895NA`!Z6WH>4 zQ+Zmt--MRTcja??b$>n3zG=t21^s9}7Pu9^W#?r3zVq{dV@bs3hZ(1NC2X6|z^3^e zBQkG$suY>2|x5-I?w@cY(Xe-P=9DJ=9(59`CMnPjk<7&vh?w*SMFsm%CTGSGybBjqWY( z9qwIjpL@Uip!=}y)9&f5Q+w1ifdV6^LcuTy) zy=C5V?<8-Pcec0MyU<(fUFxm#)_d1@*Lyd4w|bksyS;CE4|orG1Kwla6W&wavtGq} z*?Yx%-TNzy=D_dp$NQ7~>Hb`QXMcBpFMmJ(VE;(}Sbv3oihsI)j(@&?k$>H#2@mX_h0mX=fCRL{J#ZG5C)w=cQ84a8O#e7 z1dD>bg9Cy?gQdao!OGyY;LPCM;DTUHa7l1^aAk0Hup!tO+!EXo+!gc%_XiIK4+oD1 zgTa%*)4>bDZ-Q#@TJY!KjhvT@a$UKe+|=CcTyJh+ZjanPxh1*7bIWqeb0_6i<<8En z&Rv*Wo4Yi(F1J2+P44>KO}Sfhn{#*PzL|R<_fT#i_gLX3$F;T3a<@s2seed zhj)heg!hI0;djGF!pFm(ghS!;;fvw#!dJsu__w^15A&V*?)>EZ%>2Ckg8ZWV-uVOa zhvt{&kI%2npO!x}e{TMQ{F?kF`OEWH=C96g$ZyQwlD{K=SH3TQfBwPz!}&+^gZU@( zPv>9A|0Z9}zn1@V{*B0sqNpqCiKa%gquyv?v`4g0v?Mw_S{5yjPKs7VXGg1}3!}Bs zrO~=*eRNH9eRNZFYqUAKJNjnyK=e>F5Iq(>5j_<>8&#s0qgSHWqrVoU5EMEJ;|r4t z(+hJ8I~R5@>{ZyWaB$(s!m))Fg;NTr7tSf1U%04napAJU6@{w`*A{LlY%1JdxU+Ci z;l4tD;k$)L3XeDP;4JU79AYW!v;6q8EPu9{-^cveUPZnu8)I(Gn!_?YIOfeuVqVBL z85z=oXJ@W;CTHsR&6LZTGWmK7ew^j&F^|mfLis53YuKMGU(E1$xr}YTH-cxskQsZ1 z*V>wox8~pSswrpZ|E-i8GeJY2!}G3_jj|WdcJBwOL%!!(_<%h8M-^+KVen@^HTV$>L33mUdGgGyp>F2F3sYe8hpb literal 0 HcmV?d00001 diff --git a/src/hooks/index.js b/src/hooks/index.js index 98f71c34..ee6693df 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -4,6 +4,7 @@ import useRestriction from 'hooks/useRestriction'; import useParsedAmount from 'hooks/useParsedAmount'; import useLatestAction from 'hooks/useLatestAction'; import useWindowDimensions from 'hooks/useWindowDimensions'; +import usePrevious from 'hooks/usePrevious'; export { useDateFromNow, @@ -12,4 +13,5 @@ export { useParsedAmount, useLatestAction, useWindowDimensions, + usePrevious, }; diff --git a/src/hooks/useFee.js b/src/hooks/useFee.js index 10204f96..d83bac45 100644 --- a/src/hooks/useFee.js +++ b/src/hooks/useFee.js @@ -1,10 +1,11 @@ import { useState, useEffect, useContext } from 'react'; import { ethers } from 'ethers'; -import { ZkAccountContext } from 'contexts'; +import { ZkAccountContext, PoolContext } from 'contexts'; export default (amount, txType) => { const { estimateFee } = useContext(ZkAccountContext); + const { currentPool } = useContext(PoolContext); const [fee, setFee] = useState(ethers.constants.Zero); const [numberOfTxs, setNumberOfTxs] = useState(ethers.constants.Zero); const [isLoadingFee, setIsLoadingFee] = useState(false); @@ -20,7 +21,7 @@ export default (amount, txType) => { setIsLoadingFee(false); } updateFee(); - }, [amount, txType, estimateFee]); + }, [amount, txType, estimateFee, currentPool]); return { fee, numberOfTxs, isLoadingFee }; }; diff --git a/src/hooks/usePrevious.js b/src/hooks/usePrevious.js new file mode 100644 index 00000000..acb356cd --- /dev/null +++ b/src/hooks/usePrevious.js @@ -0,0 +1,9 @@ +import { useRef, useEffect } from 'react'; + +export default value => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} diff --git a/src/pages/Deposit/index.js b/src/pages/Deposit/index.js index cedd393d..17bcda08 100644 --- a/src/pages/Deposit/index.js +++ b/src/pages/Deposit/index.js @@ -3,11 +3,12 @@ import { useAccount } from 'wagmi' import { TxType } from 'zkbob-client-js'; import { ethers } from 'ethers'; import * as Sentry from '@sentry/react'; +import { HistoryTransactionType } from 'zkbob-client-js'; import AccountSetUpButton from 'containers/AccountSetUpButton'; import PendingAction from 'containers/PendingAction'; -import { ZkAccountContext, TokenBalanceContext, ModalContext, IncreasedLimitsContext } from 'contexts'; +import { ZkAccountContext, TokenBalanceContext, ModalContext, IncreasedLimitsContext, PoolContext } from 'contexts'; import TransferInput from 'components/TransferInput'; import Card from 'components/Card'; @@ -22,8 +23,7 @@ import { useDepositLimit, useMaxAmountExceeded } from './hooks'; import { tokenSymbol } from 'utils/token'; import { formatNumber, minBigNumber } from 'utils'; - -import { HISTORY_ACTION_TYPES } from 'constants'; +import config from 'config'; const note = `${tokenSymbol()} will be deposited to your account inside the zero knowledge pool.`; @@ -39,10 +39,12 @@ export default () => { const { status: increasedLimitsStatus } = useContext(IncreasedLimitsContext); const [displayAmount, setDisplayAmount] = useState(''); const amount = useParsedAmount(displayAmount); - const latestAction = useLatestAction(HISTORY_ACTION_TYPES.DEPOSIT); + const latestAction = useLatestAction(HistoryTransactionType.Deposit); const { fee, isLoadingFee } = useFee(amount, TxType.Deposit); const depositLimit = useDepositLimit(); const maxAmountExceeded = useMaxAmountExceeded(amount, balance, fee, depositLimit); + const { currentPool } = useContext(PoolContext); + const { chainId, kycUrls } = config.pools[currentPool]; const onDeposit = useCallback(() => { setDisplayAmount(''); @@ -91,11 +93,12 @@ export default () => { else return ; })()}
- {(increasedLimitsStatus && process.env.REACT_APP_KYC_STATUS_URL) && + {(increasedLimitsStatus && !!kycUrls) && } { shielded={false} actions={latestAction.actions} txHash={latestAction.txHash} + currentChainId={chainId} /> )} diff --git a/src/pages/History/index.js b/src/pages/History/index.js index 395b4a72..398eaffd 100644 --- a/src/pages/History/index.js +++ b/src/pages/History/index.js @@ -8,13 +8,14 @@ import HistoryItem from 'components/HistoryItem'; import AccountSetUpButton from 'containers/AccountSetUpButton'; -import { ZkAccountContext } from 'contexts'; +import { PoolContext, ZkAccountContext } from 'contexts'; export default () => { const { history, zkAccount, zkAccountId, isLoadingZkAccount, isLoadingHistory, } = useContext(ZkAccountContext); + const { currentPool } = useContext(PoolContext); const pageSize = 5; const [currentPage, setCurrentPage] = useState(1); @@ -43,7 +44,7 @@ export default () => { {!isHistoryEmpty && ( <> {history.slice((currentPage - 1) * pageSize, currentPage * pageSize).map((item, index) => - + )} {history.length > pageSize && ( { const { zkAccount, isLoadingState, transferMulti, - estimateFee, verifyShieldedAddress, + estimateFee, verifyShieldedAddress, zkAccountId, } = useContext(ZkAccountContext); const [data, setData] = useState(''); const [parsedData, setParsedData] = useState([]); @@ -177,6 +177,7 @@ export default forwardRef((props, ref) => { isOpen={isDetailsModalOpen} onBack={closeDetailsModal} transfers={parsedData} + zkAccountId={zkAccountId} /> ); diff --git a/src/pages/Transfer/index.js b/src/pages/Transfer/index.js index 7cdfd226..61dc65a3 100644 --- a/src/pages/Transfer/index.js +++ b/src/pages/Transfer/index.js @@ -1,5 +1,6 @@ import React, { useContext, useState, useRef } from 'react'; import styled from 'styled-components'; +import { HistoryTransactionType } from 'zkbob-client-js'; import PendingAction from 'containers/PendingAction'; @@ -13,21 +14,22 @@ import { ReactComponent as InfoIconDefault } from 'assets/info.svg'; import SingleTransfer from './SingleTransfer'; import MultiTransfer from './MultiTransfer'; -import { ZkAccountContext } from 'contexts'; +import { ZkAccountContext, PoolContext } from 'contexts'; import { useLatestAction } from 'hooks'; - -import { HISTORY_ACTION_TYPES } from 'constants'; +import config from 'config'; const note = 'The transfer will be performed privately within the zero knowledge pool. Sender, recipient and amount are never disclosed.'; const tooltipText = 'Click Upload CSV to add a prepared .csv file from your machine. Each row should contain: zkAddress, amount'; export default () => { const { isPending } = useContext(ZkAccountContext); - const latestAction = useLatestAction(HISTORY_ACTION_TYPES.TRANSFER_OUT); + const latestAction = useLatestAction(HistoryTransactionType.TransferOut); const [isMulti, setIsMulti] = useState(false); const multitransferRef = useRef(null); const fileInputRef = useRef(null); + const { currentPool } = useContext(PoolContext); + const currentChainId = config.pools[currentPool].chainId; return isPending ? : ( <> @@ -65,6 +67,7 @@ export default () => { shielded={true} actions={latestAction.actions} txHash={latestAction.txHash} + currentChainId={currentChainId} /> )} diff --git a/src/pages/Withdraw/index.js b/src/pages/Withdraw/index.js index e0b70344..36ad247c 100644 --- a/src/pages/Withdraw/index.js +++ b/src/pages/Withdraw/index.js @@ -2,11 +2,12 @@ import React, { useState, useCallback, useContext } from 'react'; import styled from 'styled-components'; import { ethers } from 'ethers'; import { TxType } from 'zkbob-client-js'; +import { HistoryTransactionType } from 'zkbob-client-js'; import AccountSetUpButton from 'containers/AccountSetUpButton'; import PendingAction from 'containers/PendingAction'; -import { ZkAccountContext } from 'contexts'; +import { ZkAccountContext, PoolContext } from 'contexts'; import TransferInput from 'components/TransferInput'; import Card from 'components/Card'; @@ -25,8 +26,9 @@ import { useFee, useParsedAmount, useLatestAction } from 'hooks'; import { tokenSymbol } from 'utils/token'; import { formatNumber, minBigNumber } from 'utils'; +import config from 'config'; -import { NETWORKS, HISTORY_ACTION_TYPES } from 'constants'; +import { NETWORKS } from 'constants'; import { useMaxAmountExceeded } from './hooks'; const note = `${tokenSymbol()} will be withdrawn from the zero knowledge pool and deposited into the selected account.`; @@ -37,13 +39,15 @@ export default () => { isPending, maxTransferable, isDemo, limits, isLoadingLimits, minTxAmount, } = useContext(ZkAccountContext); + const { currentPool } = useContext(PoolContext); const [displayAmount, setDisplayAmount] = useState(''); const amount = useParsedAmount(displayAmount); const [receiver, setReceiver] = useState(''); const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); - const latestAction = useLatestAction(HISTORY_ACTION_TYPES.WITHDRAWAL); + const latestAction = useLatestAction(HistoryTransactionType.Withdrawal); const { fee, numberOfTxs, isLoadingFee } = useFee(amount, TxType.Withdraw); const maxAmountExceeded = useMaxAmountExceeded(amount, maxTransferable, limits.dailyWithdrawalLimit?.available); + const currentChainId = config.pools[currentPool].chainId; const onWihdrawal = useCallback(() => { setIsConfirmModalOpen(false); @@ -98,30 +102,32 @@ export default () => { isLoadingFee={isLoadingFee} /> {button} - - - Withdraw at least - - 10 BOB - - - and receive an additional 0.1 MATIC * - * only addresses with
a 0 MATIC balance receive additional MATIC} - placement="right" - delay={0} - width={180} - > - -
-
-
+ {currentChainId === 137 && ( // only polygon + + + Withdraw at least + + 10 BOB + + + and receive an additional 0.1 MATIC * + * only addresses with
a 0 MATIC balance receive additional MATIC} + placement="right" + delay={0} + width={180} + > + +
+
+
+ )} { shielded={true} actions={latestAction.actions} txHash={latestAction.txHash} + currentChainId={currentChainId} /> )} diff --git a/src/pages/index.js b/src/pages/index.js index f4239840..04fde1b8 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -14,10 +14,10 @@ import AccountSetUpModal from 'containers/AccountSetUpModal'; import PasswordModal from 'containers/PasswordModal'; import TermsModal from 'containers/TermsModal'; import SwapModal from 'containers/SwapModal'; -import SwapOptionsModal from 'containers/SwapOptionsModal'; import ConfirmLogoutModal from 'containers/ConfirmLogoutModal'; import SeedPhraseModal from 'containers/SeedPhraseModal'; import IncreasedLimitsModal from 'containers/IncreasedLimitsModal'; +import RedeemGiftCardModal from 'containers/RedeemGiftCardModal'; import ChangePasswordModal from 'components/ChangePasswordModal'; import ToastContainer from 'components/ToastContainer'; @@ -135,12 +135,12 @@ const Content = () => { + - diff --git a/src/providers/Web3Provider.js b/src/providers/Web3Provider.js index 61517987..ce7561df 100644 --- a/src/providers/Web3Provider.js +++ b/src/providers/Web3Provider.js @@ -1,18 +1,12 @@ import { WagmiConfig, configureChains, createClient } from 'wagmi'; import { publicProvider } from 'wagmi/providers/public'; -import { sepolia, polygon, goerli } from 'wagmi/chains'; +import { sepolia, polygon, goerli, optimism, optimismGoerli } from 'wagmi/chains'; import { InjectedConnector } from 'wagmi/connectors/injected'; -import { WalletConnectConnector } from 'wagmi/connectors/walletConnect'; +// import { WalletConnectConnector } from 'wagmi/connectors/walletConnect'; import { WalletConnectLegacyConnector } from 'wagmi/connectors/walletConnectLegacy'; -const chainsMap = { - '137': polygon, - '11155111': sepolia, - '5': goerli, -}; - const { chains, provider, webSocketProvider } = configureChains( - [chainsMap[process.env.REACT_APP_NETWORK]], + [sepolia, polygon, goerli, optimism, optimismGoerli], [publicProvider()], ); @@ -28,21 +22,21 @@ const walletConnectV1 = new WalletConnectLegacyConnector({ qrcode: true, }, }); -const walletConnectV2 = new WalletConnectConnector({ - chains, - options: { - qrcode: true, - projectId: process.env.REACT_APP_WALLETCONNECT_PROJECT_ID, - name: 'zkBob', - relayUrl: 'wss://relay.walletconnect.org' - }, -}); +// const walletConnectV2 = new WalletConnectConnector({ +// chains, +// options: { +// qrcode: true, +// projectId: process.env.REACT_APP_WALLETCONNECT_PROJECT_ID, +// name: 'zkBob', +// relayUrl: 'wss://relay.walletconnect.org' +// }, +// }); const client = createClient({ autoConnect: true, provider, webSocketProvider, - connectors: [injected, walletConnectV1, walletConnectV2], + connectors: [injected, walletConnectV1, /*walletConnectV2*/], }); export default ({ children }) => ( diff --git a/src/utils/index.js b/src/utils/index.js index f1d7ef54..f9b66326 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,4 +1,5 @@ import { ethers } from 'ethers'; +import { toast } from 'react-toastify'; const { parseEther, formatEther, commify } = ethers.utils; @@ -25,3 +26,12 @@ export const minBigNumber = (...numbers) => export const maxBigNumber = (...numbers) => numbers.reduce((p, v) => (p.gt(v) ? p : v)); + +export const showLoadingError = cause => { + toast.error( + + Error loading {cause}.
+ Please try again later or contact our support if the issue persists. +
+ ); +}; diff --git a/yarn.lock b/yarn.lock index ae006827..cb590a33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5385,6 +5385,11 @@ base-x@^3.0.2, base-x@^3.0.8: dependencies: safe-buffer "^5.0.1" +base-x@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" + integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -5690,6 +5695,13 @@ browserslist@^4.21.4: node-releases "^2.0.6" update-browserslist-db "^1.0.9" +bs58@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" + integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== + dependencies: + base-x "^4.0.0" + bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" @@ -10649,15 +10661,15 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libzkbob-rs-wasm-web-mt@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web-mt/-/libzkbob-rs-wasm-web-mt-1.0.0.tgz#243d785cbc433d3da4e6f6e7fb01dce1ab27b857" - integrity sha512-4jNpN18ViLG9DDkHrUax9Py3CEkJDl1tcJ3V2OFxvfnYV9AMjyxS9p7YQkQuKRyIbyiXaofmmpugfYr8MxxRpw== +libzkbob-rs-wasm-web-mt@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web-mt/-/libzkbob-rs-wasm-web-mt-1.2.0.tgz#4058dfc416bca9e91e293fc3cb172eba41b0f217" + integrity sha512-d6TElAdXjNSdYtOlWNzzjOgtyN7YF1YUHmCfmSUnOR8VXtv3AzSRuYNjGC5PKivXqWCP8KCXI8qPpsCO535mpw== -libzkbob-rs-wasm-web@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web/-/libzkbob-rs-wasm-web-1.0.0.tgz#74eba1caa2bfc728be8fd1f3161e5089360f2a72" - integrity sha512-3xW0BjAZ9NDoNLCwlGHNjg5q1GN7fEpXCVf7+emQxQrt/vey9PNTAQnNIEjWZ/M0Fn52t06Og2P6zgqcGUCz7Q== +libzkbob-rs-wasm-web@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web/-/libzkbob-rs-wasm-web-1.2.0.tgz#bb2f82877ca9f8a14c33f69d33e3ab38e6b1900f" + integrity sha512-yTKp+dV/XXhnKQSpsC2bDZrRSXHqSRPX1jRmAUP0huI2fwFToARMz75lK/nF3ZipieU4ge5JSuV2e8/ADRrQbw== lie@3.1.1: version "3.1.1" @@ -16477,21 +16489,22 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zkbob-client-js@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-2.1.0.tgz#26d19a9a00ef5a2468798ae9eed00a062894ab33" - integrity sha512-BOjXrQF40ZjirEqHZqIIzBq5WJcxKGEcoGTLddIyNG5TWO8BEG9nqrtzLkTYuuTDreoetlnyiB34nKRtQ7346w== +zkbob-client-js@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-3.2.1.tgz#34eee71eadf9592ced8779f3f471e8cf08517e5a" + integrity sha512-zPc2N5g1Hf5MpZj/eS72cwLq0266HQxGbrOJA0Dxilt/tASQlUErJghljWdKVQqymDdbXdtluGfrbiyKPXJJrg== dependencies: "@ethereumjs/util" "^8.0.2" "@metamask/eth-sig-util" "5.0.0" "@scure/bip32" "1.1.1" "@scure/bip39" "1.1.0" + bs58 "5.0.0" comlink "^4.3.1" fast-sha256 "^1.3.0" hdwallet-babyjub "^0.0.2" idb "^7.0.0" - libzkbob-rs-wasm-web "1.0.0" - libzkbob-rs-wasm-web-mt "1.0.0" + libzkbob-rs-wasm-web "1.2.0" + libzkbob-rs-wasm-web-mt "1.2.0" regenerator-runtime "^0.13.9" wasm-feature-detect "^1.2.11" web3 "1.8.0"