From 0e20eae02c4c5cabfba16e244a7d73a13e46ff00 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 28 Sep 2023 18:29:32 +0200 Subject: [PATCH 01/45] move all wallet logic to a context --- src/components/AccountSetUpModal/index.js | 11 +++-- src/components/Header/index.js | 4 +- src/components/WalletConnectors/index.js | 18 ++++--- src/components/WalletDropdown/index.js | 6 +-- src/containers/IncreasedLimitsModal/index.js | 8 +-- src/contexts/IncreasedLimitsContext/index.js | 5 +- src/contexts/TokenBalanceContext/index.js | 22 +++------ src/contexts/WalletContext/index.js | 52 ++++++++++++++++++++ src/contexts/ZkAccountContext/index.js | 15 ++---- src/contexts/index.js | 27 +++++----- src/hooks/useApproval.js | 16 ++---- src/pages/Deposit/index.js | 8 +-- src/pages/Payment/Header/index.js | 4 +- src/pages/Payment/PseudoInput/index.js | 2 +- src/pages/Payment/hooks.js | 20 +++----- src/pages/Payment/index.js | 47 +++++++++++------- src/pages/Withdraw/hooks.js | 15 +++--- 17 files changed, 156 insertions(+), 124 deletions(-) create mode 100644 src/contexts/WalletContext/index.js diff --git a/src/components/AccountSetUpModal/index.js b/src/components/AccountSetUpModal/index.js index a1aa65a6..1c762a2d 100644 --- a/src/components/AccountSetUpModal/index.js +++ b/src/components/AccountSetUpModal/index.js @@ -1,7 +1,6 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useContext } from 'react'; import styled from 'styled-components'; import { ethers } from 'ethers'; -import { useSignMessage } from 'wagmi'; import md5 from 'js-md5'; import { useTranslation, Trans } from 'react-i18next'; @@ -9,6 +8,8 @@ import Modal from 'components/Modal'; import Button from 'components/Button'; import Link from 'components/Link'; +import { WalletContext } from 'contexts'; + import Create from './Create'; import Confirm from './Confirm'; import Restore from './Restore'; @@ -101,7 +102,7 @@ const PasswordPrompt = ({ setStep, close }) => { export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) => { const { t } = useTranslation(); - const { signMessageAsync } = useSignMessage(); + const { signMessage } = useContext(WalletContext); const [step, setStep] = useState(STEP.START); const [newMnemonic, setNewMnemonic] = useState(); const [confirmedMnemonic, setConfirmedMnemonic] = useState(); @@ -133,7 +134,7 @@ export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) const generate = useCallback(async () => { const message = 'Access zkBob account.\n\nOnly sign this message for a trusted client!'; - let signedMessage = await signMessageAsync({ message }); + let signedMessage = await signMessage({ message }); if (!window.location.host.includes(process.env.REACT_APP_LEGACY_SIGNATURE_DOMAIN)) { // Metamask with ledger returns V=0/1 here too, we need to adjust it to be ethereum's valid value (27 or 28) const MIN_VALID_V_VALUE = 27; @@ -146,7 +147,7 @@ export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) const newMnemonic = ethers.utils.entropyToMnemonic(md5.array(signedMessage)); setConfirmedMnemonic(newMnemonic); setStep(STEP.CREATE_PASSWORD_PROMPT); - }, [signMessageAsync]); + }, [signMessage]); const confirmPassword = useCallback(password => { const isNewAccount = !!newMnemonic; diff --git a/src/components/Header/index.js b/src/components/Header/index.js index 0158061b..56342f75 100644 --- a/src/components/Header/index.js +++ b/src/components/Header/index.js @@ -1,7 +1,6 @@ import React, { useContext, useCallback } from 'react'; import styled from 'styled-components'; import { ethers } from 'ethers'; -import { useAccount } from 'wagmi'; import { useTranslation } from 'react-i18next'; import ButtonDefault from 'components/Button'; @@ -25,6 +24,7 @@ import { useWindowDimensions } from 'hooks'; import { ZkAccountContext, ModalContext, TokenBalanceContext, PoolContext, + WalletContext, } from 'contexts'; const { parseUnits } = ethers.utils; @@ -43,7 +43,7 @@ const BalanceSkeleton = isMobile => ( export default ({ empty }) => { const { t } = useTranslation(); - const { address: account, connector } = useAccount(); + const { address: account, connector } = useContext(WalletContext); const { balance, nativeBalance, updateBalance, isLoadingBalance } = useContext(TokenBalanceContext); const { zkAccount, isLoadingZkAccount, balance: poolBalance, diff --git a/src/components/WalletConnectors/index.js b/src/components/WalletConnectors/index.js index d739e62e..72c5c831 100644 --- a/src/components/WalletConnectors/index.js +++ b/src/components/WalletConnectors/index.js @@ -1,6 +1,7 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useContext } from 'react'; import styled from 'styled-components'; -import { useAccount, useConnect, useDisconnect } from 'wagmi'; + +import { WalletContext } from 'contexts'; import { CONNECTORS_ICONS } from 'constants'; @@ -11,17 +12,18 @@ const getConnectorName = connector => { } export default ({ callback, gaIdPrefix = '' }) => { - const { connector: activeConnector } = useAccount(); - const { connectAsync, connectors } = useConnect(); - const { disconnectAsync } = useDisconnect(); + const { + connector: activeConnector, connect, + disconnect, connectors, + } = useContext(WalletContext); const connectWallet = useCallback(async connector => { if (connector.id === activeConnector?.id) { - await disconnectAsync(); + await disconnect(); } - await connectAsync({ connector }); + await connect({ connector }); callback?.(); - }, [connectAsync, disconnectAsync, activeConnector, callback]); + }, [connect, disconnect, activeConnector, callback]); return ( <> diff --git a/src/components/WalletDropdown/index.js b/src/components/WalletDropdown/index.js index 2b3c7911..68f105d8 100644 --- a/src/components/WalletDropdown/index.js +++ b/src/components/WalletDropdown/index.js @@ -1,7 +1,6 @@ import { useState, useCallback, useContext } from 'react'; import styled from 'styled-components'; import { CopyToClipboard } from 'react-copy-to-clipboard'; -import { useAccount, useDisconnect } from 'wagmi'; import { useTranslation } from 'react-i18next'; import Dropdown from 'components/Dropdown'; @@ -16,7 +15,7 @@ import { formatNumber } from 'utils'; import { CONNECTORS_ICONS, NETWORKS, TOKENS_ICONS } from 'constants'; -import { ModalContext, TokenBalanceContext, PoolContext } from 'contexts'; +import { ModalContext, TokenBalanceContext, PoolContext, WalletContext } from 'contexts'; const Balance = ({ tokenSymbol, balance, isWrapped, isNative, tokenDecimals }) => ( @@ -111,8 +110,7 @@ const Content = ({ }; export default ({ children }) => { - const { address, connector } = useAccount(); - const { disconnect } = useDisconnect(); + const { address, connector, disconnect } = useContext(WalletContext); const { balance, nativeBalance, isLoadingBalance } = useContext(TokenBalanceContext); const { openWalletModal, isWalletDropdownOpen, diff --git a/src/containers/IncreasedLimitsModal/index.js b/src/containers/IncreasedLimitsModal/index.js index d5162786..cf3c531f 100644 --- a/src/containers/IncreasedLimitsModal/index.js +++ b/src/containers/IncreasedLimitsModal/index.js @@ -1,14 +1,16 @@ import { useContext, useEffect } from 'react'; -import { useAccount } from 'wagmi' import IncreasedLimitsModal from 'components/IncreasedLimitsModal'; -import { ModalContext, IncreasedLimitsContext, ZkAccountContext, PoolContext } from 'contexts'; +import { + ModalContext, IncreasedLimitsContext, + ZkAccountContext, PoolContext, WalletContext, +} from 'contexts'; import { INCREASED_LIMITS_STATUSES } from 'constants'; export default () => { - const { address: account } = useAccount(); + const { adrress: account } = useContext(WalletContext); const { isIncreasedLimitsModalOpen, closeIncreasedLimitsModal, isWalletModalOpen, openWalletModal, diff --git a/src/contexts/IncreasedLimitsContext/index.js b/src/contexts/IncreasedLimitsContext/index.js index 17317da5..02f3f76f 100644 --- a/src/contexts/IncreasedLimitsContext/index.js +++ b/src/contexts/IncreasedLimitsContext/index.js @@ -1,9 +1,8 @@ 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 { PoolContext, WalletContext } from 'contexts'; const IncreasedLimitsContext = createContext({}); @@ -11,7 +10,7 @@ export default IncreasedLimitsContext; export const IncreasedLimitsContextProvider = ({ children }) => { const { currentPool } = useContext(PoolContext); - const { address } = useAccount(); + const { address } = useContext(WalletContext); const [status, setStatus] = useState(null); const updateStatus = useCallback(async () => { diff --git a/src/contexts/TokenBalanceContext/index.js b/src/contexts/TokenBalanceContext/index.js index 67e84131..cfa722d1 100644 --- a/src/contexts/TokenBalanceContext/index.js +++ b/src/contexts/TokenBalanceContext/index.js @@ -1,9 +1,8 @@ import React, { createContext, useState, useEffect, useCallback, useContext } from 'react'; import { ethers } from 'ethers'; -import { useContract, useAccount, useProvider, useBalance } from 'wagmi'; import * as Sentry from '@sentry/react'; -import { PoolContext } from 'contexts'; +import { PoolContext, WalletContext } from 'contexts'; import { showLoadingError } from 'utils'; @@ -14,18 +13,8 @@ const TokenBalanceContext = createContext({ balance: null }); export default TokenBalanceContext; export const TokenBalanceContextProvider = ({ children }) => { - const { address: account } = useAccount(); + const { address: account, provider, getBalance } = useContext(WalletContext); const { currentPool } = useContext(PoolContext); - const provider = useProvider({ chainId: currentPool.chainId }); - const token = useContract({ - address: currentPool.tokenAddress, - abi: TOKEN_ABI, - signerOrProvider: provider - }); - const { refetch: getNativeBalance } = useBalance({ - address: account, - chainId: currentPool.chainId, - }); const [balance, setBalance] = useState(ethers.constants.Zero); const [nativeBalance, setNativeBalance] = useState(ethers.constants.Zero); const [isLoadingBalance, setIsLoadingBalance] = useState(false); @@ -34,11 +23,12 @@ export const TokenBalanceContextProvider = ({ children }) => { setIsLoadingBalance(true); let balance = ethers.constants.Zero; let nativeBalance = ethers.constants.Zero; - if (account && token) { + if (account) { try { + const token = new ethers.Contract(currentPool.tokenAddress, TOKEN_ABI, provider); [balance, nativeBalance] = await Promise.all([ token.balanceOf(account), - getNativeBalance().then(({ data: { value } }) => value), + getBalance().then(({ data: { value } }) => value), ]); } catch (error) { console.error(error); @@ -49,7 +39,7 @@ export const TokenBalanceContextProvider = ({ children }) => { setBalance(balance); setNativeBalance(nativeBalance); setIsLoadingBalance(false); - }, [token, account, getNativeBalance]); + }, [account, getBalance, provider, currentPool.tokenAddress]); useEffect(() => { updateBalance(); diff --git a/src/contexts/WalletContext/index.js b/src/contexts/WalletContext/index.js new file mode 100644 index 00000000..894f9238 --- /dev/null +++ b/src/contexts/WalletContext/index.js @@ -0,0 +1,52 @@ +import { createContext, useContext } from 'react'; +import { + useAccount, useSignMessage, useConnect, useDisconnect, + useBalance, useProvider, useSigner, useNetwork, + useSwitchNetwork, +} from 'wagmi'; + +import PoolContext from 'contexts/PoolContext'; + +const WalletContext = createContext({}); + +export default WalletContext; + +const useEvmWallet = () => { + const { currentPool: pool } = useContext(PoolContext); + const { address, connector } = useAccount(); + const provider = useProvider({ chainId: pool.chainId }); + const { signMessageAsync } = useSignMessage(); + const { connectAsync, connectors } = useConnect(); + const { disconnectAsync } = useDisconnect(); + const { refetch: getBalance } = useBalance({ address, chainId: pool.chainId }); + const { data: signer } = useSigner({ chainId: pool.chainId }); + const { chain } = useNetwork(); + const { switchNetworkAsync } = useSwitchNetwork({ + chainId: pool.chainId, + throwForSwitchChainNotSupported: true, + }); + + return { + address, + chain, + provider, + signer, + connector, + connectors, + connect: connectAsync, + disconnect: disconnectAsync, + signMessage: signMessageAsync, + switchNetwork: switchNetworkAsync, + getBalance, + }; +}; + + +export const WalletContextProvider = ({ children }) => { + const wallet = useEvmWallet(); + return ( + + {children} + + ); +}; diff --git a/src/contexts/ZkAccountContext/index.js b/src/contexts/ZkAccountContext/index.js index fe48600e..d4ae09dc 100644 --- a/src/contexts/ZkAccountContext/index.js +++ b/src/contexts/ZkAccountContext/index.js @@ -1,6 +1,5 @@ import React, { createContext, useState, useEffect, useCallback, useContext } from 'react'; import { ethers, BigNumber } from 'ethers'; -import { useAccount, useSigner, useNetwork, useSwitchNetwork } from 'wagmi'; import AES from 'crypto-js/aes'; import Utf8 from 'crypto-js/enc-utf8'; import * as Sentry from "@sentry/react"; @@ -14,7 +13,7 @@ import { Trans } from 'react-i18next'; import { TransactionModalContext, ModalContext, PoolContext, - TokenBalanceContext, SupportIdContext, + TokenBalanceContext, SupportIdContext, WalletContext, } from 'contexts'; import { TX_STATUSES } from 'constants'; @@ -42,13 +41,7 @@ export default ZkAccountContext; export const ZkAccountContextProvider = ({ children }) => { const { currentPool, setCurrentPool } = useContext(PoolContext); const previousPoolAlias = usePrevious(currentPool.alias); - const { address: account } = useAccount(); - const { chain } = useNetwork(); - const { data: signer } = useSigner({ chainId: currentPool.chainId }); - const { switchNetworkAsync } = useSwitchNetwork({ - chainId: currentPool.chainId, - throwForSwitchChainNotSupported: true, - }); + const { address: account, chain, signer, switchNetwork } = useContext(WalletContext); const { openTxModal, setTxStatus, setTxAmount, setTxError } = useContext(TransactionModalContext); const { openPasswordModal, closePasswordModal, closeAllModals } = useContext(ModalContext); const { updateBalance: updateTokenBalance } = useContext(TokenBalanceContext); @@ -317,7 +310,7 @@ export const ZkAccountContextProvider = ({ children }) => { if (chain.id !== currentPool.chainId) { setTxStatus(TX_STATUSES.SWITCH_NETWORK); try { - await switchNetworkAsync(); + await switchNetwork(); } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.deposit.switchNetwork' } }); @@ -354,7 +347,7 @@ export const ZkAccountContextProvider = ({ children }) => { }, [ zkClient, updatePoolData, signer, openTxModal, setTxAmount, setTxStatus, updateTokenBalance, toShieldedAmount, setTxError, - chain, switchNetworkAsync, currentPool, + chain, switchNetwork, currentPool, ]); const transfer = useCallback(async (to, amount, relayerFee) => { diff --git a/src/contexts/index.js b/src/contexts/index.js index 4ad77382..1c931f1d 100644 --- a/src/contexts/index.js +++ b/src/contexts/index.js @@ -9,23 +9,26 @@ import IncreasedLimitsContext, { IncreasedLimitsContextProvider } from 'contexts import PoolContext, { PoolContextProvider } from 'contexts/PoolContext'; import TokenPriceContext, { TokenPriceContextProvider } from 'contexts/TokenPriceContext'; import LanguageContext, { LanguageContextProvider } from 'contexts/LanguageContext'; +import WalletContext, { WalletContextProvider } from 'contexts/WalletContext'; const ContextsProvider = ({ children }) => ( - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + @@ -36,5 +39,5 @@ export default ContextsProvider; export { ZkAccountContext, TokenBalanceContext, TransactionModalContext, ModalContext, SupportIdContext, IncreasedLimitsContext, PoolContext, - TokenPriceContext, LanguageContext, + TokenPriceContext, LanguageContext, WalletContext, }; diff --git a/src/hooks/useApproval.js b/src/hooks/useApproval.js index 2b5a3641..edea9d4c 100644 --- a/src/hooks/useApproval.js +++ b/src/hooks/useApproval.js @@ -1,9 +1,8 @@ import { useState, useEffect, useContext, useCallback } from 'react'; import { ethers } from 'ethers'; import * as Sentry from '@sentry/react'; -import { useAccount, useSigner, useNetwork, useSwitchNetwork, useProvider } from 'wagmi'; -import { TransactionModalContext } from 'contexts'; +import { TransactionModalContext, WalletContext } from 'contexts'; import { TX_STATUSES, PERMIT2_CONTRACT_ADDRESS } from 'constants'; import { useMemo } from 'react'; @@ -16,14 +15,7 @@ const TOKEN_ABI = [ export default (chainId, tokenAddress, amount, balance) => { const { openTxModal, closeTxModal, setTxStatus, setTxError } = useContext(TransactionModalContext); - const { address: account } = useAccount(); - const { chain } = useNetwork(); - const { data: signer } = useSigner({ chainId }); - const provider = useProvider({ chainId }); - const { switchNetworkAsync } = useSwitchNetwork({ - chainId, - throwForSwitchChainNotSupported: true, - }); + const { address: account, provider, signer, chain, switchNetwork } = useContext(WalletContext); const [allowance, setAllowance] = useState(ethers.constants.Zero); const isApproved = useMemo(() => allowance.gte(amount), [allowance, amount]); @@ -49,7 +41,7 @@ export default (chainId, tokenAddress, amount, balance) => { if (chain.id !== chainId) { setTxStatus(TX_STATUSES.SWITCH_NETWORK); try { - await switchNetworkAsync(); + await switchNetwork(); } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'hooks.useApproval.approve.switchNetwork' } }); @@ -74,7 +66,7 @@ export default (chainId, tokenAddress, amount, balance) => { setTxStatus(TX_STATUSES.REJECTED); } }, [ - openTxModal, setTxStatus, setTxError, switchNetworkAsync, chain, + openTxModal, setTxStatus, setTxError, switchNetwork, chain, signer, updateAllowance, chainId, tokenAddress, closeTxModal, ]); diff --git a/src/pages/Deposit/index.js b/src/pages/Deposit/index.js index f39ee1cb..5f14ddce 100644 --- a/src/pages/Deposit/index.js +++ b/src/pages/Deposit/index.js @@ -1,5 +1,4 @@ import React, { useState, useContext, useCallback, useMemo } from 'react'; -import { useAccount } from 'wagmi' import { TxType } from 'zkbob-client-js'; import { ethers } from 'ethers'; import * as Sentry from '@sentry/react'; @@ -10,7 +9,10 @@ import { useTranslation, Trans } from 'react-i18next'; import AccountSetUpButton from 'containers/AccountSetUpButton'; import PendingAction from 'containers/PendingAction'; -import { ZkAccountContext, TokenBalanceContext, ModalContext, IncreasedLimitsContext, PoolContext } from 'contexts'; +import { + ZkAccountContext, TokenBalanceContext, ModalContext, + IncreasedLimitsContext, PoolContext, WalletContext, +} from 'contexts'; import TransferInput from 'components/TransferInput'; import Card from 'components/Card'; @@ -31,7 +33,7 @@ import { formatNumber, minBigNumber } from 'utils'; export default () => { const { t } = useTranslation(); - const { address: account } = useAccount(); + const { address: account } = useContext(WalletContext); const { zkAccount, isLoadingZkAccount, deposit, isLoadingState, isPending, isDemo, diff --git a/src/pages/Payment/Header/index.js b/src/pages/Payment/Header/index.js index ee9f5a29..9077a9b0 100644 --- a/src/pages/Payment/Header/index.js +++ b/src/pages/Payment/Header/index.js @@ -1,6 +1,5 @@ import React, { useContext } from 'react'; import styled from 'styled-components'; -import { useAccount } from 'wagmi'; import { useTranslation } from 'react-i18next'; import Button from 'components/Button'; @@ -14,10 +13,11 @@ import { shortAddress } from 'utils'; import { CONNECTORS_ICONS, NETWORKS } from 'constants'; import ModalContext from 'contexts/ModalContext'; +import WalletContext from 'contexts/WalletContext'; export default ({ pool }) => { const { t } = useTranslation(); - const { address: account, connector } = useAccount(); + const { address: account, connector } = useContext(WalletContext); const { openWalletModal } = useContext(ModalContext); return (
diff --git a/src/pages/Payment/PseudoInput/index.js b/src/pages/Payment/PseudoInput/index.js index c298c705..90b2f82f 100644 --- a/src/pages/Payment/PseudoInput/index.js +++ b/src/pages/Payment/PseudoInput/index.js @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { ethers } from 'ethers'; import { useTranslation } from 'react-i18next'; diff --git a/src/pages/Payment/hooks.js b/src/pages/Payment/hooks.js index 4a939270..83b189e1 100644 --- a/src/pages/Payment/hooks.js +++ b/src/pages/Payment/hooks.js @@ -1,13 +1,13 @@ import { useEffect, useState, useContext, useCallback } from 'react'; import * as Sentry from '@sentry/react'; import { ethers, BigNumber } from 'ethers'; -import { useAccount, useSigner, useNetwork, useSwitchNetwork, useProvider } from 'wagmi'; import { LiFi } from '@lifi/sdk'; import { Multicall } from 'ethereum-multicall'; import { useTranslation } from 'react-i18next'; import SupportIdContext from 'contexts/SupportIdContext'; import TransactionModalContext from 'contexts/TransactionModalContext'; +import WalletContext from 'contexts/WalletContext'; import zp from 'contexts/ZkAccountContext/zp'; @@ -27,9 +27,8 @@ const MAX_DIFF = BigNumber.from('10000'); // 1.0% const TOKEN_ABI = ['function balanceOf(address) pure returns (uint256)']; -export function useTokensBalances(tokenList, chainId) { - const { address: account } = useAccount(); - const provider = useProvider({ chainId }); +export function useTokensBalances(tokenList) { + const { address: account, provider } = useContext(WalletContext); const [balances, setBalances] = useState({}); const [isLoadingBalances, setIsLoadingBalances] = useState(true); @@ -270,14 +269,7 @@ export function useLimitsAndFees(pool) { export function usePayment(token, tokenAmount, amount, fee, pool, zkAddress, liFiRoute) { const { openTxModal, setTxStatus, setTxHash, setTxError, setCsvLink } = useContext(TransactionModalContext); const { t } = useTranslation(); - const { address: account } = useAccount(); - const { chain } = useNetwork(); - const { data: signer } = useSigner({ chainId: pool.chainId }); - const provider = useProvider({ chainId: pool.chainId }); - const { switchNetworkAsync } = useSwitchNetwork({ - chainId: pool.chainId, - throwForSwitchChainNotSupported: true, - }); + const { address: account, chain, provider, signer, switchNetwork } = useContext(WalletContext); const send = useCallback(async () => { openTxModal(); @@ -285,7 +277,7 @@ export function usePayment(token, tokenAmount, amount, fee, pool, zkAddress, liF if (chain.id !== pool.chainId) { setTxStatus(TX_STATUSES.SWITCH_NETWORK); try { - await switchNetworkAsync(); + await switchNetwork(); } catch (error) { console.error(error); Sentry.captureException(error, { tags: { method: 'ZkAccountContext.deposit.switchNetwork' } }); @@ -394,7 +386,7 @@ export function usePayment(token, tokenAmount, amount, fee, pool, zkAddress, liF } }, [ chain, pool, token, tokenAmount, account, provider, signer, - openTxModal, setTxStatus, setTxError, switchNetworkAsync, + openTxModal, setTxStatus, setTxError, switchNetwork, zkAddress, fee, amount, setTxHash, liFiRoute, t, setCsvLink, ]); diff --git a/src/pages/Payment/index.js b/src/pages/Payment/index.js index ebf59605..fcab51db 100644 --- a/src/pages/Payment/index.js +++ b/src/pages/Payment/index.js @@ -1,7 +1,6 @@ import React, { useState, useContext, useEffect, useMemo } from 'react'; import styled from 'styled-components'; import { useHistory, useParams } from 'react-router-dom'; -import { useAccount } from 'wagmi'; import { ethers } from 'ethers'; import { useTranslation, Trans } from 'react-i18next'; @@ -26,6 +25,8 @@ import ModalContext, { ModalContextProvider } from 'contexts/ModalContext'; import SupportIdContext, { SupportIdContextProvider } from 'contexts/SupportIdContext'; import TransactionModalContext, { TransactionModalContextProvider } from 'contexts/TransactionModalContext'; import { LanguageContextProvider } from 'contexts/LanguageContext'; +import WalletContext, { WalletContextProvider } from 'contexts/WalletContext'; +import PoolContext from 'contexts/PoolContext'; import config from 'config'; @@ -38,19 +39,14 @@ const pools = Object.values(config.pools).map((pool, index) => ({ ...pool, alias: Object.keys(config.pools)[index] }) ); -const Payment = () => { +const Payment = ({ pool }) => { const { t } = useTranslation(); const { supportId } = useContext(SupportIdContext); const history = useHistory(); const params = useParams(); - const addressPrefix = params.address.split(':')[0]; - const pool = Object.values(pools).find(pool => pool.addressPrefix === addressPrefix); - if (!pool.paymentContractAddress) { - history.push('/'); - } const currency = ['USDC', 'BOB'].includes(pool.tokenSymbol) ? 'USD' : pool.tokenSymbol; - const { address: account } = useAccount(); + const { address: account } = useContext(WalletContext); const [displayedAmount, setDisplayedAmount] = useState(''); const amount = useMemo(() => ethers.utils.parseUnits(displayedAmount || '0', pool?.tokenDecimals), [displayedAmount, pool]); const [selectedToken, setSelectedToken] = useState(null); @@ -172,17 +168,30 @@ const Payment = () => { ); } -export default () => ( - - - - - - - - - -); +export default () => { + const history = useHistory(); + const params = useParams(); + const addressPrefix = params.address.split(':')[0]; + const pool = Object.values(pools).find(pool => pool.addressPrefix === addressPrefix); + if (!pool.paymentContractAddress) { + history.push('/'); + } + return ( + + + + + + + + + + + + + + ); +}; const Row = styled.div` display: flex; diff --git a/src/pages/Withdraw/hooks.js b/src/pages/Withdraw/hooks.js index e39cd4c5..16e5dddc 100644 --- a/src/pages/Withdraw/hooks.js +++ b/src/pages/Withdraw/hooks.js @@ -1,7 +1,8 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useContext } from 'react'; import * as Sentry from '@sentry/react'; import { ethers } from 'ethers'; -import { useContract, useProvider } from 'wagmi'; + +import WalletContext from 'contexts/WalletContext'; export const useMaxAmountExceeded = (amount, maxWithdrawable, limit = ethers.constants.Zero) => { const [maxAmountExceeded, setMaxAmountExceeded] = useState(false); @@ -28,12 +29,7 @@ const POOL_CONTRACT_ABI = ['function tokenSeller() pure returns (address)']; const SWAP_CONTRACT_ABI = ['function quoteSellForETH(uint256) pure returns (uint256)']; export const useConvertion = (currentPool) => { - const provider = useProvider({ chainId: currentPool.chainId }); - const poolContract = useContract({ - address: currentPool.poolAddress, - abi: POOL_CONTRACT_ABI, - signerOrProvider: provider - }); + const { provider } = useContext(WalletContext); const [price, setPrice] = useState(ethers.constants.Zero); const [exist, setExist] = useState(false); @@ -41,6 +37,7 @@ export const useConvertion = (currentPool) => { async function getPrice() { try { let swapContractAddress = ethers.constants.AddressZero; + const poolContract = new ethers.Contract(currentPool.poolAddress, POOL_CONTRACT_ABI, provider); try { swapContractAddress = await poolContract.tokenSeller(); } catch (error) {} @@ -60,7 +57,7 @@ export const useConvertion = (currentPool) => { getPrice(); const interval = setInterval(getPrice, 1000 * 60 * 5); return () => clearInterval(interval); - }, [poolContract, provider, currentPool]); + }, [provider, currentPool]); return { exist, price, decimals: 18, toTokenSymbol: NATIVE_TOKENS[currentPool.alias] }; }; From 07209d6770e1dd2740705508bc361c794cdc186c Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 9 Oct 2023 19:36:40 +0200 Subject: [PATCH 02/45] integrate tron --- package.json | 6 +- public/locales/en/translation.json | 4 +- public/locales/pt/translation.json | 4 +- src/App.js | 5 +- src/abis/token.json | 101 +++ src/assets/tron.png | Bin 0 -> 60790 bytes src/assets/tronlink.png | Bin 0 -> 22407 bytes src/assets/usdt.png | Bin 0 -> 8157 bytes .../AccountSetUpModal/Generate/index.js | 42 +- src/components/AccountSetUpModal/index.js | 2 +- src/components/Header/index.js | 10 +- src/components/WalletConnectors/index.js | 6 +- src/components/ZkAccountDropdown/index.js | 18 +- src/config/index.js | 18 + src/constants/index.js | 10 + src/contexts/TokenBalanceContext/index.js | 12 +- src/contexts/WalletContext/index.js | 137 +++- src/contexts/ZkAccountContext/index.js | 13 +- src/contexts/ZkAccountContext/zp.js | 16 +- src/hooks/useApproval.js | 40 +- src/pages/Deposit/index.js | 4 +- src/pages/Payment/index.js | 2 +- src/pages/Payment/utils.js | 7 +- src/pages/Withdraw/hooks.js | 13 +- src/pages/Withdraw/index.js | 5 +- yarn.lock | 590 +++++++++++++++++- 26 files changed, 965 insertions(+), 100 deletions(-) create mode 100644 src/abis/token.json create mode 100644 src/assets/tron.png create mode 100644 src/assets/tronlink.png create mode 100644 src/assets/usdt.png diff --git a/package.json b/package.json index ce98c749..44de1116 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "@tronweb3/tronwallet-abstract-adapter": "^1.1.5", + "@tronweb3/tronwallet-adapter-react-ui": "^1.1.7", + "@tronweb3/tronwallet-adapters": "^1.1.8", "codemirror": "5.65.9", "crypto-browserify": "^3.12.0", "crypto-js": "^4.1.1", @@ -45,12 +48,13 @@ "react-toastify": "^8.2.0", "stream-browserify": "^3.0.0", "styled-components": "^5.3.0", + "tronweb": "^5.3.0", "unique-names-generator": "^4.7.1", "uuid": "^9.0.0", "wagmi": "^0.12.1", "web-vitals": "^1.0.1", "webpack": "^5.70.0", - "zkbob-client-js": "5.3.0-beta6" + "zkbob-client-js": "5.3.0-beta7" }, "scripts": { "start": "react-app-rewired start", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 58290e2b..709ab309 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -223,7 +223,9 @@ }, "restoreWithWallet": { "title": "Login to your zkAccount", - "description": "Select the wallet you used to create your zkAccount" + "description": "Select the wallet you used to create your zkAccount", + "warning": "Wallets are shown for the current selected pool. If you want to use Tron with the Tronlink wallet, please <1>switch the pool here.", + "warning_tron": "Wallets are shown for the current selected pool. If you want to use Polygon, Optimism or Ethereum, please <1>switch the pool here." }, "restoreWithSecret": { "title": "Restore zkAccount", diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index a05cd3c2..9fcc4840 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -223,7 +223,9 @@ }, "restoreWithWallet": { "title": "Acessar sua conta zkAccount", - "description": "Selecione a carteira que você usou para criar sua conta zkAccount" + "description": "Selecione a carteira que você usou para criar sua conta zkAccount", + "warning": "As carteiras são exibidas para a pool selecionada atualmente. Se você deseja usar o Tron com a carteira Tronlink, por favor <1>troque a pool aqui.", + "warning_tron": "As carteiras são exibidas para a pool selecionada atualmente. Se você deseja usar Polygon, Optimism ou Ethereum, por favor <1>troque a pool aqui." }, "restoreWithSecret": { "title": "Restaurar conta zkAccount", diff --git a/src/App.js b/src/App.js index ff626f5d..fd560913 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,5 @@ import { createGlobalStyle } from 'styled-components'; +import { WalletProvider as TronWalletProvider } from '@tronweb3/tronwallet-adapter-react-hooks'; import ThemeProvider from 'providers/ThemeProvider'; import Web3Provider from 'providers/Web3Provider'; @@ -66,7 +67,9 @@ export default () => ( - + + + ); diff --git a/src/abis/token.json b/src/abis/token.json new file mode 100644 index 00000000..2b94af9e --- /dev/null +++ b/src/abis/token.json @@ -0,0 +1,101 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/assets/tron.png b/src/assets/tron.png new file mode 100644 index 0000000000000000000000000000000000000000..e078e536d1abf9b6c34e9bcfc925df86aa765194 GIT binary patch literal 60790 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 IDO>pcA1);Kg8%>k literal 0 HcmV?d00001 diff --git a/src/components/AccountSetUpModal/Generate/index.js b/src/components/AccountSetUpModal/Generate/index.js index c62be222..93fe4f73 100644 --- a/src/components/AccountSetUpModal/Generate/index.js +++ b/src/components/AccountSetUpModal/Generate/index.js @@ -1,13 +1,38 @@ -import React from 'react'; +import React, { useContext, useCallback } from 'react'; import styled from 'styled-components'; -import { useTranslation } from 'react-i18next'; +import { useTranslation, Trans } from 'react-i18next'; import WalletConnectors from 'components/WalletConnectors'; +import Button from 'components/Button'; + +import { PoolContext } from 'contexts'; + +import config from 'config'; + +const poolsWithAliases = Object.values(config.pools).map((pool, index) => ({ + ...pool, + alias: Object.keys(config.pools)[index], +})); export default ({ next, isCreation }) => { const { t } = useTranslation(); + const { currentPool, setCurrentPool } = useContext(PoolContext); + + const switchPool = useCallback(() => { + const pool = poolsWithAliases.find(pool => pool.isTron !== currentPool.isTron); + setCurrentPool(pool.alias); + }, [setCurrentPool, currentPool.isTron]); + return ( + {!isCreation && ( + + }} + /> + + )} {isCreation ? t('accountSetupModal.createWithWallet.description') @@ -41,3 +66,16 @@ const Description = styled.span` line-height: 20px; text-align: center; `; + +const Warning = styled.div` + background: ${({ theme }) => theme.warning.background}; + border: 1px solid ${({ theme }) => theme.warning.border}; + color: ${({ theme }) => theme.warning.text.color}; + border-radius: 16px; + padding: 16px 24px; + font-size: 14px; + line-height: 20px; + margin-left: -7px; + margin-right: -7px; + text-align: center; +`; diff --git a/src/components/AccountSetUpModal/index.js b/src/components/AccountSetUpModal/index.js index 1c762a2d..4fc448d5 100644 --- a/src/components/AccountSetUpModal/index.js +++ b/src/components/AccountSetUpModal/index.js @@ -134,7 +134,7 @@ export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) const generate = useCallback(async () => { const message = 'Access zkBob account.\n\nOnly sign this message for a trusted client!'; - let signedMessage = await signMessage({ message }); + let signedMessage = await signMessage(message); if (!window.location.host.includes(process.env.REACT_APP_LEGACY_SIGNATURE_DOMAIN)) { // Metamask with ledger returns V=0/1 here too, we need to adjust it to be ethereum's valid value (27 or 28) const MIN_VALID_V_VALUE = 27; diff --git a/src/components/Header/index.js b/src/components/Header/index.js index 56342f75..edd61c6f 100644 --- a/src/components/Header/index.js +++ b/src/components/Header/index.js @@ -43,7 +43,7 @@ const BalanceSkeleton = isMobile => ( export default ({ empty }) => { const { t } = useTranslation(); - const { address: account, connector } = useContext(WalletContext); + const { address: account, connector, isTron } = useContext(WalletContext); const { balance, nativeBalance, updateBalance, isLoadingBalance } = useContext(TokenBalanceContext); const { zkAccount, isLoadingZkAccount, balance: poolBalance, @@ -157,9 +157,11 @@ export default ({ empty }) => { {!isMobile && networkDropdown} - - {t('buttonText.getToken', { symbol: currentPool.tokenSymbol })} - + {!isTron && ( + + {t('buttonText.getToken', { symbol: currentPool.tokenSymbol })} + + )} {!isMobile && walletDropdown} {!isMobile && zkAccountDropdown} {(zkAccount && !isMobile) && ( diff --git a/src/components/WalletConnectors/index.js b/src/components/WalletConnectors/index.js index 72c5c831..ba16f0f8 100644 --- a/src/components/WalletConnectors/index.js +++ b/src/components/WalletConnectors/index.js @@ -14,16 +14,16 @@ const getConnectorName = connector => { export default ({ callback, gaIdPrefix = '' }) => { const { connector: activeConnector, connect, - disconnect, connectors, + disconnect, connectors, isTron, } = useContext(WalletContext); const connectWallet = useCallback(async connector => { - if (connector.id === activeConnector?.id) { + if (connector.id === activeConnector?.id && !isTron) { await disconnect(); } await connect({ connector }); callback?.(); - }, [connect, disconnect, activeConnector, callback]); + }, [connect, disconnect, activeConnector, callback, isTron]); return ( <> diff --git a/src/components/ZkAccountDropdown/index.js b/src/components/ZkAccountDropdown/index.js index a84a8fc5..0c4312f5 100644 --- a/src/components/ZkAccountDropdown/index.js +++ b/src/components/ZkAccountDropdown/index.js @@ -16,13 +16,13 @@ import { formatNumber } from 'utils'; import { TOKENS_ICONS } from 'constants'; -import { ZkAccountContext, ModalContext, PoolContext } from 'contexts'; +import { ZkAccountContext, ModalContext, PoolContext, WalletContext } from 'contexts'; const Content = ({ balance, generateAddress, getSeed, setPassword, removePassword, logout, close, showSeedPhrase, isLoadingState, initializeGiftCard, currentPool, - generatePaymentLink, + generatePaymentLink, isTron, }) => { const { t } = useTranslation(); const [privateAddress, setPrivateAddress] = useState(null); @@ -148,11 +148,13 @@ const Content = ({ {t('buttonText.getPaymentLink')} )} - - - {t('buttonText.redeemGiftCard')} - - + {!isTron && ( + + + {t('buttonText.redeemGiftCard')} + + + )} setShowSettings(true)} data-ga-id="zkaccount-settings"> {t('common.settings')} @@ -174,6 +176,7 @@ export default ({ children }) => { isZkAccountDropdownOpen, openZkAccountDropdown, closeZkAccountDropdown, } = useContext(ModalContext); const { currentPool } = useContext(PoolContext); + const { isTron } = useContext(WalletContext); return ( { currentPool={currentPool} close={closeZkAccountDropdown} generatePaymentLink={openPaymentLinkModal} + isTron={isTron} /> )} > diff --git a/src/config/index.js b/src/config/index.js index fdc18478..86c3bd03 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -154,6 +154,21 @@ const config = { ddSubgraph: 'zkbob-eth-goerli', addressPrefix: 'zkbob_goerli_eth', }, + 'USDT-shasta': { + chainId: 2494104990, + poolAddress: 'TLTyi81NhoeGfsq8Ef1STDYs6E7HFSAruV', + tokenAddress: 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs', + relayerUrls: ['https://shasta-relayer.thgkjlr.website'], + delegatedProverUrls: [], + coldStorageConfigPath: '', + tokenSymbol: 'USDT', + tokenDecimals: 6, + feeDecimals: 2, + depositScheme: 'approve', + minTxAmount: 50000n, // 0.05 USDT + addressPrefix: 'zkbob_shasta', + isTron: true, + }, }, chains: { '11155111': { @@ -164,6 +179,9 @@ const config = { }, '420': { rpcUrls: ['https://goerli.optimism.io'] + }, + '2494104990': { + rpcUrls: ['https://api.shasta.trongrid.io'] } }, snarkParams: { diff --git a/src/constants/index.js b/src/constants/index.js index 572e39af..9e4f8d1a 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -60,6 +60,14 @@ export const NETWORKS = { tx: 'https://optimism.blockscout.com/tx/%s', }, }, + 2494104990: { + name: 'Shasta', + icon: require('assets/tron.png'), + blockExplorerUrls: { + address: 'https://shasta.tronscan.org/#/address/%s', + tx: 'https://shasta.tronscan.org/#/transaction/%s', + }, + } }; export const TOKENS_ICONS = { @@ -68,12 +76,14 @@ export const TOKENS_ICONS = { 'BOB': require('assets/bob.svg').default, 'USDM': require('assets/usdc.svg').default, 'USDC': require('assets/usdc.svg').default, + 'USDT': require('assets/usdt.png'), }; export const CONNECTORS_ICONS = { 'MetaMask': require('assets/metamask.svg').default, 'WalletConnect': require('assets/walletconnect.svg').default, 'WalletConnectLegacy': require('assets/walletconnect.svg').default, + 'TronLink': require('assets/tronlink.png'), }; export const INCREASED_LIMITS_STATUSES = { diff --git a/src/contexts/TokenBalanceContext/index.js b/src/contexts/TokenBalanceContext/index.js index cfa722d1..b76c3606 100644 --- a/src/contexts/TokenBalanceContext/index.js +++ b/src/contexts/TokenBalanceContext/index.js @@ -5,15 +5,14 @@ import * as Sentry from '@sentry/react'; import { PoolContext, WalletContext } from 'contexts'; import { showLoadingError } from 'utils'; - -const TOKEN_ABI = ['function balanceOf(address) pure returns (uint256)']; +import tokenAbi from 'abis/token.json'; const TokenBalanceContext = createContext({ balance: null }); export default TokenBalanceContext; export const TokenBalanceContextProvider = ({ children }) => { - const { address: account, provider, getBalance } = useContext(WalletContext); + const { address: account, getBalance, callContract } = useContext(WalletContext); const { currentPool } = useContext(PoolContext); const [balance, setBalance] = useState(ethers.constants.Zero); const [nativeBalance, setNativeBalance] = useState(ethers.constants.Zero); @@ -25,10 +24,9 @@ export const TokenBalanceContextProvider = ({ children }) => { let nativeBalance = ethers.constants.Zero; if (account) { try { - const token = new ethers.Contract(currentPool.tokenAddress, TOKEN_ABI, provider); [balance, nativeBalance] = await Promise.all([ - token.balanceOf(account), - getBalance().then(({ data: { value } }) => value), + callContract(currentPool.tokenAddress, tokenAbi, 'balanceOf', [account]), + getBalance(), ]); } catch (error) { console.error(error); @@ -39,7 +37,7 @@ export const TokenBalanceContextProvider = ({ children }) => { setBalance(balance); setNativeBalance(nativeBalance); setIsLoadingBalance(false); - }, [account, getBalance, provider, currentPool.tokenAddress]); + }, [account, getBalance, callContract, currentPool.tokenAddress]); useEffect(() => { updateBalance(); diff --git a/src/contexts/WalletContext/index.js b/src/contexts/WalletContext/index.js index 894f9238..1a7197d7 100644 --- a/src/contexts/WalletContext/index.js +++ b/src/contexts/WalletContext/index.js @@ -1,24 +1,28 @@ -import { createContext, useContext } from 'react'; +import { createContext, useContext, useCallback, useMemo, useEffect, useState } from 'react'; +import { ethers, BigNumber } from 'ethers'; + import { useAccount, useSignMessage, useConnect, useDisconnect, useBalance, useProvider, useSigner, useNetwork, useSwitchNetwork, } from 'wagmi'; +import TronWeb from 'tronweb'; +import { useWallet } from '@tronweb3/tronwallet-adapter-react-hooks'; + import PoolContext from 'contexts/PoolContext'; const WalletContext = createContext({}); export default WalletContext; -const useEvmWallet = () => { - const { currentPool: pool } = useContext(PoolContext); +const useEvmWallet = pool => { const { address, connector } = useAccount(); const provider = useProvider({ chainId: pool.chainId }); const { signMessageAsync } = useSignMessage(); const { connectAsync, connectors } = useConnect(); const { disconnectAsync } = useDisconnect(); - const { refetch: getBalance } = useBalance({ address, chainId: pool.chainId }); + const { refetch } = useBalance({ address, chainId: pool.chainId }); const { data: signer } = useSigner({ chainId: pool.chainId }); const { chain } = useNetwork(); const { switchNetworkAsync } = useSwitchNetwork({ @@ -26,6 +30,20 @@ const useEvmWallet = () => { throwForSwitchChainNotSupported: true, }); + const getBalance = useCallback(async () => { + let balance = ethers.constants.Zero; + try { + const { data: { value } } = await refetch(); + balance = value; + } catch (error) {} + return balance; + }, [refetch]); + + const callContract = useCallback(async (address, abi, method, params = [], isSend = false) => { + const contract = new ethers.Contract(address, abi, isSend ? signer : provider); + return contract[method](...params); + }, [provider, signer]); + return { address, chain, @@ -35,15 +53,122 @@ const useEvmWallet = () => { connectors, connect: connectAsync, disconnect: disconnectAsync, - signMessage: signMessageAsync, + sign: message => signMessageAsync({ message }), + signMessage: message => signMessageAsync({ message }), + signTypedData: signer?._signTypedData, + sendTransaction: signer?.sendTransaction, switchNetwork: switchNetworkAsync, getBalance, + callContract, + waitForTx: tx => tx.wait(), + isAddress: ethers.utils.isAddress, + isTron: false, + }; +}; + +const convertWalletToConnector = wallet => ({ + ...wallet.adapter, + id: wallet.adapter.name.toLowerCase(), + ready: !['Loading', 'NotFound'].includes(wallet.state), +}); + +const useTronWallet = pool => { + const { address, connect, disconnect, select, wallet, wallets, signMessage } = useWallet(); + + const connector = useMemo(() => wallet ? convertWalletToConnector(wallet) : null, [wallet]); + const connectors = useMemo(() => wallets.map(convertWalletToConnector), [wallets]); + + const [chainId, setChainId] = useState(null); + + useEffect(() => { + async function updateChainId() { + const { chainId } = await wallet.adapter.network(); + setChainId(BigNumber.from(chainId).toNumber()); + } + if (wallet) updateChainId(); + }, [wallet]); + + const selectWalletAndConnect = useCallback(async ({ connector }) => { + try { + select(connector.name); + await connect(); + } catch (error) { + console.error(error); + } + }, [select, connect]); + + const getBalance = useCallback(async () => { + let balance = ethers.constants.Zero; + if (address && window.tronWeb) { + try { + const result = await window.tronWeb.trx.getBalance(address); + balance = BigNumber.from(result); + } catch (error) { + console.error(error); + } + } + return balance; + }, [address]); + + const callContract = async (address, abi, method, params = [], isSend = false) => { + if (!window.tronWeb) throw new Error('TronWeb not found'); + const contract = await window.tronWeb.contract(abi, address); + return contract[method](...params)[isSend ? 'send' : 'call'](); + } + + const waitForTx = async tx => { + if (!window.tronWeb) throw new Error('TronWeb not found'); + async function wait(attempt = 0) { + const response = await window.tronWeb.trx.getTransactionInfo(tx); + if (!response.receipt) { + if (attempt > 60) throw new Error('Response timeout'); + await new Promise(resolve => setTimeout(resolve, 3000)); + return wait(attempt + 1); + } + if (response.receipt.result !== 'SUCCESS') throw new Error('Transaction failed'); + return response; + } + return wait(); + } + + const signTypedData = async (domain, types, message) => { + if (!window.tronWeb) throw new Error('TronWeb not found'); + return window.tronWeb.trx._signTypedData(domain, types, message); + } + + // used for signing nullifier + const sign = async message => { + if (!window.tronWeb) throw new Error('TronWeb not found'); + return window.tronWeb.trx.sign(message); + } + + return { + address, + chain: { id: chainId }, + connector, + connectors, + connect: selectWalletAndConnect, + disconnect, + sign, + signMessage, + signTypedData, + sendTransaction: () => {}, + getBalance, + callContract, + waitForTx, + isAddress: TronWeb.isAddress, + isTron: true, }; }; export const WalletContextProvider = ({ children }) => { - const wallet = useEvmWallet(); + const { currentPool } = useContext(PoolContext); + const evmWallet = useEvmWallet(currentPool); + const tronWallet = useTronWallet(currentPool); + + const wallet = currentPool.isTron ? tronWallet : evmWallet; + return ( {children} diff --git a/src/contexts/ZkAccountContext/index.js b/src/contexts/ZkAccountContext/index.js index d4ae09dc..607e96cc 100644 --- a/src/contexts/ZkAccountContext/index.js +++ b/src/contexts/ZkAccountContext/index.js @@ -41,7 +41,10 @@ export default ZkAccountContext; export const ZkAccountContextProvider = ({ children }) => { const { currentPool, setCurrentPool } = useContext(PoolContext); const previousPoolAlias = usePrevious(currentPool.alias); - const { address: account, chain, signer, switchNetwork } = useContext(WalletContext); + const { + address: account, chain, switchNetwork, + sign, signTypedData, sendTransaction, + } = useContext(WalletContext); const { openTxModal, setTxStatus, setTxAmount, setTxError } = useContext(TransactionModalContext); const { openPasswordModal, closePasswordModal, closeAllModals } = useContext(ModalContext); const { updateBalance: updateTokenBalance } = useContext(TokenBalanceContext); @@ -320,9 +323,9 @@ export const ZkAccountContextProvider = ({ children }) => { } const shieldedAmount = await toShieldedAmount(amount); if (isNative) { - await zp.directDeposit(signer, zkClient, shieldedAmount, setTxStatus); + await zp.directDeposit(account, sendTransaction, zkClient, shieldedAmount, setTxStatus); } else { - await zp.deposit(signer, zkClient, shieldedAmount, relayerFee, setTxStatus); + await zp.deposit(account, sign, signTypedData, zkClient, shieldedAmount, relayerFee, setTxStatus); } updatePoolData(); setTimeout(updateTokenBalance, 5000); @@ -345,9 +348,9 @@ export const ZkAccountContextProvider = ({ children }) => { } } }, [ - zkClient, updatePoolData, signer, openTxModal, setTxAmount, + zkClient, updatePoolData, signTypedData, openTxModal, setTxAmount, setTxStatus, updateTokenBalance, toShieldedAmount, setTxError, - chain, switchNetwork, currentPool, + chain, switchNetwork, currentPool, sendTransaction, account, sign, ]); const transfer = useCallback(async (to, amount, relayerFee) => { diff --git a/src/contexts/ZkAccountContext/zp.js b/src/contexts/ZkAccountContext/zp.js index 71e7adbe..a8f9f139 100644 --- a/src/contexts/ZkAccountContext/zp.js +++ b/src/contexts/ZkAccountContext/zp.js @@ -33,7 +33,7 @@ const createAccount = async (zkClient, secretKey, birthIndex, useDelegatedProver }); }; -const deposit = async (signer, zkClient, amount, fee, setTxStatus) => { +const deposit = async (from, sign, signTypedData, zkClient, amount, fee, setTxStatus) => { setTxStatus(TX_STATUSES.GENERATING_PROOF); const signFunction = async ({ type, data }) => { setTxStatus(TX_STATUSES.SIGN_MESSAGE); @@ -41,28 +41,28 @@ const deposit = async (signer, zkClient, amount, fee, setTxStatus) => { if (type === SignatureType.TypedDataV4) { const { domain, types, message } = data; delete types.EIP712Domain; - signature = await signer._signTypedData(domain, types, message); + signature = await signTypedData(domain, types, message); + } else if (type === SignatureType.PersonalSign) { + signature = await sign(data); } setTxStatus(TX_STATUSES.GENERATING_PROOF); return signature; }; - const myAddress = await signer.getAddress(); - const jobId = await zkClient.deposit(amount, signFunction, myAddress, fee); + const jobId = await zkClient.deposit(amount, signFunction, from, fee); setTxStatus(TX_STATUSES.WAITING_FOR_RELAYER); await zkClient.waitJobTxHash(jobId); setTxStatus(TX_STATUSES.DEPOSITED); }; -const directDeposit = async (signer, zkClient, amount, setTxStatus) => { +const directDeposit = async (from, sendTransaction, zkClient, amount, setTxStatus) => { setTxStatus(TX_STATUSES.CONFIRM_TRANSACTION); const sendFunction = async ({ to, amount, data }) => { - const tx = await signer.sendTransaction({ to, value: amount, data }); + const tx = await sendTransaction({ to, value: amount, data }); setTxStatus(TX_STATUSES.WAITING_FOR_TRANSACTION); const receipt = await tx.wait(); return receipt.transactionHash; }; - const myAddress = await signer.getAddress(); - await zkClient.directDeposit(DirectDepositType.Native, myAddress, amount, sendFunction); + await zkClient.directDeposit(DirectDepositType.Native, from, amount, sendFunction); setTxStatus(TX_STATUSES.DEPOSITED); }; diff --git a/src/hooks/useApproval.js b/src/hooks/useApproval.js index edea9d4c..131bccd5 100644 --- a/src/hooks/useApproval.js +++ b/src/hooks/useApproval.js @@ -1,23 +1,22 @@ -import { useState, useEffect, useContext, useCallback } from 'react'; +import { useState, useEffect, useContext, useCallback, useMemo } from 'react'; import { ethers } from 'ethers'; import * as Sentry from '@sentry/react'; import { TransactionModalContext, WalletContext } from 'contexts'; import { TX_STATUSES, PERMIT2_CONTRACT_ADDRESS } from 'constants'; -import { useMemo } from 'react'; +import tokenAbi from 'abis/token.json'; -const TOKEN_ABI = [ - 'function allowance(address, address) pure returns (uint256)', - 'function approve(address, uint256) returns (bool)', -]; - -export default (chainId, tokenAddress, amount, balance) => { +export default (pool, tokenAddress, amount, balance, type = 'permit2') => { const { openTxModal, closeTxModal, setTxStatus, setTxError } = useContext(TransactionModalContext); - const { address: account, provider, signer, chain, switchNetwork } = useContext(WalletContext); + const { address: account, chain, switchNetwork, callContract, waitForTx } = useContext(WalletContext); const [allowance, setAllowance] = useState(ethers.constants.Zero); + const contractForApproval = useMemo(() => + type === 'permit2' ? PERMIT2_CONTRACT_ADDRESS : pool.poolAddress, + [type, pool.poolAddress] + ); const isApproved = useMemo(() => allowance.gte(amount), [allowance, amount]); const updateAllowance = useCallback(async () => { @@ -25,11 +24,9 @@ export default (chainId, tokenAddress, amount, balance) => { setAllowance(ethers.constants.Zero); return; } - const token = new ethers.Contract(tokenAddress, TOKEN_ABI, provider); - token.allowance(account, PERMIT2_CONTRACT_ADDRESS).then(allowance => { - setAllowance(allowance); - }); - }, [account, provider, tokenAddress]); + const allowance = await callContract(tokenAddress, tokenAbi, 'allowance', [account, contractForApproval]); + setAllowance(allowance); + }, [account, callContract, tokenAddress, contractForApproval]); useEffect(() => { updateAllowance(); @@ -38,7 +35,7 @@ export default (chainId, tokenAddress, amount, balance) => { const approve = useCallback(async () => { try { openTxModal(); - if (chain.id !== chainId) { + if (chain.id !== pool.chainId) { setTxStatus(TX_STATUSES.SWITCH_NETWORK); try { await switchNetwork(); @@ -50,24 +47,23 @@ export default (chainId, tokenAddress, amount, balance) => { } } setTxStatus(TX_STATUSES.APPROVE_TOKENS); - const token = new ethers.Contract(tokenAddress, TOKEN_ABI, signer); - const tx = await token.approve(PERMIT2_CONTRACT_ADDRESS, ethers.constants.MaxUint256); + const tx = await callContract(tokenAddress, tokenAbi, 'approve', [contractForApproval, ethers.constants.MaxUint256], true); setTxStatus(TX_STATUSES.WAITING_FOR_TRANSACTION); - await tx.wait(); + await waitForTx(tx); closeTxModal(); updateAllowance(); } catch (error) { - console.error(error); Sentry.captureException(error, { tags: { method: 'hooks.useApproval.approve' } }); - const message = error.message.includes('user rejected transaction') + const message = error.message?.includes('user rejected transaction') ? 'User denied transaction signature' : error.message; - setTxError(message); + setTxError(message || error); setTxStatus(TX_STATUSES.REJECTED); } }, [ openTxModal, setTxStatus, setTxError, switchNetwork, chain, - signer, updateAllowance, chainId, tokenAddress, closeTxModal, + waitForTx, updateAllowance, pool.chainId, tokenAddress, closeTxModal, + callContract, contractForApproval, ]); return { isApproved, approve, updateAllowance }; diff --git a/src/pages/Deposit/index.js b/src/pages/Deposit/index.js index 5f14ddce..5ccd070c 100644 --- a/src/pages/Deposit/index.js +++ b/src/pages/Deposit/index.js @@ -60,7 +60,7 @@ export default () => { () => isNativeTokenUsed ? directDepositFee : fee, [isNativeTokenUsed, directDepositFee, fee], ); - const { isApproved, approve } = useApproval(currentPool.chainId, currentPool.tokenAddress, amount.add(fee), balance); + const { isApproved, approve } = useApproval(currentPool, currentPool.tokenAddress, amount.add(fee), balance, currentPool.depositScheme); const depositLimit = useDepositLimit(limits, isNativeTokenUsed); const maxAmountExceeded = useMaxAmountExceeded(amount, usedBalance, usedFee, depositLimit); @@ -136,7 +136,7 @@ export default () => { else if (amount.gt(depositLimit)) { return ; } - else if (currentPool.isNative && !isNativeSelected && !isApproved) { + else if (['permit2', 'approve'].includes(currentPool.depositScheme) && !isNativeTokenUsed && !isApproved) { return ; } else { diff --git a/src/pages/Payment/index.js b/src/pages/Payment/index.js index fcab51db..c716c079 100644 --- a/src/pages/Payment/index.js +++ b/src/pages/Payment/index.js @@ -53,7 +53,7 @@ const Payment = ({ pool }) => { const { limit, isLoadingLimit, fee, isLoadingFee } = useLimitsAndFees(pool); const { tokenAmount, liFiRoute, isTokenAmountLoading } = useTokenAmount(pool, selectedToken?.address, amount, fee); - const { isApproved, approve } = useApproval(pool?.chainId, selectedToken?.address, tokenAmount); + const { isApproved, approve } = useApproval(pool, selectedToken?.address, tokenAmount); const permitType = useMemo(() => getPermitType(selectedToken, pool?.chainId), [pool, selectedToken]); const { send } = usePayment(selectedToken, tokenAmount, amount, fee, pool, params.address, liFiRoute); diff --git a/src/pages/Payment/utils.js b/src/pages/Payment/utils.js index fd0acf31..d815a32d 100644 --- a/src/pages/Payment/utils.js +++ b/src/pages/Payment/utils.js @@ -1,6 +1,7 @@ import { ethers, BigNumber } from 'ethers'; import { PERMIT2_CONTRACT_ADDRESS } from 'constants'; +import tokenAbi from 'abis/token.json'; export function getPermitType(token, chainId) { if (token?.symbol === 'USDC') return chainId === 137 ? 'permit-usdc-polygon' : 'permit-usdc'; @@ -23,11 +24,7 @@ export function getNullifier(permitType) { } async function getNameAndNonce(tokenAddress, ownerAddress, provider) { - const tokenABI = [ - 'function name() view returns (string)', - 'function nonces(address) view returns (uint256)', - ]; - const tokenContractInstance = new ethers.Contract(tokenAddress, tokenABI, provider); + const tokenContractInstance = new ethers.Contract(tokenAddress, tokenAbi, provider); const [name, nonce] = await Promise.all([ tokenContractInstance.name(), tokenContractInstance.nonces(ownerAddress), diff --git a/src/pages/Withdraw/hooks.js b/src/pages/Withdraw/hooks.js index 16e5dddc..f306cb2c 100644 --- a/src/pages/Withdraw/hooks.js +++ b/src/pages/Withdraw/hooks.js @@ -29,23 +29,24 @@ const POOL_CONTRACT_ABI = ['function tokenSeller() pure returns (address)']; const SWAP_CONTRACT_ABI = ['function quoteSellForETH(uint256) pure returns (uint256)']; export const useConvertion = (currentPool) => { - const { provider } = useContext(WalletContext); + const { callContract, isTron } = useContext(WalletContext); const [price, setPrice] = useState(ethers.constants.Zero); const [exist, setExist] = useState(false); useEffect(() => { async function getPrice() { + if (isTron) return; try { let swapContractAddress = ethers.constants.AddressZero; - const poolContract = new ethers.Contract(currentPool.poolAddress, POOL_CONTRACT_ABI, provider); try { - swapContractAddress = await poolContract.tokenSeller(); + swapContractAddress = await callContract(currentPool.poolAddress, POOL_CONTRACT_ABI, 'tokenSeller'); } catch (error) {} const exist = swapContractAddress !== ethers.constants.AddressZero; setExist(exist); if (!exist) return; - const swapContract = new ethers.Contract(swapContractAddress, SWAP_CONTRACT_ABI, provider); - const price = await swapContract.quoteSellForETH(ethers.utils.parseUnits('1', currentPool.tokenDecimals)); + const price = await callContract(swapContractAddress, SWAP_CONTRACT_ABI, 'quoteSellForETH', [ + ethers.utils.parseUnits('1', currentPool.tokenDecimals), + ]); setPrice(price); } catch (error) { console.error(error); @@ -57,7 +58,7 @@ export const useConvertion = (currentPool) => { getPrice(); const interval = setInterval(getPrice, 1000 * 60 * 5); return () => clearInterval(interval); - }, [provider, currentPool]); + }, [callContract, currentPool, isTron]); return { exist, price, decimals: 18, toTokenSymbol: NATIVE_TOKENS[currentPool.alias] }; }; diff --git a/src/pages/Withdraw/index.js b/src/pages/Withdraw/index.js index 56874d5b..79650bec 100644 --- a/src/pages/Withdraw/index.js +++ b/src/pages/Withdraw/index.js @@ -8,7 +8,7 @@ import { useTranslation, Trans } from 'react-i18next'; import AccountSetUpButton from 'containers/AccountSetUpButton'; import PendingAction from 'containers/PendingAction'; -import { ZkAccountContext, PoolContext } from 'contexts'; +import { ZkAccountContext, PoolContext, WalletContext } from 'contexts'; import TransferInput from 'components/TransferInput'; import Card from 'components/Card'; @@ -35,6 +35,7 @@ export default () => { isPending, isDemo, limits, isLoadingLimits, minTxAmount, } = useContext(ZkAccountContext); const { currentPool } = useContext(PoolContext); + const { isAddress } = useContext(WalletContext); const [displayAmount, setDisplayAmount] = useState(''); const amount = useParsedAmount(displayAmount, currentPool.tokenDecimals); const [receiver, setReceiver] = useState(''); @@ -82,7 +83,7 @@ export default () => { button = ; } else if (!receiver) { button = ; - } else if (!ethers.utils.isAddress(receiver)) { + } else if (!isAddress(receiver)) { button = ; } else { button = ( diff --git a/yarn.lock b/yarn.lock index c7da32ad..ff28762d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,11 +2,21 @@ # yarn lockfile v1 +"@adraffy/ens-normalize@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.0.tgz#223572538f6bea336750039bb43a4016dcc8182d" + integrity sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ== + "@adraffy/ens-normalize@1.9.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz#60111a5d9db45b2e5cbb6231b0bb8d97e8659316" integrity sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg== +"@adraffy/ens-normalize@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.4.tgz#aae21cb858bbb0411949d5b7b3051f4209043f62" + integrity sha512-UK0bHA7hh9cR39V+4gl2/NnBBjoXIxkuWAPCaY4X7fbH4L/azIi7ilWOCjMUYfpJgraLUAqkRi2BqrjME8Rynw== + "@ampproject/remapping@^2.1.0": version "2.1.2" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" @@ -1682,6 +1692,29 @@ stream-browserify "^3.0.0" util "^0.12.4" +"@coinbase/wallet-sdk@^3.6.6": + version "3.7.2" + resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-3.7.2.tgz#7a89bd9e3a06a1f26d4480d8642af33fb0c7e3aa" + integrity sha512-lIGvXMsgpsQWci/XOMQIJ2nIZ8JUy/L+bvC0wkRaYarr0YylwpXrJ2gRM3hCXPS477pkyO7N/kSiAoRgEXUdJQ== + dependencies: + "@metamask/safe-event-emitter" "2.0.0" + "@solana/web3.js" "^1.70.1" + bind-decorator "^1.0.11" + bn.js "^5.1.1" + buffer "^6.0.3" + clsx "^1.1.0" + eth-block-tracker "6.1.0" + eth-json-rpc-filters "5.1.0" + eth-rpc-errors "4.0.2" + json-rpc-engine "6.1.0" + keccak "^3.0.1" + preact "^10.5.9" + qs "^6.10.3" + rxjs "^6.6.3" + sha.js "^2.4.11" + stream-browserify "^3.0.0" + util "^0.12.4" + "@coinbase/wallet-sdk@^3.7.1": version "3.7.1" resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-3.7.1.tgz#44b3b7a925ff5cc974e4cbf7a44199ffdcf03541" @@ -3511,6 +3544,77 @@ resolved "https://registry.yarnpkg.com/@ledgerhq/connect-kit-loader/-/connect-kit-loader-1.0.2.tgz#8554e16943f86cc2a5f6348a14dfe6e5bd0c572a" integrity sha512-TQ21IjcZOw/scqypaVFY3jHVqI7X7Hta3qN/us6FvTol3AY06UmrhhXGww0E9xHmAbdX241ddwXEiMBSQZFr9g== +"@ledgerhq/connect-kit-loader@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ledgerhq/connect-kit-loader/-/connect-kit-loader-1.1.2.tgz#d550e3c1f046e4c796f32a75324b03606b7e226a" + integrity sha512-mscwGroSJQrCTjtNGBu+18FQbZYA4+q6Tyx6K7CXHl6AwgZKbWfZYdgP2F+fyZcRUdGRsMX8QtvU61VcGGtO1A== + +"@ledgerhq/devices@^6.27.1": + version "6.27.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-6.27.1.tgz#3b13ab1d1ba8201e9e74a08f390560483978c962" + integrity sha512-jX++oy89jtv7Dp2X6gwt3MMkoajel80JFWcdc0HCouwDsV1mVJ3SQdwl/bQU0zd8HI6KebvUP95QTwbQLLK/RQ== + dependencies: + "@ledgerhq/errors" "^6.10.0" + "@ledgerhq/logs" "^6.10.0" + rxjs "6" + semver "^7.3.5" + +"@ledgerhq/devices@^8.0.7": + version "8.0.7" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.0.7.tgz#206434dbd8a097529bbfc95f5eef94c2923c7578" + integrity sha512-BbPyET52lXnVs7CxJWrGYqmtGdbGzj+XnfCqLsDnA7QYr1CZREysxmie+Rr6BKpNDBRVesAovXjtaVaZOn+upw== + dependencies: + "@ledgerhq/errors" "^6.14.0" + "@ledgerhq/logs" "^6.10.1" + rxjs "6" + semver "^7.3.5" + +"@ledgerhq/errors@^6.10.0", "@ledgerhq/errors@^6.14.0": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.14.0.tgz#0bf253983773ef12eebce2091f463bc719223b37" + integrity sha512-ZWJw2Ti6Dq1Ott/+qYqJdDWeZm16qI3VNG5rFlb0TQ3UcAyLIQZbnnzzdcVVwVeZiEp66WIpINd/pBdqsHVyOA== + +"@ledgerhq/hw-app-trx@^6.27.8": + version "6.27.19" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-trx/-/hw-app-trx-6.27.19.tgz#4b5a23727f24377b5c3aa00d84004c6734556d35" + integrity sha512-I5hBm+rIBpa+YkSnrFzm3LpaBcQ91HijO3xpLLWwL+wp1Pr/TE70tGc/c5oGmhV8kQGNvUzJ1Pe0NWsI7/EBlw== + dependencies: + "@ledgerhq/errors" "^6.14.0" + "@ledgerhq/hw-transport" "^6.28.8" + +"@ledgerhq/hw-transport-webhid@6.27.1": + version "6.27.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.27.1.tgz#8fd1710d23b6bd7cbe2382dd02054dfabe788447" + integrity sha512-u74rBYlibpbyGblSn74fRs2pMM19gEAkYhfVibq0RE1GNFjxDMFC1n7Sb+93Jqmz8flyfB4UFJsxs8/l1tm2Kw== + dependencies: + "@ledgerhq/devices" "^6.27.1" + "@ledgerhq/errors" "^6.10.0" + "@ledgerhq/hw-transport" "^6.27.1" + "@ledgerhq/logs" "^6.10.0" + +"@ledgerhq/hw-transport@6.27.1": + version "6.27.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.27.1.tgz#88072278f69c279cb6569352acd4ae2fec33ace3" + integrity sha512-hnE4/Fq1YzQI4PA1W0H8tCkI99R3UWDb3pJeZd6/Xs4Qw/q1uiQO+vNLC6KIPPhK0IajUfuI/P2jk0qWcMsuAQ== + dependencies: + "@ledgerhq/devices" "^6.27.1" + "@ledgerhq/errors" "^6.10.0" + events "^3.3.0" + +"@ledgerhq/hw-transport@^6.27.1", "@ledgerhq/hw-transport@^6.28.8": + version "6.28.8" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.28.8.tgz#f99a5c71c5c09591e9bfb1b970c42aafbe81351f" + integrity sha512-XxQVl4htd018u/M66r0iu5nlHi+J6QfdPsORzDF6N39jaz+tMqItb7tUlXM/isggcuS5lc7GJo7NOuJ8rvHZaQ== + dependencies: + "@ledgerhq/devices" "^8.0.7" + "@ledgerhq/errors" "^6.14.0" + events "^3.3.0" + +"@ledgerhq/logs@^6.10.0", "@ledgerhq/logs@^6.10.1": + version "6.10.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.10.1.tgz#5bd16082261d7364eabb511c788f00937dac588d" + integrity sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w== + "@lifi/sdk@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@lifi/sdk/-/sdk-2.1.0.tgz#7eb9917d6d461566fb15f660b7d2344ad66e22fe" @@ -3864,6 +3968,13 @@ resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz#ea6d23ade78a325f7a52750aab1526b02b628c29" integrity sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg== +"@noble/curves@1.0.0", "@noble/curves@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" + integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== + dependencies: + "@noble/hashes" "1.3.0" + "@noble/curves@1.1.0", "@noble/curves@~1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" @@ -3871,6 +3982,13 @@ dependencies: "@noble/hashes" "1.3.1" +"@noble/curves@1.2.0", "@noble/curves@~1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + "@noble/ed25519@^1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.1.tgz#6899660f6fbb97798a6fbd227227c4589a454724" @@ -3881,11 +3999,21 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== +"@noble/hashes@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" + integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== + "@noble/hashes@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== +"@noble/hashes@1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1", "@noble/hashes@~1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@noble/hashes@^1.1.2": version "1.1.5" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.5.tgz#1a0377f3b9020efe2fae03290bd2a12140c95c11" @@ -3896,11 +4024,6 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.3.tgz#360afc77610e0a61f3417e497dcf36862e4f8111" integrity sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A== -"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" - integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== - "@noble/secp256k1@1.6.3", "@noble/secp256k1@~1.6.0": version "1.6.3" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.6.3.tgz#7eed12d9f4404b416999d0c87686836c4c5c9b94" @@ -4054,6 +4177,14 @@ "@safe-global/safe-apps-sdk" "7.9.0" events "^3.3.0" +"@safe-global/safe-apps-provider@^0.17.1": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-provider/-/safe-apps-provider-0.17.1.tgz#72df2a66be5343940ed505efe594ed3b0f2f7015" + integrity sha512-lYfRqrbbK1aKU1/UGkYWc/X7PgySYcumXKc5FB2uuwAs2Ghj8uETuW5BrwPqyjBknRxutFbTv+gth/JzjxAhdQ== + dependencies: + "@safe-global/safe-apps-sdk" "8.0.0" + events "^3.3.0" + "@safe-global/safe-apps-sdk@7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-7.9.0.tgz#0c79a7760470bfdaf4cce9aa5bceef56898c7037" @@ -4062,6 +4193,14 @@ "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" ethers "^5.7.2" +"@safe-global/safe-apps-sdk@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-8.0.0.tgz#9bdfe0e0d85e1b2d279bb840f40c4b930aaf8bc1" + integrity sha512-gYw0ki/EAuV1oSyMxpqandHjnthZjYYy+YWpTAzf8BqfXM3ItcZLpjxfg+3+mXW8HIO+3jw6T9iiqEXsqHaMMw== + dependencies: + "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" + viem "^1.0.0" + "@safe-global/safe-apps-sdk@^7.9.0": version "7.10.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-7.10.0.tgz#e75fc581126f27c52ec2601da51bca5eb99b61f4" @@ -4070,6 +4209,14 @@ "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" ethers "^5.7.2" +"@safe-global/safe-apps-sdk@^8.0.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-8.1.0.tgz#d1d0c69cd2bf4eef8a79c5d677d16971926aa64a" + integrity sha512-XJbEPuaVc7b9n23MqlF6c+ToYIS3f7P2Sel8f3cSBQ9WORE4xrSuvhMpK9fDSFqJ7by/brc+rmJR/5HViRr0/w== + dependencies: + "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" + viem "^1.0.0" + "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.7.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.7.0.tgz#2af52f1bc73759b1b6a549fed598781c8c5fce72" @@ -4082,6 +4229,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== +"@scure/base@~1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f" + integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q== + "@scure/bip32@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.0.tgz#dea45875e7fbc720c2b4560325f1cf5d2246d95b" @@ -4100,6 +4252,15 @@ "@noble/secp256k1" "~1.7.0" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.0.tgz#6c8d980ef3f290987736acd0ee2e0f0d50068d87" + integrity sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q== + dependencies: + "@noble/curves" "~1.0.0" + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@scure/bip32@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" @@ -4109,6 +4270,15 @@ "@noble/hashes" "~1.3.1" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" + integrity sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA== + dependencies: + "@noble/curves" "~1.2.0" + "@noble/hashes" "~1.3.2" + "@scure/base" "~1.1.2" + "@scure/bip39@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.0.tgz#92f11d095bae025f166bef3defcc5bf4945d419a" @@ -4117,6 +4287,14 @@ "@noble/hashes" "~1.1.1" "@scure/base" "~1.1.0" +"@scure/bip39@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b" + integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@scure/bip39@1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" @@ -4634,6 +4812,107 @@ resolved "https://registry.yarnpkg.com/@tronweb3/google-protobuf/-/google-protobuf-3.21.2.tgz#0964cf83ed7826d31c3cb4e4ecf07655681631c9" integrity sha512-IVcT2GfWX3K6tHUVhs14NP5uzKhQt4KeDya1g9ACxuZsUzsaoGUIGzceK2Ltu7xp1YV94AaHOf4yxLAivlvEkQ== +"@tronweb3/tronwallet-abstract-adapter@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-abstract-adapter/-/tronwallet-abstract-adapter-1.1.5.tgz#8d85be6fe9b5c88ba1f3132633df5b26b9b8d42a" + integrity sha512-hkZPlvv1ZemzNZ0kLR+isooaqLbOpG2JI+l/my+Io1ZOF0lh5hu6AVtCV7/OkkekX4qRH5jZ20GlSSkvQ4kchg== + dependencies: + eventemitter3 "^4.0.0" + +"@tronweb3/tronwallet-adapter-bitkeep@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-adapter-bitkeep/-/tronwallet-adapter-bitkeep-1.0.1.tgz#ee2780c0570219569890d7579d3d41ce7059a65c" + integrity sha512-Es8KkoZ9amSQZB3EjxAULCsUxyingdqziJNdzH1bGS7H0NpNnmj7dhul/2C1pXFKMY7DwRPrUpDxfotIZ57wKg== + dependencies: + "@tronweb3/tronwallet-abstract-adapter" "^1.1.5" + "@tronweb3/tronwallet-adapter-tronlink" "^1.1.6" + +"@tronweb3/tronwallet-adapter-ledger@^1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-adapter-ledger/-/tronwallet-adapter-ledger-1.1.7.tgz#39db1580f5fb2bcad75883b1f36c58ac74c3009a" + integrity sha512-xBr8IX6XAzd3qEmpzPiuFQ8F1G0LpkxnZMCPMCQkzhbBFVEfj6dIrzG7n8S58CareniqZtAmEwYhUaCPtozhHg== + dependencies: + "@ledgerhq/hw-app-trx" "^6.27.8" + "@ledgerhq/hw-transport" "6.27.1" + "@ledgerhq/hw-transport-webhid" "6.27.1" + "@tronweb3/tronwallet-abstract-adapter" "^1.1.5" + buffer "^6.0.3" + eventemitter3 "^4.0.0" + preact "^10.11.3" + +"@tronweb3/tronwallet-adapter-okxwallet@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-adapter-okxwallet/-/tronwallet-adapter-okxwallet-1.0.1.tgz#570cd8dbac66ae5fb68f8b6376de385bba16237d" + integrity sha512-Sch5VVXYoEe21abWniFpou15oU4fLL4nrYPAVlowZ3h8aHJCMsTuQRh5ASS9PG8zRwnw3SxwPtFEtWjSRE/rOQ== + dependencies: + "@tronweb3/tronwallet-abstract-adapter" "^1.1.5" + "@tronweb3/tronwallet-adapter-tronlink" "^1.1.6" + +"@tronweb3/tronwallet-adapter-react-hooks@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-adapter-react-hooks/-/tronwallet-adapter-react-hooks-1.1.6.tgz#0cef1d5cb4bc868e00ff4c6d4adf5951f1578102" + integrity sha512-AU83y34ivwEOj9RKDi5mCLlI5BaBSvjyIQ9Y0VIWj6bIDqr0SxtYwD7g/m1JpH5ht+kiclWlMDvm1X4aaG1qPw== + dependencies: + "@tronweb3/tronwallet-abstract-adapter" "^1.1.5" + "@tronweb3/tronwallet-adapter-tronlink" "^1.1.8" + +"@tronweb3/tronwallet-adapter-react-ui@^1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-adapter-react-ui/-/tronwallet-adapter-react-ui-1.1.7.tgz#4a27ced35d8aac09ef8a5e6931d94f21d3d24a47" + integrity sha512-bF0RYYMz5aAGFivbTuW1UTvJi8UtWMkH+yoEpKf4Nec367jct1NHQYkCql3trgT0eBpJT6g/APq4v+nLdJleSQ== + dependencies: + "@tronweb3/tronwallet-abstract-adapter" "^1.1.5" + "@tronweb3/tronwallet-adapter-react-hooks" "^1.1.6" + +"@tronweb3/tronwallet-adapter-tokenpocket@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-adapter-tokenpocket/-/tronwallet-adapter-tokenpocket-1.0.1.tgz#2b96f3b707b58087d49dc7074da149da199dbca2" + integrity sha512-fz8aBLd6NTBLWZfrN6xnVDClKJxwsEj7VkokghL5EmdUnwH8RSHFWNSuQ7ECiVDO3eKJtM6FHh4+PTEPw14fyg== + dependencies: + "@tronweb3/tronwallet-abstract-adapter" "^1.1.5" + "@tronweb3/tronwallet-adapter-tronlink" "^1.1.6" + +"@tronweb3/tronwallet-adapter-tronlink@^1.1.6", "@tronweb3/tronwallet-adapter-tronlink@^1.1.8": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-adapter-tronlink/-/tronwallet-adapter-tronlink-1.1.8.tgz#1dabe004d9691ec904ebfc4fb980a5ba87554c41" + integrity sha512-NCvl2rei6YF2HwfGk9weO+3i+XUI+Tr/YHhijsmqYft8Dmc+d41MtbN0Hvbahs/9cp1DDsmVl+b5971JnOjh9w== + dependencies: + "@tronweb3/tronwallet-abstract-adapter" "^1.1.5" + +"@tronweb3/tronwallet-adapter-walletconnect@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-adapter-walletconnect/-/tronwallet-adapter-walletconnect-1.0.4.tgz#30612f5c1903b3fbfe5e920547c397f7cdf2db00" + integrity sha512-ryGItC8qMHjSTNXfrAkxQ4U5oxr0jx9zjBqUtfc+AGtBF05bSqVVosE5CL1ug90u0CXtsGUejgH5WbPBBZrf2w== + dependencies: + "@tronweb3/tronwallet-abstract-adapter" "^1.1.5" + "@tronweb3/walletconnect-tron" "2.0.0" + "@wagmi/core" "^1.1.0" + "@walletconnect/sign-client" "^2.1.4" + "@walletconnect/types" "^2.1.4" + "@web3modal/ethereum" "^2.4.2" + viem "^0.3.50" + +"@tronweb3/tronwallet-adapters@^1.1.8": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@tronweb3/tronwallet-adapters/-/tronwallet-adapters-1.1.8.tgz#45e5da1698ba5633e174f05054bf08fedba1e750" + integrity sha512-Qr8MF6IBkJ0E1B8t7PfeqrL2RONX5rI6g/iE6KpV2DjaoQuL35cxy+Mmv5J0zXWJKquvdn6MdPyB+iaQoSuvBw== + dependencies: + "@tronweb3/tronwallet-adapter-bitkeep" "^1.0.1" + "@tronweb3/tronwallet-adapter-ledger" "^1.1.7" + "@tronweb3/tronwallet-adapter-okxwallet" "^1.0.1" + "@tronweb3/tronwallet-adapter-tokenpocket" "^1.0.1" + "@tronweb3/tronwallet-adapter-tronlink" "^1.1.8" + "@tronweb3/tronwallet-adapter-walletconnect" "^1.0.4" + +"@tronweb3/walletconnect-tron@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tronweb3/walletconnect-tron/-/walletconnect-tron-2.0.0.tgz#067b36595e6608ff5c432d93a443380fc0dae986" + integrity sha512-COAm1zOnCUHKU+rMFsTDYn5PX9fD8z1/QItHNpWo33BBDPk+nZfz0mK7orGpVOpO5IcOG33AsyGxqv/C/gJnTQ== + dependencies: + "@walletconnect/sign-client" "^2.1.3" + "@walletconnect/utils" "^2.1.3" + "@web3modal/standalone" "^2.4.1" + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -5070,6 +5349,13 @@ dependencies: "@types/node" "*" +"@types/ws@^8.5.5": + version "8.5.6" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.6.tgz#e9ad51f0ab79b9110c50916c9fcbddc36d373065" + integrity sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -5181,6 +5467,11 @@ resolved "https://registry.yarnpkg.com/@wagmi/chains/-/chains-0.2.11.tgz#3f5bed54524f2d3399944946b88b1e7207aa5a8d" integrity sha512-aMrI1zKKXdeAaiTxBiv+3Zfgd3IajCDpxBtPPvpjXuWVRe4ikwzbyZ1HARKj3V1+wNMPng8EJiWpN966PcvROg== +"@wagmi/chains@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@wagmi/chains/-/chains-1.0.0.tgz#41710941f2c2a699a246c4e3a6112b4efd996171" + integrity sha512-eNbqRWyHbivcMNq5tbXJks4NaOzVLHnNQauHPeE/EDT9AlpqzcrMc+v2T1/2Iw8zN4zgqB86NCsxeJHJs7+xng== + "@wagmi/connectors@0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-0.3.2.tgz#18e810af77467b05f37a131a13921f09a2d70b8a" @@ -5195,6 +5486,22 @@ abitype "^0.3.0" eventemitter3 "^4.0.7" +"@wagmi/connectors@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-3.1.2.tgz#4fd33fc4061ffb53c68860a203f099c6cac649c3" + integrity sha512-IlLKErqCzQRBUcCvXGPowcczbWcvJtEG006gPsAoePNJEXCHEWoKASghgu+L/bqD7006Z6mW6zlTNjcSQJvFAg== + dependencies: + "@coinbase/wallet-sdk" "^3.6.6" + "@ledgerhq/connect-kit-loader" "^1.1.0" + "@safe-global/safe-apps-provider" "^0.17.1" + "@safe-global/safe-apps-sdk" "^8.0.0" + "@walletconnect/ethereum-provider" "2.10.1" + "@walletconnect/legacy-provider" "^2.0.0" + "@walletconnect/modal" "2.6.2" + "@walletconnect/utils" "2.10.1" + abitype "0.8.7" + eventemitter3 "^4.0.7" + "@wagmi/core@0.10.1": version "0.10.1" resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-0.10.1.tgz#a1d676075c76790403de16c39f8df413a275ec37" @@ -5206,6 +5513,38 @@ eventemitter3 "^4.0.7" zustand "^4.3.1" +"@wagmi/core@^1.1.0": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-1.4.3.tgz#1dfe1492b3cc1579d18fa07c88a9cfb82111ffcc" + integrity sha512-CIV9jwv5ue+WpqmA3FvwGa+23cppe7oIaz6TRnlGm0Hm0wDImSaQSWqcsFyOPvleD29oOIJ8e3KnHINEvI64AA== + dependencies: + "@wagmi/connectors" "3.1.2" + abitype "0.8.7" + eventemitter3 "^4.0.7" + zustand "^4.3.1" + +"@walletconnect/core@2.10.1": + version "2.10.1" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.10.1.tgz#d1fb442bd77424666bacdb0f5a07f7708fb3d984" + integrity sha512-WAoXfmj+Zy5q48TnrKUjmHXJCBahzKwbul+noepRZf7JDtUAZ9IOWpUjg+UPRbfK5EiWZ0TF42S6SXidf7EHoQ== + dependencies: + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-provider" "1.0.13" + "@walletconnect/jsonrpc-types" "1.0.3" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.13" + "@walletconnect/keyvaluestorage" "^1.0.2" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/relay-api" "^1.0.9" + "@walletconnect/relay-auth" "^1.0.4" + "@walletconnect/safe-json" "^1.0.2" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.10.1" + "@walletconnect/utils" "2.10.1" + events "^3.3.0" + lodash.isequal "4.5.0" + uint8arrays "^3.1.0" + "@walletconnect/core@2.4.7": version "2.4.7" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.4.7.tgz#f1e2935eded3b882128a7fa6b56eff25221e6f2c" @@ -5278,6 +5617,21 @@ dependencies: tslib "1.14.1" +"@walletconnect/ethereum-provider@2.10.1": + version "2.10.1" + resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.10.1.tgz#4733a98f0b388cf5ae6c2b269f50da87da432ee5" + integrity sha512-Yhoz8EXkKzxOlBT6G+elphqCx/gkH6RxD9/ZAiy9lLc8Ng5p1gvKCVVP5zsGNE9FbkKmHd+J9JJRzn2Bw2yqtQ== + dependencies: + "@walletconnect/jsonrpc-http-connection" "^1.0.7" + "@walletconnect/jsonrpc-provider" "^1.0.13" + "@walletconnect/jsonrpc-types" "^1.0.3" + "@walletconnect/jsonrpc-utils" "^1.0.8" + "@walletconnect/sign-client" "2.10.1" + "@walletconnect/types" "2.10.1" + "@walletconnect/universal-provider" "2.10.1" + "@walletconnect/utils" "2.10.1" + events "^3.3.0" + "@walletconnect/ethereum-provider@^2.4.7": version "2.4.7" resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.4.7.tgz#e863e90dc277b1fbec7de0685eaf28f5fcaeb136" @@ -5419,6 +5773,17 @@ "@walletconnect/jsonrpc-types" "^1.0.2" tslib "1.14.1" +"@walletconnect/jsonrpc-ws-connection@1.0.13": + version "1.0.13" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.13.tgz#23b0cdd899801bfbb44a6556936ec2b93ef2adf4" + integrity sha512-mfOM7uFH4lGtQxG+XklYuFBj6dwVvseTt5/ahOkkmpcAEgz2umuzu7fTR+h5EmjQBdrmYyEBOWADbeaFNxdySg== + dependencies: + "@walletconnect/jsonrpc-utils" "^1.0.6" + "@walletconnect/safe-json" "^1.0.2" + events "^3.3.0" + tslib "1.14.1" + ws "^7.5.1" + "@walletconnect/jsonrpc-ws-connection@^1.0.11": version "1.0.11" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.11.tgz#1ce59d86f273d576ca73385961303ebd44dd923f" @@ -5525,6 +5890,13 @@ buffer "6.0.3" valtio "1.10.6" +"@walletconnect/modal-core@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@walletconnect/modal-core/-/modal-core-2.6.2.tgz#d73e45d96668764e0c8668ea07a45bb8b81119e9" + integrity sha512-cv8ibvdOJQv2B+nyxP9IIFdxvQznMz8OOr/oR/AaUZym4hjXNL/l1a2UlSQBXrVjo3xxbouMxLb3kBsHoYP2CA== + dependencies: + valtio "1.11.2" + "@walletconnect/modal-ui@2.5.9": version "2.5.9" resolved "https://registry.yarnpkg.com/@walletconnect/modal-ui/-/modal-ui-2.5.9.tgz#4d07f1697147ec9f75d85d93f564cadae05a5e59" @@ -5535,6 +5907,24 @@ motion "10.16.2" qrcode "1.5.3" +"@walletconnect/modal-ui@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@walletconnect/modal-ui/-/modal-ui-2.6.2.tgz#fa57c087c57b7f76aaae93deab0f84bb68b59cf9" + integrity sha512-rbdstM1HPGvr7jprQkyPggX7rP4XiCG85ZA+zWBEX0dVQg8PpAgRUqpeub4xQKDgY7pY/xLRXSiCVdWGqvG2HA== + dependencies: + "@walletconnect/modal-core" "2.6.2" + lit "2.8.0" + motion "10.16.2" + qrcode "1.5.3" + +"@walletconnect/modal@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@walletconnect/modal/-/modal-2.6.2.tgz#4b534a836f5039eeb3268b80be7217a94dd12651" + integrity sha512-eFopgKi8AjKf/0U4SemvcYw9zlLpx9njVN8sf6DAkowC2Md0gPU/UNEbH1Wwj407pEKnEds98pKWib1NN1ACoA== + dependencies: + "@walletconnect/modal-core" "2.6.2" + "@walletconnect/modal-ui" "2.6.2" + "@walletconnect/modal@^2.5.5": version "2.5.9" resolved "https://registry.yarnpkg.com/@walletconnect/modal/-/modal-2.5.9.tgz#28840f2a46bcd0a47c5fda60d18a5f1607a92a72" @@ -5587,6 +5977,21 @@ dependencies: tslib "1.14.1" +"@walletconnect/sign-client@2.10.1", "@walletconnect/sign-client@^2.1.3", "@walletconnect/sign-client@^2.1.4": + version "2.10.1" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.10.1.tgz#db60bc9400cd79f0cb2380067343512b21ee4749" + integrity sha512-iG3eJGi1yXeG3xGeVSSMf8wDFyx239B0prLQfy1uYDtYFb2ynnH/09oqAZyKn96W5nfQzUgM2Mz157PVdloH3Q== + dependencies: + "@walletconnect/core" "2.10.1" + "@walletconnect/events" "^1.0.1" + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.10.1" + "@walletconnect/utils" "2.10.1" + events "^3.3.0" + "@walletconnect/sign-client@2.4.7": version "2.4.7" resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.4.7.tgz#d01e645f189726d5f919724a4145cdd16e4c4044" @@ -5626,6 +6031,18 @@ dependencies: tslib "1.14.1" +"@walletconnect/types@2.10.1", "@walletconnect/types@^2.1.4": + version "2.10.1" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.10.1.tgz#1355bce236f3eef575716ea3efe4beed98a873ef" + integrity sha512-7pccAhajQdiH2kYywjE1XI64IqRI+4ioyGy0wvz8d0UFQ/DSG3MLKR8jHf5aTOafQQ/HRLz6xvlzN4a7gIVkUQ== + dependencies: + "@walletconnect/events" "^1.0.1" + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-types" "1.0.3" + "@walletconnect/keyvaluestorage" "^1.0.2" + "@walletconnect/logger" "^2.0.1" + events "^3.3.0" + "@walletconnect/types@2.4.7": version "2.4.7" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.4.7.tgz#9f102b444631149b2cb0d264830860dc5e211dc0" @@ -5650,6 +6067,21 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" +"@walletconnect/universal-provider@2.10.1": + version "2.10.1" + resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.10.1.tgz#c4a77bd2eed1a335edae5b2b298636092fff63ef" + integrity sha512-81QxTH/X4dRoYCz0U9iOrBYOcj7N897ONcB57wsGhEkV7Rc9htmWJq2CzeOuxvVZ+pNZkE+/aw9LrhizO1Ltxg== + dependencies: + "@walletconnect/jsonrpc-http-connection" "^1.0.7" + "@walletconnect/jsonrpc-provider" "1.0.13" + "@walletconnect/jsonrpc-types" "^1.0.2" + "@walletconnect/jsonrpc-utils" "^1.0.7" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/sign-client" "2.10.1" + "@walletconnect/types" "2.10.1" + "@walletconnect/utils" "2.10.1" + events "^3.3.0" + "@walletconnect/universal-provider@2.4.7": version "2.4.7" resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.4.7.tgz#20b960cc1456a7a1cc77c173c9d38a145ed2ea02" @@ -5682,6 +6114,26 @@ "@walletconnect/utils" "2.8.6" events "^3.3.0" +"@walletconnect/utils@2.10.1", "@walletconnect/utils@^2.1.3": + version "2.10.1" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.10.1.tgz#65b37c9800eb0e80a08385b6987471fb46e1e22e" + integrity sha512-DM0dKgm9O58l7VqJEyV2OVv16XRePhDAReI23let6WdW1dSpw/Y/A89Lp99ZJOjLm2FxyblMRF3YRaZtHwBffw== + dependencies: + "@stablelib/chacha20poly1305" "1.0.1" + "@stablelib/hkdf" "1.0.1" + "@stablelib/random" "^1.0.2" + "@stablelib/sha256" "1.0.1" + "@stablelib/x25519" "^1.0.3" + "@walletconnect/relay-api" "^1.0.9" + "@walletconnect/safe-json" "^1.0.2" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.10.1" + "@walletconnect/window-getters" "^1.0.1" + "@walletconnect/window-metadata" "^1.0.1" + detect-browser "5.3.0" + query-string "7.1.3" + uint8arrays "^3.1.0" + "@walletconnect/utils@2.4.7": version "2.4.7" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.4.7.tgz#f9589f3181f5dc3fd3d4e2cb4c41a08af42e2aae" @@ -5746,6 +6198,19 @@ buffer "6.0.3" valtio "1.9.0" +"@web3modal/core@2.4.3": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@web3modal/core/-/core-2.4.3.tgz#ea6d3911e52a132c70defb7584f869d09a8af974" + integrity sha512-7Z/sDe9RIYQ2k9ITcxgEa/u7FvlI76vcVVZn9UY4ISivefqrH4JAS3GX4JmVNUUlovwuiZdyqBv4llAQOMK6Rg== + dependencies: + buffer "6.0.3" + valtio "1.10.5" + +"@web3modal/ethereum@^2.4.2": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@web3modal/ethereum/-/ethereum-2.7.1.tgz#464dbc1d00d075c16961b77e9a353b1966538653" + integrity sha512-1x3qhYh9qgtvw1MDQD4VeDf2ZOsVANKRPtUty4lF+N4L8xnAIwvNKUAMA4j6T5xSsjqUfq5Tdy5mYsNxLmsWMA== + "@web3modal/standalone@2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@web3modal/standalone/-/standalone-2.1.1.tgz#e496e54af5ecf6e282ff7f287eebce7f1ac90bd2" @@ -5754,6 +6219,14 @@ "@web3modal/core" "2.1.1" "@web3modal/ui" "2.1.1" +"@web3modal/standalone@^2.4.1": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@web3modal/standalone/-/standalone-2.4.3.tgz#98aaa65eba725c34d5be9078ef04b4e9b769d0f3" + integrity sha512-5ATXBoa4GGm+TIUSsKWsfWCJunv1XevOizpgTFhqyeGgRDmWhqsz9UIPzH/1mk+g0iJ/xqMKs5F6v9D2QeKxag== + dependencies: + "@web3modal/core" "2.4.3" + "@web3modal/ui" "2.4.3" + "@web3modal/ui@2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@web3modal/ui/-/ui-2.1.1.tgz#300dceeee8a54be70aad74fb4a781ac22439eded" @@ -5764,6 +6237,16 @@ motion "10.15.5" qrcode "1.5.1" +"@web3modal/ui@2.4.3": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@web3modal/ui/-/ui-2.4.3.tgz#986c6bed528dccab679c734ff531e42f6605c5b2" + integrity sha512-J989p8CdtEhI9gZHf/rZ/WFqYlrAHWw9GmAhFoiNODwjAp0BoG/uoaPiijJMchXdngihZOjLGCQwDXU16DHiKg== + dependencies: + "@web3modal/core" "2.4.3" + lit "2.7.5" + motion "10.16.2" + qrcode "1.5.3" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -5970,6 +6453,16 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== +abitype@0.8.7: + version "0.8.7" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.8.7.tgz#e4b3f051febd08111f486c0cc6a98fa72d033622" + integrity sha512-wQ7hV8Yg/yKmGyFpqrNZufCxbszDe5es4AZGYPBitocfSqXtjrTG9JMWFcc4N30ukl2ve48aBTwt7NJxVQdU3w== + +abitype@0.9.8: + version "0.9.8" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" + integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== + abitype@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.3.0.tgz#75150e337d88cc0b2423ed0d3fc36935f139d04c" @@ -12348,6 +12841,13 @@ lit-html@^2.7.0: dependencies: "@types/trusted-types" "^2.0.2" +lit-html@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.8.0.tgz#96456a4bb4ee717b9a7d2f94562a16509d39bffa" + integrity sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q== + dependencies: + "@types/trusted-types" "^2.0.2" + lit@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/lit/-/lit-2.6.1.tgz#5951a2098b9bde5b328c73b55c15fdc0eefd96d7" @@ -12366,6 +12866,15 @@ lit@2.7.5: lit-element "^3.3.0" lit-html "^2.7.0" +lit@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/lit/-/lit-2.8.0.tgz#4d838ae03059bf9cafa06e5c61d8acc0081e974e" + integrity sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA== + dependencies: + "@lit/reactive-element" "^1.6.0" + lit-element "^3.3.0" + lit-html "^2.8.0" + loader-runner@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" @@ -14294,6 +14803,11 @@ postcss@^8.3.5, postcss@^8.4.4, postcss@^8.4.6, postcss@^8.4.7: picocolors "^1.0.0" source-map-js "^1.0.2" +preact@^10.11.3: + version "10.18.1" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.18.1.tgz#3b84bb305f0b05f4ad5784b981d15fcec4e105da" + integrity sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg== + preact@^10.12.0: version "10.13.0" resolved "https://registry.yarnpkg.com/preact/-/preact-10.13.0.tgz#f8bd3cf257a4dbe41da71a52131b79916d4ca89d" @@ -15445,7 +15959,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^6.4.0, rxjs@^6.6.3: +rxjs@6, rxjs@^6.4.0, rxjs@^6.6.3: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -17138,6 +17652,14 @@ validator@^13.7.0: resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== +valtio@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.10.5.tgz#7852125e3b774b522827d96bd9c76d285c518678" + integrity sha512-jTp0k63VXf4r5hPoaC6a6LCG4POkVSh629WLi1+d5PlajLsbynTMd7qAgEiOSPxzoX5iNvbN7iZ/k/g29wrNiQ== + dependencies: + proxy-compare "2.5.1" + use-sync-external-store "1.2.0" + valtio@1.10.6: version "1.10.6" resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.10.6.tgz#80ed00198b949939863a0fa56ae687abb417fc4f" @@ -17146,6 +17668,14 @@ valtio@1.10.6: proxy-compare "2.5.1" use-sync-external-store "1.2.0" +valtio@1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.11.2.tgz#b8049c02dfe65620635d23ebae9121a741bb6530" + integrity sha512-1XfIxnUXzyswPAPXo1P3Pdx2mq/pIqZICkWN60Hby0d9Iqb+MEIpqgYVlbflvHdrp2YR/q3jyKWRPJJ100yxaw== + dependencies: + proxy-compare "2.5.1" + use-sync-external-store "1.2.0" + valtio@1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.9.0.tgz#d5d9f664319eaf18dd98f758d50495eca28eb0b8" @@ -17183,6 +17713,36 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +viem@^0.3.50: + version "0.3.50" + resolved "https://registry.yarnpkg.com/viem/-/viem-0.3.50.tgz#999a7682eda7eabc48c923f4b9923c3f098fc1ab" + integrity sha512-s+LxCYZTR9F/qPk1/n1YDVAX9vSeVz7GraqBZWGrDuenCJxo9ArCoIceJ6ksI0WwSeNzcZ0VVbD/kWRzTxkipw== + dependencies: + "@adraffy/ens-normalize" "1.9.0" + "@noble/curves" "1.0.0" + "@noble/hashes" "1.3.0" + "@scure/bip32" "1.3.0" + "@scure/bip39" "1.2.0" + "@wagmi/chains" "1.0.0" + abitype "0.8.7" + isomorphic-ws "5.0.0" + ws "8.12.0" + +viem@^1.0.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/viem/-/viem-1.15.0.tgz#b7e863b17c25ccb3169fdeb4b2d7b3b90d5b80a6" + integrity sha512-dVbwqkizIE5Nep01HlckwRZRFLr8LLC8iaqs3efpzT/njssNJ7DvsS/QIgiuUBhTT1NWgceten+oQa2gyOcl4g== + dependencies: + "@adraffy/ens-normalize" "1.9.4" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@scure/bip32" "1.3.2" + "@scure/bip39" "1.2.1" + "@types/ws" "^8.5.5" + abitype "0.9.8" + isomorphic-ws "5.0.0" + ws "8.13.0" + void-elements@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" @@ -18212,6 +18772,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.12.0, ws@^8.5.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" + integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== + ws@8.13.0, ws@^8.12.0, ws@^8.13.0: version "8.13.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" @@ -18241,11 +18806,6 @@ ws@^7.4.5, ws@^7.5.1: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@^8.5.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" - integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== - xhr-request-promise@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/xhr-request-promise/-/xhr-request-promise-0.1.3.tgz#2d5f4b16d8c6c893be97f1a62b0ed4cf3ca5f96c" @@ -18455,10 +19015,10 @@ 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@5.3.0-beta6: - version "5.3.0-beta6" - resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.3.0-beta6.tgz#3baf6568cbeebac84ab5f03c9225ecf66fbb32fb" - integrity sha512-y1pV0doNriqKT+C5+VV6x9QOglWwYjvBJkqpjdIoaFMUXhk4JwAHEDl47IOHaKIweG6nuL21Nj9nyMZUX+pXhg== +zkbob-client-js@5.3.0-beta7: + version "5.3.0-beta7" + resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.3.0-beta7.tgz#3f12398eceff818a55294798869885e7c93bfd7b" + integrity sha512-Lk6IhJzmu8vt90dwQiwCT1aV67rddjEHRdTma5BqaTmGSGlpD9/duyRq1YLI5YAku3AFMhfXAVtvnAO26r1dJw== dependencies: "@ethereumjs/util" "^8.0.2" "@graphprotocol/client-cli" "3.0.0" From 4099562b592633362d9ba4b1f5fbf7ebd4ae6a03 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Tue, 10 Oct 2023 16:31:05 +0200 Subject: [PATCH 03/45] set pool via params in the url --- src/contexts/PoolContext/index.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/contexts/PoolContext/index.js b/src/contexts/PoolContext/index.js index c5e60151..e5f01013 100644 --- a/src/contexts/PoolContext/index.js +++ b/src/contexts/PoolContext/index.js @@ -1,5 +1,6 @@ -import React, { createContext, useState, useCallback } from 'react'; +import React, { createContext, useState, useEffect } from 'react'; import * as Sentry from '@sentry/react'; +import { useLocation } from 'react-router-dom'; import config from 'config'; @@ -8,19 +9,31 @@ const PoolContext = createContext({ currentPool: null }); export default PoolContext; export const PoolContextProvider = ({ children }) => { + const location = useLocation(); + const [currentPool, setPool] = useState(() => { const savedPoolAlias = window.localStorage.getItem('pool'); const alias = config.pools[savedPoolAlias] ? savedPoolAlias : config.defaultPool; return { ...config.pools[alias], alias }; }); - const setCurrentPool = useCallback(alias => { + const setCurrentPool = alias => { setPool({ ...config.pools[alias], alias }); localStorage.setItem('pool', alias); Sentry.configureScope(scope => { scope.setTag('pool_id', alias); }); - }, []); + }; + + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const poolInParams = queryParams.get('pool'); + const alias = Object.keys(config.pools).find(alias => + alias.toLowerCase() === poolInParams?.toLowerCase() + ); + if (!alias) return; + setCurrentPool(alias); + }, [location.search]); return ( From 8affb07dc68dcdd90fdaa159f6452a0c801b4c86 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 12 Oct 2023 14:36:50 +0200 Subject: [PATCH 04/45] remove duplicate data with address prefixes --- src/pages/Transfer/MultiTransfer/index.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/pages/Transfer/MultiTransfer/index.js b/src/pages/Transfer/MultiTransfer/index.js index 2580795f..1e5e1ef3 100644 --- a/src/pages/Transfer/MultiTransfer/index.js +++ b/src/pages/Transfer/MultiTransfer/index.js @@ -20,17 +20,6 @@ import { PoolContext, ZkAccountContext } from 'contexts'; import { formatNumber } from 'utils'; import { useFee } from 'hooks'; -const prefixes = { - 'BOB2USDC-polygon': 'zkbob_polygon', - 'BOB2USDC-optimism': 'zkbob_optimism', - 'BOB-sepolia': 'zkbob_sepolia', - 'BOB2USDC-goerli': 'zkbob_goerli', - 'USDC-goerli': 'zkbob_goerli_usdc', - 'BOB-op-goerli': 'zkbob_goerli_optimism', - 'WETH-goerli': 'zkbob_goerli_eth', - 'WETH-optimism': 'zkbob_optimism_eth', -}; - export default forwardRef((props, ref) => { const { t } = useTranslation(); const { @@ -150,7 +139,7 @@ export default forwardRef((props, ref) => { From 097296de1292b0b09ca3bacf1a92608084f9c878 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 16 Oct 2023 17:01:09 +0200 Subject: [PATCH 05/45] load translations in another way --- src/i18n.js | 10 ++++++++-- .../en/translation.json => src/translations/en.json | 0 .../pt/translation.json => src/translations/pt.json | 0 3 files changed, 8 insertions(+), 2 deletions(-) rename public/locales/en/translation.json => src/translations/en.json (100%) rename public/locales/pt/translation.json => src/translations/pt.json (100%) diff --git a/src/i18n.js b/src/i18n.js index 283182e0..4c6a5cc6 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,15 +1,21 @@ import i18n from 'i18next'; -import Backend from 'i18next-http-backend'; import LanguageDetector from 'i18next-browser-languagedetector'; import { initReactI18next } from 'react-i18next'; i18n - .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ fallbackLng: ['en', 'pt'], nonExplicitSupportedLngs: true, + resources: { + en: { + translation: require('translations/en.json'), + }, + pt: { + translation: require('translations/pt.json'), + }, + }, }); export default i18n; diff --git a/public/locales/en/translation.json b/src/translations/en.json similarity index 100% rename from public/locales/en/translation.json rename to src/translations/en.json diff --git a/public/locales/pt/translation.json b/src/translations/pt.json similarity index 100% rename from public/locales/pt/translation.json rename to src/translations/pt.json From 30988aff6348c76c5117714ae8024778cf9f1f0e Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Tue, 17 Oct 2023 18:56:41 +0200 Subject: [PATCH 06/45] show all wallets (tron + evm) when restoring account --- .../AccountSetUpModal/Generate/index.js | 25 +++------------ src/components/AccountSetUpModal/index.js | 25 ++++++++++++--- src/components/WalletConnectors/index.js | 31 +++++++++---------- src/contexts/WalletContext/index.js | 3 +- src/translations/en.json | 4 +-- src/translations/pt.json | 4 +-- 6 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/components/AccountSetUpModal/Generate/index.js b/src/components/AccountSetUpModal/Generate/index.js index 93fe4f73..c1571304 100644 --- a/src/components/AccountSetUpModal/Generate/index.js +++ b/src/components/AccountSetUpModal/Generate/index.js @@ -1,36 +1,20 @@ -import React, { useContext, useCallback } from 'react'; +import React, { useContext } from 'react'; import styled from 'styled-components'; -import { useTranslation, Trans } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import WalletConnectors from 'components/WalletConnectors'; -import Button from 'components/Button'; import { PoolContext } from 'contexts'; -import config from 'config'; - -const poolsWithAliases = Object.values(config.pools).map((pool, index) => ({ - ...pool, - alias: Object.keys(config.pools)[index], -})); - export default ({ next, isCreation }) => { const { t } = useTranslation(); - const { currentPool, setCurrentPool } = useContext(PoolContext); - - const switchPool = useCallback(() => { - const pool = poolsWithAliases.find(pool => pool.isTron !== currentPool.isTron); - setCurrentPool(pool.alias); - }, [setCurrentPool, currentPool.isTron]); + const { currentPool } = useContext(PoolContext); return ( {!isCreation && ( - }} - /> + {t(`accountSetupModal.restoreWithWallet.warning${currentPool.isTron ? '_tron' : ''}`)} )} @@ -42,6 +26,7 @@ export default ({ next, isCreation }) => { ); diff --git a/src/components/AccountSetUpModal/index.js b/src/components/AccountSetUpModal/index.js index 4fc448d5..71f5a95e 100644 --- a/src/components/AccountSetUpModal/index.js +++ b/src/components/AccountSetUpModal/index.js @@ -102,10 +102,11 @@ const PasswordPrompt = ({ setStep, close }) => { export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) => { const { t } = useTranslation(); - const { signMessage } = useContext(WalletContext); + const { tronWallet, evmWallet } = useContext(WalletContext); const [step, setStep] = useState(STEP.START); const [newMnemonic, setNewMnemonic] = useState(); const [confirmedMnemonic, setConfirmedMnemonic] = useState(); + const [isTronWalletSelected, setIsTronWalletSelected] = useState(false); const closeModal = useCallback(() => { setStep(STEP.START); @@ -133,6 +134,7 @@ export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) }, []); const generate = useCallback(async () => { + const { signMessage } = isTronWalletSelected ? tronWallet : evmWallet; const message = 'Access zkBob account.\n\nOnly sign this message for a trusted client!'; let signedMessage = await signMessage(message); if (!window.location.host.includes(process.env.REACT_APP_LEGACY_SIGNATURE_DOMAIN)) { @@ -147,7 +149,7 @@ export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) const newMnemonic = ethers.utils.entropyToMnemonic(md5.array(signedMessage)); setConfirmedMnemonic(newMnemonic); setStep(STEP.CREATE_PASSWORD_PROMPT); - }, [signMessage]); + }, [isTronWalletSelected, tronWallet, evmWallet]); const confirmPassword = useCallback(password => { const isNewAccount = !!newMnemonic; @@ -187,7 +189,15 @@ export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) break; case STEP.CREATE_WITH_WALLET: title = t('accountSetupModal.createWithWallet.title'); - component = setStep(STEP.SING_MESSAGE_TO_CREATE)} />; + component = ( + { + setIsTronWalletSelected(connector.isTron); + setStep(STEP.SING_MESSAGE_TO_CREATE); + }} + /> + ); prevStep = STEP.CREATE_OPTIONS; break; case STEP.CREATE_WITH_SECRET: @@ -197,7 +207,14 @@ export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) break; case STEP.RESTORE_WITH_WALLET: title = t('accountSetupModal.restoreWithWallet.title'); - component = setStep(STEP.SING_MESSAGE_TO_RESTORE)} />; + component = ( + { + setIsTronWalletSelected(connector.isTron); + setStep(STEP.SING_MESSAGE_TO_RESTORE); + }} + /> + ); prevStep = STEP.RESTORE_OPTIONS; break; case STEP.RESTORE_WITH_SECRET: diff --git a/src/components/WalletConnectors/index.js b/src/components/WalletConnectors/index.js index ba16f0f8..c4e8e8cf 100644 --- a/src/components/WalletConnectors/index.js +++ b/src/components/WalletConnectors/index.js @@ -1,29 +1,28 @@ -import React, { useCallback, useContext } from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; import styled from 'styled-components'; import { WalletContext } from 'contexts'; import { CONNECTORS_ICONS } from 'constants'; -const getConnectorName = connector => { - if (connector.name === 'WalletConnectLegacy') return 'WalletConnect'; - // if (connector.name === 'WalletConnect') return 'WalletConnect v2'; - return connector.name; -} +export default ({ callback, gaIdPrefix = '', showAll = false }) => { + const { isTron, evmWallet, tronWallet } = useContext(WalletContext); -export default ({ callback, gaIdPrefix = '' }) => { - const { - connector: activeConnector, connect, - disconnect, connectors, isTron, - } = useContext(WalletContext); + const connectors = useMemo(() => { + if (showAll && isTron) return [...tronWallet.connectors, ...evmWallet.connectors]; + if (showAll) return [...evmWallet.connectors, ...tronWallet.connectors]; + if (isTron) return tronWallet.connectors; + return evmWallet.connectors; + }, [showAll, isTron, evmWallet, tronWallet]); const connectWallet = useCallback(async connector => { - if (connector.id === activeConnector?.id && !isTron) { - await disconnect(); + if (!connector.isTron && connector.id === evmWallet.connector?.id) { + await evmWallet.disconnect(); } + const { connect } = connector.isTron ? tronWallet : evmWallet; await connect({ connector }); - callback?.(); - }, [connect, disconnect, activeConnector, callback, isTron]); + callback?.(connector); + }, [callback, evmWallet, tronWallet]); return ( <> @@ -33,7 +32,7 @@ export default ({ callback, gaIdPrefix = '' }) => { onClick={() => connectWallet(connector)} data-ga-id={gaIdPrefix + connector.name} > - {getConnectorName(connector)} + {connector.name} )} diff --git a/src/contexts/WalletContext/index.js b/src/contexts/WalletContext/index.js index 1a7197d7..54db8722 100644 --- a/src/contexts/WalletContext/index.js +++ b/src/contexts/WalletContext/index.js @@ -70,6 +70,7 @@ const convertWalletToConnector = wallet => ({ ...wallet.adapter, id: wallet.adapter.name.toLowerCase(), ready: !['Loading', 'NotFound'].includes(wallet.state), + isTron: true, }); const useTronWallet = pool => { @@ -170,7 +171,7 @@ export const WalletContextProvider = ({ children }) => { const wallet = currentPool.isTron ? tronWallet : evmWallet; return ( - + {children} ); diff --git a/src/translations/en.json b/src/translations/en.json index 709ab309..6bcee91f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -224,8 +224,8 @@ "restoreWithWallet": { "title": "Login to your zkAccount", "description": "Select the wallet you used to create your zkAccount", - "warning": "Wallets are shown for the current selected pool. If you want to use Tron with the Tronlink wallet, please <1>switch the pool here.", - "warning_tron": "Wallets are shown for the current selected pool. If you want to use Polygon, Optimism or Ethereum, please <1>switch the pool here." + "warning": "TronLink wallet will be used only for restoring access to your previous zkBob account", + "warning_tron": "MetaMask and Wallet Connect wallets will be used only for restoring access to your previous zkBob account" }, "restoreWithSecret": { "title": "Restore zkAccount", diff --git a/src/translations/pt.json b/src/translations/pt.json index 9fcc4840..0f1f6061 100644 --- a/src/translations/pt.json +++ b/src/translations/pt.json @@ -224,8 +224,8 @@ "restoreWithWallet": { "title": "Acessar sua conta zkAccount", "description": "Selecione a carteira que você usou para criar sua conta zkAccount", - "warning": "As carteiras são exibidas para a pool selecionada atualmente. Se você deseja usar o Tron com a carteira Tronlink, por favor <1>troque a pool aqui.", - "warning_tron": "As carteiras são exibidas para a pool selecionada atualmente. Se você deseja usar Polygon, Optimism ou Ethereum, por favor <1>troque a pool aqui." + "warning": "A carteira TronLink será usada apenas para restaurar o acesso à sua conta anterior zkBob", + "warning_tron": "As carteiras MetaMask e Wallet Connect serão usadas apenas para restaurar o acesso à sua conta anterior zkBob" }, "restoreWithSecret": { "title": "Restaurar conta zkAccount", From caa365f269363eee842687a645a57c57e7db1eaf Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Tue, 17 Oct 2023 19:47:37 +0200 Subject: [PATCH 07/45] hot fix for token list --- src/utils/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/index.js b/src/utils/index.js index 607540e5..4c56259b 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -14,7 +14,7 @@ export const shortAddress = (string, length = 10) => export const formatNumber = (wei, tokenDecimals, customNumberDecimals) => { if (wei.isZero()) return '0'; - if (wei.lte(parseUnits('0.0001', tokenDecimals))) return '≈ 0'; + if (tokenDecimals > 4 && wei.lte(parseUnits('0.0001', tokenDecimals))) return '≈ 0'; const numberDecimals = typeof customNumberDecimals === 'number' ? customNumberDecimals From 391f466d51250c0c8e4070c5efcc72a1a1beca8e Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 23 Oct 2023 14:15:23 +0200 Subject: [PATCH 08/45] fix message signing --- src/contexts/WalletContext/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contexts/WalletContext/index.js b/src/contexts/WalletContext/index.js index 54db8722..051b5367 100644 --- a/src/contexts/WalletContext/index.js +++ b/src/contexts/WalletContext/index.js @@ -55,8 +55,8 @@ const useEvmWallet = pool => { disconnect: disconnectAsync, sign: message => signMessageAsync({ message }), signMessage: message => signMessageAsync({ message }), - signTypedData: signer?._signTypedData, - sendTransaction: signer?.sendTransaction, + signTypedData: (domain, types, message) => signer?._signTypedData(domain, types, message), + sendTransaction: ({ to, value, data }) => signer?.sendTransaction({ to, value, data }), switchNetwork: switchNetworkAsync, getBalance, callContract, From 7a54288b4f47abba47c6812a25ffc2ab61e3eef4 Mon Sep 17 00:00:00 2001 From: vladimir Date: Mon, 23 Oct 2023 17:26:45 +0400 Subject: [PATCH 09/45] adds license files --- LICENSE_APACHE | 176 +++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE_MIT | 21 ++++++ 2 files changed, 197 insertions(+) create mode 100644 LICENSE_APACHE create mode 100644 LICENSE_MIT diff --git a/LICENSE_APACHE b/LICENSE_APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/LICENSE_APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE_MIT b/LICENSE_MIT new file mode 100644 index 00000000..82e8ac92 --- /dev/null +++ b/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 ZeroPool + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 56e659131d57aecf496e8822b44b5f9656fdccf1 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 23 Oct 2023 18:02:50 +0200 Subject: [PATCH 10/45] optional proxy and different params --- src/config/index.js | 7 +++++-- src/pages/index.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index ffad3cd7..3bd98960 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -75,9 +75,12 @@ const config = { rpcUrls: ['https://rpc.ankr.com/optimism'], }, }, - snarkParams: { + snarkParams: process.env.REACT_APP_HOSTING === 'netlify' ? { transferParamsUrl: 'https://r2.zkbob.com/transfer_params_22022023.bin', - transferVkUrl: 'https://r2.zkbob.com/transfer_verification_key_22022023.json' + transferVkUrl: 'https://r2.zkbob.com/transfer_verification_key_22022023.json', + } : { + transferParamsUrl: 'https://5tqpkqtbrkd5ookgni4yydvxgsnaazxl53pdgymjgkiaqwb56lzq.arweave.net/7OD1QmGKh9c5Rmo5jA63NJoAZuvu3jNhiTKQCFg98vM', + transferVkUrl: 'https://rhm3gvehfvhrnll2cuuem2s77hruahjgifqctaw7ld2z37ehpcta.arweave.net/idmzVIctTxatehUoRmpf-eNAHSZBYCmC31j1nfyHeKY', }, }, dev: { diff --git a/src/pages/index.js b/src/pages/index.js index 27522d6a..bed5254f 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -59,7 +59,7 @@ if (PUBLIC_KEY && PRIVATE_KEY && PROJECT_ID) { Sentry.init({ dsn: sentryDsn, - tunnel: '/telemetry', + tunnel: process.env.REACT_APP_HOSTING === 'netlify' ? '/telemetry' : undefined, integrations: [ new BrowserTracing({ routingInstrumentation: Sentry.reactRouterV5Instrumentation(history), From 324f754298cca127c22a2b7885d189f258f3d14b Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 23 Oct 2023 18:37:17 +0200 Subject: [PATCH 11/45] Update README.md --- README.md | 88 ++++++++++++------------------------------------------- 1 file changed, 19 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 02aac3f6..63c262f6 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,20 @@ -# Getting Started with Create React App +# zkBob UI + +## Local development +Install dependencies and start the development server: +```bash +yarn +yarn start +``` + +### Environment variables +To run this project, you'll need to set up the following environment variables: +``` +REACT_APP_CONFIG= // "prod" or "dev" +REACT_APP_WALLETCONNECT_PROJECT_ID= +``` +You can create a .env file in the root directory and set these variables. + +## License +This project is licensed under the Apache-2.0 and the MIT licenses. -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `yarn start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.\ -You will also see any lint errors in the console. - -### `yarn test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `yarn build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `yarn eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) - -### Analyzing the Bundle Size - -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) - -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `yarn build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) From 3a7b28461221e6976e03275db0936c19560ab3da Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 23 Oct 2023 18:51:58 +0200 Subject: [PATCH 12/45] add Chinese and Russian languages --- src/assets/languages/ru.svg | 5 + src/assets/languages/zh.svg | 8 + src/components/MoreDropdown/index.js | 2 + src/components/Tabs/index.js | 8 +- src/i18n.js | 18 +- src/{translations => locales}/en.json | 0 src/{translations => locales}/pt.json | 0 src/locales/ru.json | 423 ++++++++++++++++++++++++++ src/locales/zh.json | 423 ++++++++++++++++++++++++++ 9 files changed, 875 insertions(+), 12 deletions(-) create mode 100644 src/assets/languages/ru.svg create mode 100644 src/assets/languages/zh.svg rename src/{translations => locales}/en.json (100%) rename src/{translations => locales}/pt.json (100%) create mode 100644 src/locales/ru.json create mode 100644 src/locales/zh.json diff --git a/src/assets/languages/ru.svg b/src/assets/languages/ru.svg new file mode 100644 index 00000000..3cfa65dd --- /dev/null +++ b/src/assets/languages/ru.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/languages/zh.svg b/src/assets/languages/zh.svg new file mode 100644 index 00000000..81bd43c3 --- /dev/null +++ b/src/assets/languages/zh.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/MoreDropdown/index.js b/src/components/MoreDropdown/index.js index 4b8cac5e..a055630e 100644 --- a/src/components/MoreDropdown/index.js +++ b/src/components/MoreDropdown/index.js @@ -19,6 +19,8 @@ const links = [ const languages = { en: 'English', pt: 'Português', + ru: 'Русский', + zh: '中文', }; const Content = ({ close }) => { diff --git a/src/components/Tabs/index.js b/src/components/Tabs/index.js index 429ae818..5e6e165b 100644 --- a/src/components/Tabs/index.js +++ b/src/components/Tabs/index.js @@ -29,9 +29,6 @@ const Tabs = styled.div` display: flex; justify-content: space-between; box-sizing: border-box; - & > * { - flex: 1; - } @media only screen and (max-width: 400px) { width: 100%; } @@ -65,4 +62,9 @@ const Tab = styled.div` height: 6px; border-radius: 50%; background-color: #E53E3E; + } + display: flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; `; diff --git a/src/i18n.js b/src/i18n.js index 4c6a5cc6..7f65e6fa 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -2,20 +2,20 @@ import i18n from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import { initReactI18next } from 'react-i18next'; +const supportedLanguages = ['en', 'pt', 'ru', 'zh']; + i18n .use(LanguageDetector) .use(initReactI18next) .init({ - fallbackLng: ['en', 'pt'], + fallbackLng: supportedLanguages, nonExplicitSupportedLngs: true, - resources: { - en: { - translation: require('translations/en.json'), - }, - pt: { - translation: require('translations/pt.json'), - }, - }, + resources: supportedLanguages.reduce((acc, lang) => { + acc[lang] = { + translation: require(`locales/${lang}.json`), + }; + return acc; + }, {}), }); export default i18n; diff --git a/src/translations/en.json b/src/locales/en.json similarity index 100% rename from src/translations/en.json rename to src/locales/en.json diff --git a/src/translations/pt.json b/src/locales/pt.json similarity index 100% rename from src/translations/pt.json rename to src/locales/pt.json diff --git a/src/locales/ru.json b/src/locales/ru.json new file mode 100644 index 00000000..954af634 --- /dev/null +++ b/src/locales/ru.json @@ -0,0 +1,423 @@ +{ + "common": { + "learnMore": "Узнать больше", + "relayerFee": "Комиссия релеера", + "fee": "Комиссия", + "copied": "Скопировано!", + "balance": "Баланс", + "poolBalance": "Баланс пула", + "supportId": "Идентификатор поддержки", + "web": "Web", + "library": "Библиотека", + "relayer": "Релеер", + "zkAccount": "zkАккаунт", + "from": "От", + "to": "Кому", + "contactSupport": "Связаться с поддержкой", + "wallet": "Кошелёк", + "viewInExplorer": "Посмотреть в эксплорере", + "settings": "Настройки", + "password": "Пароль", + "language": "Язык", + "sender": "Отправитель", + "recipient": "Получатель", + "link": "Ссылка", + "payment": "Платеж" + }, + "buttonText": { + "connectWallet": "Подключить кошелек", + "enterAmount": "Введите сумму", + "enterAddress": "Введите адрес", + "invalidAddress": "Некорректный адрес", + "loading": "Загрузка...", + "loadingZkAccount": "Загрузка zk-Аккаунта...", + "loadingPercentage": "Загрузка ({{percentage}}%)", + "getStarted": "Начать!", + "minAmount": "Минимальная сумма: {{amount}} {{symbol}}", + "insufficientBalance": "Недостаточный {{symbol}} баланс", + "reduceAmount": "Уменьшите сумму, с учетом {{fee}} комиссии", + "amountExceedsLimit": "Сумма превышает дневной лимит", + "approveTokens": "Одобрить токены", + "deposit": "Депозит", + "transfer": "Перевести", + "withdraw": "Вывести", + "proceed": "Продолжить", + "confirm": "Подтвердить", + "send": "Отправить", + "yes": "Да", + "no": "Нет", + "done": "Готово", + "next": "Далее", + "okay": "ОК", + "gotIt": "Понятно", + "max": "Макс", + "changeWallet": "Переключить кошелек", + "disconnect": "Отключиться", + "showSecretPhrase": "Показать секретную фразу", + "setPassword": "Установить пароль", + "disablePassword": "Отключить пароль", + "generateQRCode": "Сгенерировать QR-код адреса", + "generateAddress": "Сгенерировать свой адрес", + "getPaymentLink": "Получить платежную ссылку", + "redeemGiftCard": "Использовать подарочную карту", + "logout": "Выйти", + "continue": "Продолжить", + "signMessage": "Подписать сообщение", + "skip": "Пропустить", + "verify": "Подтвердить", + "restoreAccount": "Восстановить аккаунт", + "signIn": "Войти", + "signingIn": "Вход...", + "getToken": "Получить {{symbol}}" + }, + "deposit": { + "title": "Пополнение", + "note": "{{symbol}} будет внесен на ваш zk-Аккаунт. После получения, вы сможете переводить {{symbol}} конфиденциально.", + "ddNote": "Пополнение {{symbol}} может занять до 10 минут." + }, + "transfer": { + "title": "Перевод", + "note": "Перевод будет выполнен конфиденциально. Отправитель, получатель и сумма никогда не будут раскрыты.", + "addressInputPlaceholder": "Введите адрес получателя zkBob", + "addressInputHint": "Адрес можно сгенерировать в окне аккаунта" + }, + "multitransfer": { + "title": "Мультиперевод", + "instruction": "Добавьте zk-Адрес, сумму {{symbol}} для перевода. По одному адресу на строку.", + "uploadCSV": "Загрузить CSV файл", + "uploadCSVHint": "Щелкните Загрузить CSV файл, чтобы добавить заранее подготовленный файл .csv с вашего устройства. Каждая строка должна содержать: zk-Адрес, сумму", + "errors": { + "syntax": "{{count}} $t(glossary.rows, {\"count\": {{count}} }) с неправильным $t(glossary.addresses, {\"count\": {{count}} }) или форматированием $t(glossary.issues, {\"count\": {{count}} }).", + "duplicates": "Найдены дублирующиеся адреса.", + "insufficientBalance": "Недостаточный баланс: требуется {{amount}} {{symbol}} ({{fee}} комиссия)." + }, + "addressesList": "Список адресов", + "willBeTransferredTo": "Будет переведено на {{count}} zkBob $t(glossary.addresses, {\"count\": {{count}} })", + "hasBeenTransferredTo": "Было переведено на {{count}} zkBob $t(glossary.addresses, {\"count\": {{count}} })" + }, + "withdraw": { + "title": "Вывод", + "note": "{{symbol}} будет выведен из zkBob и зачислен на указанный адрес кошелька.", + "addressInputPlaceholder": "Введите {{network}} адрес получателя", + "convertion": "Конвертировать часть {{from}} в {{to}} при выводе", + "convertionDetails": "Вы получите {{amount1}} {{symbol1}} и ~ {{amount2}} {{symbol2}}" + }, + "limits": { + "deposit": { + "perTransaction": "Лимит на пополнение на одну транзакцию", + "dailyPerAddress": "Лимит на пополнение в день на один адрес", + "daily": "Лимит на пополнение в день для протокола" + }, + "withdraw": { + "daily": "Лимит для вывода средств в день" + }, + "poolSize": "Размера пула", + "total": "из {{amount}} {{symbol}} всего" + }, + "latestAction": { + "deposit": "Последнее пополнение средств", + "withdraw": "Последний вывод средств", + "transfer": "Последний перевод средств", + "viewHistory": "История" + }, + "confirmTransaction": { + "titles": { + "withdraw": "Подтверждение вывода средств", + "transfer": "Подтверждение перевода средств", + "multitransfer": "Подтверждение мультиперевода" + }, + "details": { + "withdraw": "детали вывода средств", + "transfer": "детали перевода средств", + "multitransfer": "детали мультиперевода" + }, + "sendTo": "отправить на", + "sendToZk": "отправить на zkBob адрес", + "sendToMultiple": "$t(multitransfer.willBeTransferredTo)", + "viewAll": "посмотреть все", + "withdrawAmount": "Сумма вывода", + "numberOfTransactions": "Количество транзакций", + "senderAddress": "Адрес отправителя" + }, + "welcome": { + "title": "Добро пожаловать в zkBob!", + "description": "Создайте zk-аккаунт для конфиденциальных переводов токенов вашим друзьям и коллегам.", + "titleDemo": "Добро пожаловать в демо-версию zkBob!", + "descriptionDemo": "Переводите BOB конфиденциально друзьям, используйте для покупки спонсорских товаров или переводите на свой собственный приватный zk-аккаунт.", + "bannerDemo": "Для безопасных депозитов, выводов и полнофункциональной конфиденциальности создайте <1>аккаунт zkBob здесь" + }, + "payment": { + "title": "Введите сумму {{symbol}} и выберите токен", + "amountToSend": "Сумма, которую вы хотите отправить", + "transferAmount": "Сумма перевода", + "recipientReceives": "Получатель получит платеж в {{symbol}}", + "limit": "Максимальная сумма для оплаты с одного адреса" + }, + "tokenList": { + "title": "Список токенов", + "searchToken": "Найти токен", + "noTokensFound": "Токен не найден" + }, + "history": { + "title": "История", + "empty": "Пополните ваш кошелек, сделайте перевод или выведите средств,
чтобы сформировать вашу историю..", + "fee": "(Комиссия {{amount}} {{symbol}})", + "highFee": "Эта транзакция требовала дополнительных операций, что привело к увеличению комиссий.", + "failed": "Неуспешно", + "noDescription": "Нет описания", + "viewTx": "Детали" + }, + "loadingError": { + "titles": { + "prefix": "Ошибка загрузки", + "zkClient": "$t(loadingError.titles.prefix) zk-клиента.", + "zkAccount": "$t(loadingError.titles.prefix) zk-Аккаунта.", + "zkAccountBalance": "$t(loadingError.titles.prefix) баланс zk-Аккаунта.", + "walletBalance": "$t(loadingError.titles.prefix) баланс кошелька.", + "history": "$t(loadingError.titles.prefix) история.", + "limits": "$t(loadingError.titles.prefix) лимиты." + }, + "description": "Пожалуйста, попробуйте позже или<1>свяжитесь с нашей поддержкой, если проблема не исчезнет." + }, + "successNotification": { + "title": "Операция завершена.", + "description": "Проверьте вкладку История или баланс вашего zk-Аккаунта, чтобы получить больше информации." + }, + "pendingAction": { + "title": "Пожалуйста, подождите, пока ваша $t(glossary.transactions, {\"count\": {{count}} })завершится", + "description": "Не обновляйте страницу в течение минимум 30 секунд! Статус транзакции $t(glossary.statuses, {\"count\": {{count}} }) обновится автоматически.", + "note": "Вы можете пополнять аккаунт, переводить или выводить средства после завершения транзакции.", + "note_other": "Вы можете пополнять аккаунт, переводить или выводить средства после завершения транзакций." + }, + "swapModal": { + "title": "Обмен в процессе", + "description": "Вы можете закрыть это окно и вернуться позже, чтобы посмотреть результаты. Вы хотите закрыть окно?" + }, + "accountSetupModal": { + "start": { + "title": "zk-Аккаунт", + "description": "Для начала работы с zkBob вам нужен zk-Аккаунт", + "button1": "Создать новый zk-Аккаунт", + "button2": "У меня уже есть zk-Aккаунт" + }, + "createOptions": { + "title": "Выберите, как вы хотите создать свой аккаунт", + "description": "Создавая zk-Аккаунт, вы соглашаетесь и принимаете <1>Условия использованияzkBob.", + "button1": "Использовать мой Web3 кошелек", + "button2": "Использовать секретную фразу" + }, + "restoreOptions": { + "title": "Как вы создали свой аккаунт?", + "button1": "Я использовал Web3 кошелек", + "button2": "Я использовал секретную фразу zkBob" + }, + "createWithWallet": { + "title": "Создать новый zk-Аккаунт", + "description": "Выберите кошелек, который будет связан с вашим zk-Аккаунтом" + }, + "createWithSecret": { + "title": "Создать новый zk-Аккаунт", + "description": "Запишите или скопируйте эти слова в правильном порядке и сохраните их в надежном месте", + "warning": "Никогда не делитесь своей секретной фразой ни с кем,
храните ее надежно!", + "copy": "Скопировать секретную фразу" + }, + "restoreWithWallet": { + "title": "Вход в ваш zk-Аккаунт", + "description": "Выберите кошелек, который вы использовали для создания своего zk-Аккаунта" + }, + "restoreWithSecret": { + "title": "Восстановить zk-Аккаунт", + "description": "Введите вашу сохраненную секретную фразу, чтобы восстановить существующий zk-Аккаунт" + }, + "confirmSecret": { + "title": "Подтвердите секретную фразу", + "description": "Пожалуйста, введите вашу секретную фразу, чтобы восстановить zk-Аккаунт.
Щелкните по слову, чтобы удалить его" + }, + "signMessageToCreate": { + "title": "Подпишите сообщение для создания вашего zk-Аккаунта", + "description": "Ваш zk-Аккаунт создается на основе подключенного кошелька." + }, + "signMessageToRestore": { + "title": "Подпишите сообщение для входа в ваш zk-Аккаунт", + "description": "Доступ к вашему zk-Аккаунту с использованием подключенного кошелька." + }, + "createPasswordPrompt": { + "title": "Настроить пароль?", + "description": "Вы можете создать надежный пароль, который будет запрашиваться каждый раз при входе в zk-Аккаунт" + }, + "createPassword": { + "title": "Создать пароль", + "description": "Для повышения безопасности, ввод пароля требуется каждый раз при перезагрузке страницы." + } + }, + "password": { + "placeholder1": "Пароль 6+ символов", + "placeholder2": "Подтвердите пароль", + "rule1": "Пожалуйста, введите 6 символов или более", + "rule2": "Пароль должен совпадать" + }, + "enterPasswordModal": { + "title": "Введите пароль", + "description": "$t(accountSetupModal.createPassword.description)", + "lostPassword": "Потеряли пароль? Нажмите здесь, чтобы восстановить доступ" + }, + "setPasswordModal": { + "title": "Установить пароль" + }, + "confirmLogoutModal": { + "title": "Выйти и очистить данные браузера", + "description": "При выходе из аккаунта информация о вашем zk-Аккаунте стирается из кэша данных и больше не доступна с использованием вашего пароля. Для восстановления аккаунта и баланса используйте секретную фразу и новый пароль." + }, + "disablePasswordModal": { + "title": "Отключить пароль", + "description": "Пожалуйста, введите текущий пароль" + }, + "demoCard": { + "title": "Демо версия", + "description1": "Эта версия zkBob разработана только для частных переводов небольших сумм (не для депозитов/выводов).", + "description2": "<0>Для полнофункциональной версии,<1>создайте бесплатный аккаунт zkBob здесь." + }, + "increasedLimitsBanner": { + "inactive": "Хотите увеличить ваши лимиты на пополнения кошелька?", + "active": "Увеличенные лимиты на пополнения кошелька активированы", + "resync": "Для восстановления увеличенных лимитов на пополнение кошелька - <1>повторно синхронизируйте ваш токен BAB" + }, + "increasedLimitsModal": { + "title": "Расширенные лимиты", + "paragraph1": "Для увеличения личных лимитов депозита вам нужно иметь токен BAB на BNB Smart Chain. Что такое токен BAB? <1>Узнайте прямо сейчас.", + "paragraph2": "Верифицируйте свой BAB:", + "step1": "Подключите кошелек Metamask или WalletConnect, содержащий ваш токен BAB, к zkBob. Вам НЕ нужно переключать сети, оставайтесь на Polygon.", + "step2": "Щелкните «Подтвердить мой токен BAB» ниже.", + "paragraph3": "Вот и все. Ваши лимиты депозита автоматически увеличатся, если ваш подключенный кошелек содержит токен BAB.", + "button": "Верифицировать мой токен BAB" + }, + "more": { + "title": "Больше о zkBob", + "dune": "Dune Analytics", + "docs": "Документация", + "linktree": "Linktree" + }, + "networks": { + "title": "Сети" + }, + "pagination": { + "first": "Первая", + "last": "Последняя", + "current": "Страница {{current}} из {{total}}" + }, + "paymentLinkModal": { + "title": "Получить ссылку для платежа", + "description": "Поделитесь этой ссылкой, чтобы получать переводы конфиденциально.
Отправитель сможет выбрать и отправить любой токен. Токены будут конвертированы в {{symbol}} и зачислены на ваш zk-Аккаунт.

Примечание: Ссылка будет работать в той же сети, в которой она была создана.", + "copyAndShare": "Скопировать и поделиться ссылкой", + "getMoreInfo": "Получить больше информации конфиденциальных ссылках" + }, + "reedeemGifCardModal": { + "loading": { + "title": "Подготовка вашей подарочной карты" + }, + "start": { + "title": "Вам повезло!", + "description": "На этой подарочной карте вы найдете", + "button": "Забрать {{symbol}}" + }, + "createAccount": { + "title": "Прежде чем вы сможете воспользоваться
подарочной картой", + "description": "Для использования подарочной карты вам потребуется аккаунт в zkBob! Создайте аккаунт или войдите в существующий аккаунт, чтобы забрать ваши {{symbol}} токены.", + "button": "Войти или создать zk-Аккаунт" + }, + "switchNetwork": { + "title": "Нам нужно переключить вас на другую сеть", + "description": "Подарочная карта доступна только на {{network}}. Для ее использования нам нужно сменить сеть" + }, + "inProgress": { + "title": "Мы переводим токены на ваш zk-Аккаунт", + "description": "Этот процесс занимает больше времени, чем обычно. Если процесс не завершится в течение нескольких секунд, свяжитесь с нашей <1>службой поддержки" + }, + "completed": { + "title": "Ваши {{symbol}} уже в пути", + "description": "Ваши токены будут доставлены в ближайшие 1-2 минуты. Проверьте статус транзакции во вкладке История" + }, + "failed": { + "error": "Эту подарочную карту уже использовали", + "otherError": "Что-то пошло не так", + "description": "Пожалуйста, свяжитесь с нашей службой поддержки, чтобы решить эту проблему. Укажите Идентификатор (ID) поддержки в вашем запросе." + } + }, + "restrictionModal": { + "title": "zkBob не поддерживается в этой стране или регионе", + "description": "zkBob не поддерживается в {{country}}. Вы сможете использовать приложение из поддерживаемой страны или региона." + }, + "secretPhraseModal": { + "title": "Показать секретную фразу", + "warning": "Никогда не делитесь своей секретной фразой с кем-либо,
храните ее в надежном месте!", + "description": "Если кто-то получит вашу секретную фразу, у него будет полный контроль над вашим кошельком.", + "copy": "Скопировать секретную фразу" + }, + "transactionModal": { + "titles": { + "approveTokens": "Пожалуйста, подтвердите токены", + "approved": "Токены подтверждены", + "signMessage": "Пожалуйста, подпишите сообщение", + "confirmTransaction": "Пожалуйста, подтвердите транзакцию", + "waitingForTransaction": "Ожидание транзакции", + "generatingProof": "Генерация доказательства", + "waitingForRelayer": "Ожидание релеера", + "deposited": "Пополнение кошелька в процессе", + "transferred": "ОТправка средств в процессе", + "transferredMulti": "Мультитрансфер в процессе", + "withdrawn": "Вывод средст в процессе", + "rejected": "Транзакция отклонена", + "signatureExpired": "Срок подписи истек", + "suspiciousAccountDeposit": "Подключен подозрительный кошелек", + "suspiciousAccountWithdrawal": "Подозрительный адрес получателя", + "wrongNetwork": "Неверная сеть", + "switchNetwork": "Пожалуйста, переключитесь на другую сеть", + "sent": "Ваш платеж отправлен", + "preparingTransaction": "Подготовка транзакции" + }, + "descriptions": { + "deposited": "Пополнение кошелька на сумму {{amount}} {{symbol}} находится в процессе.

Чтобы повысить уровень анонимности, вы можете оставить токены в кошельке на некоторое время перед выводом.", + "transferred": "Ваш перевод на сумму {{amount}} {{symbol}} находится в процессе.", + "transferredMulti": "Ваш мультитрансфер на сумму {{amount}} {{symbol}} находится в процессе.", + "withdrawn": "Вывод средств на сумму {{amount}} {{symbol}} находится в процессе.", + "signatureExpired": "Ваша подпись истекла. Пожалуйста, попробуйте еще раз.", + "suspiciousAccountDeposit": "Мы обнаружили, что ваш кошелек был связан с подозрительными действиями. Из-за этого вы не можете использовать этот кошелек в zkBob. Пожалуйста, попробуйте другой кошелек.", + "suspiciousAccountWithdrawal": "Мы обнаружили, что адрес получателя был связан с подозрительными действиями. Из-за этого вы не можете выводить средства на этот адрес.", + "wrongNetwork": "Не удалось переключить сеть. Пожалуйста, подключитесь к сети {{network}} и попробуйте снова.", + "approved": "Ваше подтверждение прошло успешно. Теперь вы можете пополнить ваш кошелек.", + "sent": "Обработка платежа может занять до 10 минут.
<1>Просмотреть транзакцию

или скачать
<2><3>подтверждение транзакции", + "signMessage": "Вам необходимо подписать сообщение, разрешая контракту использовать ваши токены для пополнения средств." + } + }, + "maxButton": { + "tooltip": "Нажмите Макс, чтобы установить максимальную сумму {{symbol}}, которую вы можете отправить, включая все комиссии и ограничения." + }, + "connectWalletModal": { + "title": "Подключить web3 кошелек", + "description": "Подключите свой кошелек, чтобы внести {{symbol}} на свой zk-Аккаунт. Если вы создаете новый zk-Аккаунт, ваш кошелек используется для создания закрытого ключа шифрования для приложения zkBob.", + "note": "Подключая кошелек, вы соглашаетесь с
<1>условиями обслуживания zkBob" + }, + "qrCode": { + "title": "QR-код адреса", + "description": "Чтобы получить конфиденциальный перевод с другого zk-Аккаунта, ваш друг может отсканировать этот код из zkBob приложения.
Этому пользователю просто нужно отсканировать ваш QR-код на странице Перевод." + }, + "zkAccount": { + "addressDescription": "Используйте этот адрес для получения токенов на свой аккаунт zkBob. Мы создаем новый zk-адрес каждый раз. Вы можете получать токены на этот адрес или ранее созданные адреса." + }, + "paymentStatement": { + "amount": "Сумма в {{symbol}}", + "fee": "Комиссия в {{symbol}}" + }, + "glossary": { + "rows": "строка", + "rows_other": "строки", + "addresses": "адрес", + "addresses_other": "адреса", + "issues": "проблема", + "issues_other": "проблемы", + "transactions": "транзакция", + "transactions_other": "транзакции", + "statuses": "статус", + "statuses_other": "статусы" + } +} diff --git a/src/locales/zh.json b/src/locales/zh.json new file mode 100644 index 00000000..476ea792 --- /dev/null +++ b/src/locales/zh.json @@ -0,0 +1,423 @@ +{ + "common": { + "learnMore": "了解更多", + "relayerFee": "中继器费用", + "fee": "费用", + "copied": "已复制!", + "balance": "余额", + "poolBalance": "资金池余额", + "supportId": "Support ID", + "web": "网页", + "library": "库", + "relayer": "中继器", + "zkAccount": "zk账户", + "from": "发送自", + "to": "发送至", + "contactSupport": "联系客服", + "wallet": "钱包", + "viewInExplorer": "在浏览器中打开", + "settings": "设定", + "password": "密码", + "language": "语言", + "sender": "发送人", + "recipient": "收款人", + "link": "链接", + "payment": "支付" + }, + "buttonText": { + "connectWallet": "连接钱包", + "enterAmount": "输入金额", + "enterAddress": "输入地址", + "invalidAddress": "无效地址", + "loading": "正在载入……", + "loadingZkAccount": "正在载入zk账户……", + "loadingPercentage": "正在载入({{percentage}}%)", + "getStarted": "马上开始!", + "minAmount": "最低金额{{amount}} {{symbol}}", + "insufficientBalance": "{{symbol}}余额不足", + "reduceAmount": "降低金额以覆盖{{fee}}手续费", + "amountExceedsLimit": "金额超出每日限额", + "approveTokens": "允许调用代币", + "deposit": "存入", + "transfer": "转账", + "withdraw": "提现", + "proceed": "继续", + "confirm": "确认", + "send": "发送", + "yes": "是", + "no": "否", + "done": "已完成", + "next": "下一步", + "okay": "Okay", + "gotIt": "收到", + "max": "最大", + "changeWallet": "切换钱包", + "disconnect": "中断连接", + "showSecretPhrase": "显示助记词", + "setPassword": "设置密码", + "disablePassword": "停用密码", + "generateQRCode": "生成二维码地址", + "generateAddress": "生成收款地址", + "getPaymentLink": "生成付款链接", + "redeemGiftCard": "兑换礼品卡", + "logout": "登出", + "continue": "继续", + "signMessage": "签名", + "skip": "跳过", + "verify": "验证", + "restoreAccount": "恢复账号", + "signIn": "登入", + "signingIn": "正在登入……", + "getToken": "获取{{symbol}}" + }, + "deposit": { + "title": "存入", + "note": "{{symbol}}将会被存入您的zk账户。成功收款后,您可以进行{{symbol}}的隐私转账。", + "ddNote": "存入{{symbol}}最多需要10分钟。" + }, + "transfer": { + "title": "转账", + "note": "转账将会以隐私的方式完成。发送人,收款人,及转账金额将不会被公开。", + "addressInputPlaceholder": "请输入zkBob收款人地址", + "addressInputHint": "地址可以在账户模态窗口中生成" + }, + "multitransfer": { + "title": "转账至多个地址", + "instruction": "1 address per row. 输入zk地址,以及需要转账的{{symbol}}数量。每行一个地址。", + "uploadCSV": "上传CSV文件", + "uploadCSVHint": "点击“上传CSV文件”并从设备中添加.csv文件。每行应包括:zk地址,转账金额", + "errors": { + "syntax": "{{count}} $t(glossary.rows, {\"count\": {{count}} })与$t(glossary.addresses, {\"count\": {{count}} })错误,或格式化 $t(glossary.issues, {\"count\": {{count}} })。", + "duplicates": "地址重复。", + "insufficientBalance": "余额不足:需要{{amount}} {{symbol}} ({{fee}} 作为手续费。" + }, + "addressesList": "地址列表", + "willBeTransferredTo": "将发送至 {{count}} zkBob $t(glossary.addresses, {\"count\": {{count}} })", + "hasBeenTransferredTo": "已发送至{{count}} zkBob $t(glossary.addresses, {\"count\": {{count}} })" + }, + "withdraw": { + "title": "提现", + "note": "{{symbol}}将会从zkBob中取出并存入到您提供的钱包地址", + "addressInputPlaceholder": "输入收款人的{{network}}地址", + "convertion": "提款时将一部分 {{from}} 转化为 {{to}}", + "convertionDetails": " 您将收到 {{amount1}} {{symbol1}}~ {{amount2}} {{symbol2}}" + }, + "limits": { + "deposit": { + "perTransaction": "每笔交易存入限额", + "dailyPerAddress": "单个地址每日存入限额", + "daily": "每日存入限额" + }, + "withdraw": { + "daily": "每日提现限额" + }, + "poolSize": "资金池限额", + "total": "总共 {{amount}} {{symbol}}" + }, + "latestAction": { + "deposit": "最近存入", + "withdraw": "最近提款", + "transfer": "最近转账", + "viewHistory": "查看历史记录" + }, + "confirmTransaction": { + "titles": { + "withdraw": "确认提现", + "transfer": "确认转账", + "multitransfer": "确认多地址转账" + }, + "details": { + "withdraw": "提现详情", + "transfer": "转账详情", + "multitransfer": "多地址转账详情" + }, + "sendTo": "发送至", + "sendToZk": "发送至zkBob地址", + "sendToMultiple": "$t(multitransfer.willBeTransferredTo)", + "viewAll": "显示全部", + "withdrawAmount": "提款金额", + "numberOfTransactions": "交易笔数", + "senderAddress": "发送人地址" + }, + "welcome": { + "title": "欢迎来到zkBob!", + "description": "创建专属于您的zk账户,以安全私密的方式转账给您的家人及朋友。", + "titleDemo": "欢迎来到zkBob测试版!", + "descriptionDemo": "以安全私密的方式转账BOB给朋友、购买合作的产品、或转账至您的私人zk账户。", + "bannerDemo": " 在<1>此处创建zkBob帐户, 解锁安全私密的存取款方式,获得全方位隐私" + }, + "payment": { + "title": "输入 {{symbol}} 金额并选择代币种类", + "amountToSend": "需要发送的金额", + "transferAmount": "转账金额", + "recipientReceives": "收款者将收到{{symbol}付款", + "limit": "单个地址最大付款金额" + }, + "tokenList": { + "title": "代币列表", + "searchToken": "搜索代币", + "noTokensFound": "未找到代币" + }, + "history": { + "title": "历史记录", + "empty": "进行第一笔存入,转账,或提款以
解锁历史记录。", + "fee": "(手续费 {{amount}} {{symbol}})", + "highFee": "本次交易需要额外操作,将产生更高的手续费。", + "failed": "失败", + "noDescription": "无说明", + "viewTx": "浏览交易哈希" + }, + "loadingError": { + "titles": { + "prefix": "载入失败", + "zkClient": "zk客户端 $t(loadingError.titles.prefix)。", + "zkAccount": "zk账户 $t(loadingError.titles.prefix)。", + "zkAccountBalance": "zk账户余额 t(loadingError.titles.prefix)。", + "walletBalance": "钱包余额 t(loadingError.titles.prefix)。", + "history": "历史记录 t(loadingError.titles.prefix)。", + "limits": "限额 t(loadingError.titles.prefix)。" + }, + "description": "请重试,若仍存在问题,请<1>联系客服联系客服。" + }, + "successNotification": { + "title": "操作已完成。", + "description": "点击历史记录或您的zk账户余额以获取更多信息。" + }, + "pendingAction": { + "title": "请等待$t(glossary.transactions, {\"count\": {{count}} })完成", + "description": "请至少30秒内不要刷新页面!交易 $t(glossary.statuses, {\"count\": {{count}} }) 将自动更新。", + "note": "交易完成后,您可以存入、转账或提取资金。", + "note_other": "交易完成后,您可以存入、转账或提取资金。" + }, + "swapModal": { + "title": "兑换进行中", + "description": "您可以关闭此窗口并稍后返回页面以查看结果。是否关闭窗口?" + }, + "accountSetupModal": { + "start": { + "title": "zk账户", + "description": "您需要一个zk账户以开始使用zkBob", + "button1": "创建新的zk账户", + "button2": "已有zk账户" + }, + "createOptions": { + "title": "选择您创建账户的方式", + "description": "创建zk账户,即表示您同意并接受zkBob<1>服务条款", + "button1": "使用Web3钱包", + "button2": "使用zkBob助记词" + }, + "restoreOptions": { + "title": "您通过何种方式创建账户?", + "button1": "我用的是Web3钱包", + "button2": "我用的是zkBob助记词" + }, + "createWithWallet": { + "title": "创建新的zk账户", + "description": "选择将与您的zk账户连接的钱包" + }, + "createWithSecret": { + "title": "创建新的zk账户", + "description": "请按正确的顺序抄写或复制助记词,并妥善保存", + "warning": "请勿与任何人分享您的助记词,
请妥善保存!", + "copy": "复制助记词" + }, + "restoreWithWallet": { + "title": "登入zk账户", + "description": "请选择您用于创建zk账户的钱包地址" + }, + "restoreWithSecret": { + "title": "恢复zk账户", + "description": "输入助记词以恢复已有帐户" + }, + "confirmSecret": { + "title": "确认助记词", + "description": "请输入您的助记词并进行验证。
点击某个单词即可将其删除" + }, + "signMessageToCreate": { + "title": "完成签名,创建您的zk账户", + "description": "您正在使用已连接的钱包创建zk账户。" + }, + "signMessageToRestore": { + "title": "完成签名,登入您的zk账户", + "description": "使用已连接的钱包登入您的zk账户。" + }, + "createPasswordPrompt": { + "title": "设置密码?", + "description": "您可以创建安全密码。每当您登录zk账户时,我们都会向您询问该密码。" + }, + "createPassword": { + "title": "创建密码", + "description": "为增强安全性,每次重新加载页面时都需要您重新输入密码。" + } + }, + "password": { + "placeholder1": "密码需6位或以上字符", + "placeholder2": "确认密码", + "rule1": "请输入6位或以上字符", + "rule2": "两次输入的密码需匹配" + }, + "enterPasswordModal": { + "title": "输入密码", + "description": "$t(accountSetupModal.createPassword.description)", + "lostPassword": "忘记密码?点击此处恢复" + }, + "setPasswordModal": { + "title": "设置密码" + }, + "confirmLogoutModal": { + "title": "登出并清除浏览记录", + "description": "登出后,您的zk账户信息将从缓存中移除,您将无法使用密码再次访问,请使用助记词和新密码以恢复账户和显示余额。" + }, + "disablePasswordModal": { + "title": "停用密码", + "description": "请输入现用密码" + }, + "demoCard": { + "title": "测试版", + "description1": "此zkBob版本仅为测试私密,小额转账功能使用(不适用于存入/取款)。", + "description2": "<0>如需使用全功能版本,<1>请在此处免费创建zkBob帐户。" + }, + "increasedLimitsBanner": { + "inactive": "提高存款限额?", + "active": "存款限额已提高", + "resync": "如需恢复已提高的存款限额 - <1>请重新同步您的BAB代币" + }, + "increasedLimitsModal": { + "title": "提高限额", + "paragraph1": "若需提高个人存款限额,您需要在BNB Smart Chain上拥有BAB代币。什么是BAB代币? <1>点击了解。", + "paragraph2": "验证您的BAB代币:", + "step1": "请将持有BAB代币的Metamask或WalletConnect钱包连接到zkBob。无需切换网络,请继续使用Polygon。", + "step2": "点击下方 验证我的BAB代币。", + "paragraph3": "已完成!如果您连接的地址持有BAB代币,您的存款限额将自动提升。", + "button": "验证我的BAB代币" + }, + "more": { + "title": "了解更多关于zkBob的资讯", + "dune": "Dune Analytics", + "docs": "文档", + "linktree": "Linktree" + }, + "networks": { + "title": "网络" + }, + "pagination": { + "first": "首页", + "last": "尾页", + "current": "第 {{current}} 页,共 {{total}} 页" + }, + "paymentLinkModal": { + "title": "生成支付链接", + "description": "如需进行私密支付,请分享此链接。
发送人可以选择并发送任何代币。代币将转换为 {{symbol}} 并存入您的zk账户。

请注意:私密支付链接将只在生成它们的区块链网络上生效。", + "copyAndShare": "复制并分享您的支付链接", + "getMoreInfo": "了解更多有关支付链接的资讯" + }, + "reedeemGifCardModal": { + "loading": { + "title": "正在准备您的礼品卡" + }, + "start": { + "title": "恭喜您!", + "description": "您将获得礼品卡", + "button": "领取 {{symbol}}" + }, + "createAccount": { + "title": "在兑换
礼品卡之前", + "description": "如需兑换礼品卡,您需要一个zkBob zk账户!请创建帐户,或登入现有帐户,以领取您的 {{symbol}} 代币。", + "button": "登入或创建zk账户" + }, + "switchNetwork": { + "title": "需要切换网络", + "description": "礼品卡仅在 {{network}} 网络上可用。请切换网络以兑换礼品卡。" + }, + "inProgress": { + "title": "转账至您的zk账户", + "description": "本次交易过程将用时更长。若该笔交易在几秒钟内未完成,请联系我们的<1>支缓团队" + }, + "completed": { + "title": "您的 {{symbol}} 正在路上", + "description": "您的代币将在接下来的1-2分钟内到账。点击“历史记录”以查看交易完成状态" + }, + "failed": { + "error": "该礼品卡已被使用", + "otherError": "出错了", + "description": "请联系我们的支缓团队以解决该问题,并在客服请求中输入您的Support ID" + } + }, + "restrictionModal": { + "title": "该国家或地区暂不支缓zkBob业务", + "description": "{{country}} 暂不支缓zkBob业务。您需要在受支缓的国家或地区使用该应用。" + }, + "secretPhraseModal": { + "title": "显示助记词", + "warning": "请勿与任何人分享您的助记词,
请妥善保管!", + "description": "若他人掌握您的助记词,他们将完全掌握您钱包的控制权。", + "copy": "复制助记词" + }, + "transactionModal": { + "titles": { + "approveTokens": "请允许代币交易", + "approved": "代币已被允许交易", + "signMessage": "请在钱包中签名", + "confirmTransaction": "请确认交易", + "waitingForTransaction": "等待交易", + "generatingProof": "生成证明", + "waitingForRelayer": "等待中继器完成", + "deposited": "正在存入", + "transferred": "正在转账", + "transferredMulti": "正在转账至多地址", + "withdrawn": "正在提款", + "rejected": "交易已被拒绝", + "signatureExpired": "签名超时", + "suspiciousAccountDeposit": "连接到可疑钱包", + "suspiciousAccountWithdrawal": "可疑的收款地址", + "wrongNetwork": "网络错误", + "switchNetwork": "请切换网络", + "sent": "支付已发送", + "preparingTransaction": "正在准备交易" + }, + "descriptions": { + "deposited": "您的{{amount}} {{symbol}}正在存入零知识池。

为了提高隐私程度,请考虑将代币保存在零知识池中一段时间,再执行提款。", + "transferred": "正在进行零知识池内的{{amount}} {{symbol}}代币转账。", + "transferredMulti": "正在进行零知识池内的{{amount}} {{symbol}}多地址转账。", + "withdrawn": "正在进行零知识池内的{{amount}} {{symbol}}代币提款。", + "signatureExpired": "签名超时,请重试。", + "suspiciousAccountDeposit": "您的钱包地址可能涉及可疑活动。您无法在zkBob上使用该钱包地址,请使用另一个地址。", + "suspiciousAccountWithdrawal": "收款人的钱包地址可能涉及可疑活动,您无法将资金提取到该地址。", + "wrongNetwork": "切换网络失败。请将您的钱包连接到{{network}},然后重试。", + "approved": "Approval已完成。您现在可以存入代币。", + "sent": "处理付款可能需要最多10分钟。
<1>查看交易记录

或 下载
<2><3>交易明细< /2>", + "signMessage": "您需要在钱包中签名,并允许合约存入您的代币。" + } + }, + "maxButton": { + "tooltip": "点击“最大”以设置您可以发送的最大金额{{symbol}},已包括所有费用和限额" + }, + "connectWalletModal": { + "title": "连接Web3钱包", + "description": "连接您的钱包,并将{{symbol}}存入您的zk账户。若您正在创建新的zk账户,当前连接的钱包将被用于为zkBob应用程序生成私密的加密密钥。", + "note": "点击连接钱包即表示您同意zkBob
<1>服务条款" + }, + "qrCode": { + "title": "二维码地址", + "description": "若要接收来自其他zk账户的私密转账,转账人可以从他们的应用中扫描此二维码。
其他用户只需在转账页面上扫描您的二维码" + }, + "zkAccount": { + "addressDescription": "使用该地址接收代币至您的zkBob帐户。每次连接钱包时都将自动创建一个新地址,您可以使用该地址或之前生成的地址接收代币。" + }, + "paymentStatement": { + "amount": "{{symbol}}金额", + "fee": "{{symbol}}费用" + }, + "glossary": { + "rows": "行", + "rows_other": "行", + "addresses": "地址", + "addresses_other": "地址", + "issues": "问题", + "issues_other": "问题", + "transactions": "交易", + "transactions_other": "交易", + "statuses": "状态", + "statuses_other": "状态" + } +} \ No newline at end of file From d873c02c06240718509700f0a70993eba57adb5a Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 23 Oct 2023 18:56:28 +0200 Subject: [PATCH 13/45] upgrade version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be6e3b50..23839fcb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zkbob-ui", - "version": "3.3.1", + "version": "3.4.0", "private": true, "dependencies": { "@dicebear/avatars": "^4.10.2", From 2689f4639c89097ae5ad2105bcc86201c77847e1 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 23 Oct 2023 19:12:36 +0200 Subject: [PATCH 14/45] Update README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 63c262f6..27670f4f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,16 @@ REACT_APP_WALLETCONNECT_PROJECT_ID= ``` You can create a .env file in the root directory and set these variables. +## Deploy to IPFS using dAppling + +Follow these steps to deploy: + +1. Visit [dAppling Network](https://dappling.network) and sign in. +2. Choose the repository. +3. Check and confirm the build configuration (usually auto-filled). +4. Set up environment variables in the settings. +5. Click "Deploy". + ## License This project is licensed under the Apache-2.0 and the MIT licenses. From 6b55a52293a104d047e0a76b74c14690fb0990a8 Mon Sep 17 00:00:00 2001 From: maikReal Date: Wed, 25 Oct 2023 16:46:53 +0100 Subject: [PATCH 15/45] Added CONTRIBUTING.md file; Updated README.md file --- CONTRIBUTING.md | 41 +++++++++++++++++++++++++++++++++++++++++ README.md | 14 ++++++++++---- 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c9f186c5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing to zkBob +We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: + +- Reporting a bug +- Discussing the current state of the code +- Submitting a fix +- Proposing new features +- Becoming a maintainer + +## We Develop with Github +We use github to host code, to track issues and feature requests, as well as accept pull requests. For any technical questions you can also use [our Discord server](https://discord.com/invite/zkbob) + +## All Code Changes Happen Through Pull Requests +Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: + +1. Fork the repo and create your branch from `main`. +2. If you've changed/added any features, update [the documentation](https://docs.zkbob.com/) in the specific section. +3. Ensure the test suite passes. +4. Make sure your code lints. +5. Issue that pull request! + +## Any contributions you make will be under the MIT Software License +In short, when you submit code changes, your submissions are understood to be under the same [MIT License](https://github.com/zkBob/zkbob-ui/blob/main/LICENSE_MIT) and [Apache License](https://github.com/zkBob/zkbob-ui/blob/main/LICENSE_APACHE) that covers the project. Feel free to contact the maintainers if that's a concern. + +## Report bugs using Github's [issues](https://github.com/zkBob/zkbob-ui/issues) +We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/zkBob/zkbob-ui/issues); it's that easy! +You can also report urgent bugs on [our Discord server](https://discord.com/invite/zkbob) + +## Write bug reports with detail, background, and sample code +The issues with the bug report should be well-described. It should include at least the following sections: +- Expected Behavior +- Actual Behavior +- Steps to reproduce + - Be specific! +- Any references regarding a bug (screenshots with errors, etc) +- Your machine info (platform, what wallet do you use etc) +- zkBob UI details: your support ID, Library, Web and Relayer versions. You can find this informatio at the buttom of the UI page + + +## License +By contributing, you agree that your contributions will be licensed under its [MIT License](https://github.com/zkBob/zkbob-ui/blob/main/LICENSE_MIT) and [Apache-2.0 Licence](https://github.com/zkBob/zkbob-ui/blob/main/LICENSE_APACHE). \ No newline at end of file diff --git a/README.md b/README.md index 27670f4f..be6403c7 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # zkBob UI -## Local development +## Deployment Process + +### Local development Install dependencies and start the development server: ```bash yarn yarn start ``` -### Environment variables +#### Environment variables To run this project, you'll need to set up the following environment variables: ``` REACT_APP_CONFIG= // "prod" or "dev" @@ -15,7 +17,7 @@ REACT_APP_WALLETCONNECT_PROJECT_ID= ``` You can create a .env file in the root directory and set these variables. -## Deploy to IPFS using dAppling +### Deploy to IPFS using dAppling Follow these steps to deploy: @@ -25,6 +27,10 @@ Follow these steps to deploy: 4. Set up environment variables in the settings. 5. Click "Deploy". +## Contributing to zkBob + +For the contributing to zkBob you can use our [Contributing Guidline](https://github.com/zkBob/zkbob-ui/blob/main/CONTRIBUTING.md) + ## License -This project is licensed under the Apache-2.0 and the MIT licenses. +This project is licensed under the [MIT License](https://github.com/zkBob/zkbob-ui/blob/main/LICENSE_MIT) and [Apache-2.0 Licence](https://github.com/zkBob/zkbob-ui/blob/main/LICENSE_APACHE). From d7aeaf88023da92082de195968b396711a68e4dc Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 26 Oct 2023 17:58:14 +0200 Subject: [PATCH 16/45] delete unnecessary code from netlify config --- netlify.toml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/netlify.toml b/netlify.toml index 22e901c8..12a1742f 100644 --- a/netlify.toml +++ b/netlify.toml @@ -4,13 +4,6 @@ Cross-Origin-Embedder-Policy = "credentialless" Cross-Origin-Opener-Policy = "same-origin" -# [[redirects]] -# from = "/*" -# to = "451.html" -# status = 451 -# force = true -# conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","RU","SD","SY","US","ZW"]} - [[redirects]] from = "/*" to = "index.html" @@ -21,15 +14,3 @@ [plugins.inputs] source = '_redirects' base = '/opt/build/repo/public' - -[context.production.environment] - 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_CONFIG="dev" - REACT_APP_RESTRICTED_COUNTRIES = "AE" - REACT_APP_LOCK_TIMEOUT = "300000" - # REACT_APP_RESTRICTED_COUNTRIES = "BY,CU,IR,IQ,CI,LR,KP,RU,SD,SY,US,ZW" From bf9453737731609e0cf499c6582dfddc68480ce1 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 26 Oct 2023 18:47:08 +0200 Subject: [PATCH 17/45] fix country restrictions --- src/components/RestrictionModal/index.js | 4 ++-- src/hooks/useRestriction.js | 12 ++++++++---- src/pages/index.js | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/RestrictionModal/index.js b/src/components/RestrictionModal/index.js index 64639d9f..0a6bf1a2 100644 --- a/src/components/RestrictionModal/index.js +++ b/src/components/RestrictionModal/index.js @@ -6,7 +6,7 @@ import Link from 'components/Link'; import image from 'assets/bob-earth.png'; -export default () => { +export default ({ country }) => { const { t } = useTranslation(); return ( @@ -15,7 +15,7 @@ export default () => { {t('restrictionModal.title')} - {t('restrictionModal.description', { country: 'United Arab Emirates' })} + {t('restrictionModal.description', { country })} res.text()), - fetch('https://api.country.is').then(res => res.json()).then(data => data.country), + 'https://ipapi.co/json', + 'https://api.country.is', ]; let country; for (const api of apis) { try { - country = await api; + const res = await fetch(api); + const data = await res.json(); + country = data.country; break; } catch (error) { console.error(error); @@ -22,6 +24,7 @@ async function getUserCountry() { export default () => { const [isRestricted, setIsRestricted] = useState(false); + const [country, setCountry] = useState(null); useEffect(() => { async function check() { @@ -32,6 +35,7 @@ export default () => { const country = await getUserCountry(); if (RESTRICTED_COUNTRIES.includes(country)) { setIsRestricted(true); + setCountry(country); } } catch (error) { console.error('Failed to get country by IP.'); @@ -40,5 +44,5 @@ export default () => { check(); }, []); - return isRestricted; + return { isRestricted, country }; }; diff --git a/src/pages/index.js b/src/pages/index.js index bed5254f..b7022a24 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -110,7 +110,7 @@ const MainApp = () => { const { zkAccount, isLoadingZkAccount, isDemo, lockAccount } = useContext(ZkAccountContext); const location = useLocation(); const showWelcome = (!zkAccount && !isLoadingZkAccount && !window.localStorage.getItem('seed')) || isDemo; - const isRestricted = useRestriction(); + const { isRestricted, country } = useRestriction(); useIdleTimer({ timeout: Number(process.env.REACT_APP_LOCK_TIMEOUT) || (1000 * 60 * 15), onIdle: () => lockAccount(), @@ -119,7 +119,7 @@ const MainApp = () => { if (isRestricted) { return ( }> - + ); } From fc4441b81bd10be02187f6cafab8792f22af2bfd Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 30 Oct 2023 14:04:38 +0100 Subject: [PATCH 18/45] remove country from restriction message --- src/components/RestrictionModal/index.js | 4 ++-- src/hooks/useRestriction.js | 4 +--- src/locales/en.json | 2 +- src/locales/pt.json | 2 +- src/locales/ru.json | 2 +- src/locales/zh.json | 4 ++-- src/pages/index.js | 4 ++-- 7 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/RestrictionModal/index.js b/src/components/RestrictionModal/index.js index 0a6bf1a2..e92d6f33 100644 --- a/src/components/RestrictionModal/index.js +++ b/src/components/RestrictionModal/index.js @@ -6,7 +6,7 @@ import Link from 'components/Link'; import image from 'assets/bob-earth.png'; -export default ({ country }) => { +export default () => { const { t } = useTranslation(); return ( @@ -15,7 +15,7 @@ export default ({ country }) => { {t('restrictionModal.title')} - {t('restrictionModal.description', { country })} + {t('restrictionModal.description')} { const [isRestricted, setIsRestricted] = useState(false); - const [country, setCountry] = useState(null); useEffect(() => { async function check() { @@ -35,7 +34,6 @@ export default () => { const country = await getUserCountry(); if (RESTRICTED_COUNTRIES.includes(country)) { setIsRestricted(true); - setCountry(country); } } catch (error) { console.error('Failed to get country by IP.'); @@ -44,5 +42,5 @@ export default () => { check(); }, []); - return { isRestricted, country }; + return isRestricted; }; diff --git a/src/locales/en.json b/src/locales/en.json index 6bcee91f..91743c71 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -347,7 +347,7 @@ }, "restrictionModal": { "title": "zkBob is not supported in this country or region", - "description": "zkBob is not supported in {{country}}. You will be able to use the application from supported country or region." + "description": "zkBob is not supported in your country. You will be able to use the application from supported country or region." }, "secretPhraseModal": { "title": "Show secret phrase", diff --git a/src/locales/pt.json b/src/locales/pt.json index 0f1f6061..62293492 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -347,7 +347,7 @@ }, "restrictionModal": { "title": "zkBob não é suportado neste país ou região", - "description": "zkBob não é suportado em {{country}}. Você poderá usar o aplicativo em um país ou região suportados." + "description": "zkBob não é suportado no seu país. Você poderá usar o aplicativo em um país ou região suportados." }, "secretPhraseModal": { "title": "Mostrar frase secreta", diff --git a/src/locales/ru.json b/src/locales/ru.json index 954af634..4834de00 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -345,7 +345,7 @@ }, "restrictionModal": { "title": "zkBob не поддерживается в этой стране или регионе", - "description": "zkBob не поддерживается в {{country}}. Вы сможете использовать приложение из поддерживаемой страны или региона." + "description": "zkBob не поддерживается в вашей стране. Вы сможете использовать приложение из поддерживаемой страны или региона." }, "secretPhraseModal": { "title": "Показать секретную фразу", diff --git a/src/locales/zh.json b/src/locales/zh.json index 476ea792..fbf75d2f 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -345,7 +345,7 @@ }, "restrictionModal": { "title": "该国家或地区暂不支缓zkBob业务", - "description": "{{country}} 暂不支缓zkBob业务。您需要在受支缓的国家或地区使用该应用。" + "description": "您的国家不支持zkBob。您需要在受支缓的国家或地区使用该应用。" }, "secretPhraseModal": { "title": "显示助记词", @@ -420,4 +420,4 @@ "statuses": "状态", "statuses_other": "状态" } -} \ No newline at end of file +} diff --git a/src/pages/index.js b/src/pages/index.js index b7022a24..bed5254f 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -110,7 +110,7 @@ const MainApp = () => { const { zkAccount, isLoadingZkAccount, isDemo, lockAccount } = useContext(ZkAccountContext); const location = useLocation(); const showWelcome = (!zkAccount && !isLoadingZkAccount && !window.localStorage.getItem('seed')) || isDemo; - const { isRestricted, country } = useRestriction(); + const isRestricted = useRestriction(); useIdleTimer({ timeout: Number(process.env.REACT_APP_LOCK_TIMEOUT) || (1000 * 60 * 15), onIdle: () => lockAccount(), @@ -119,7 +119,7 @@ const MainApp = () => { if (isRestricted) { return ( }> - + ); } From ebb7e28b97fd2da8e16f041781c7a731acaf3b32 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Wed, 1 Nov 2023 15:35:40 +0100 Subject: [PATCH 19/45] show restriction message in Chinese --- src/components/RestrictionModal/index.js | 2 +- src/hooks/useRestriction.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/RestrictionModal/index.js b/src/components/RestrictionModal/index.js index e92d6f33..9cdcb187 100644 --- a/src/components/RestrictionModal/index.js +++ b/src/components/RestrictionModal/index.js @@ -18,7 +18,7 @@ export default () => { {t('restrictionModal.description')} {t('common.learnMore')} diff --git a/src/hooks/useRestriction.js b/src/hooks/useRestriction.js index fddfd933..c8ac8402 100644 --- a/src/hooks/useRestriction.js +++ b/src/hooks/useRestriction.js @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; async function getUserCountry() { const apis = [ @@ -23,6 +24,7 @@ async function getUserCountry() { } export default () => { + const { i18n } = useTranslation(); const [isRestricted, setIsRestricted] = useState(false); useEffect(() => { @@ -33,6 +35,7 @@ export default () => { const RESTRICTED_COUNTRIES = process.env.REACT_APP_RESTRICTED_COUNTRIES.split(','); const country = await getUserCountry(); if (RESTRICTED_COUNTRIES.includes(country)) { + i18n.changeLanguage('zh'); setIsRestricted(true); } } catch (error) { From c27f0912eeeafc324705c9b3ce88b6ff515ac703 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 2 Nov 2023 15:06:37 +0100 Subject: [PATCH 20/45] rename USDC to USDC.e --- src/components/NetworkDropdown/index.js | 2 +- src/config/index.js | 2 +- src/constants/index.js | 1 + src/pages/Payment/index.js | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/NetworkDropdown/index.js b/src/components/NetworkDropdown/index.js index fb831649..d2f129f0 100644 --- a/src/components/NetworkDropdown/index.js +++ b/src/components/NetworkDropdown/index.js @@ -99,7 +99,7 @@ export default ({ children }) => { const { currentPool } = useContext(PoolContext); return ( { const { supportId } = useContext(SupportIdContext); const history = useHistory(); const params = useParams(); - const currency = ['USDC', 'BOB'].includes(pool.tokenSymbol) ? 'USD' : pool.tokenSymbol; + const currency = ['USDC.e', 'USDC', 'BOB'].includes(pool.tokenSymbol) ? 'USD' : pool.tokenSymbol; const { address: account } = useContext(WalletContext); const [displayedAmount, setDisplayedAmount] = useState(''); From da735bf299fcfbd12f5c21dcdbe5c38146069920 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 2 Nov 2023 15:18:54 +0100 Subject: [PATCH 21/45] split the multicall into several --- src/pages/Payment/hooks.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/Payment/hooks.js b/src/pages/Payment/hooks.js index 071b58c8..e6a07085 100644 --- a/src/pages/Payment/hooks.js +++ b/src/pages/Payment/hooks.js @@ -44,9 +44,18 @@ export function useTokensBalances(tokenList) { abi: TOKEN_ABI, calls: [{ reference: token.address, methodName: 'balanceOf', methodParameters: [account] }], })); + const chunks = contractCallContext.reduce((acc, item) => { + const last = acc[acc.length - 1]; + if (last.length < 1000) { + last.push(item); + } else { + acc.push([item]); + } + return acc; + }, [[]]); const data = await Promise.all([ provider.getBalance(account), - multicall.call(contractCallContext), + ...chunks.map(chunk => multicall.call(chunk)), ]); balances[ethers.constants.AddressZero] = data[0]; delete data[1].results[ethers.constants.AddressZero]; From 9a189b477a41be4d8dd95fe5ed5028e3b8baff30 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 2 Nov 2023 17:19:30 +0100 Subject: [PATCH 22/45] add pool on nile --- package.json | 2 +- src/config/index.js | 20 +++++++++++++++++++- src/constants/index.js | 8 ++++++++ yarn.lock | 28 ++++++++++++++-------------- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 23839fcb..2092fa76 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "wagmi": "^0.12.1", "web-vitals": "^1.0.1", "webpack": "^5.70.0", - "zkbob-client-js": "5.3.0" + "zkbob-client-js": "5.4.0-beta3" }, "scripts": { "start": "react-app-rewired start", diff --git a/src/config/index.js b/src/config/index.js index cd7e1c7a..5f42a3b7 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -178,6 +178,21 @@ const config = { addressPrefix: 'zkbob_shasta', isTron: true, }, + 'USDT-nile': { + chainId: 3448148188, + poolAddress: 'TT8GgygLhEDh88kYwY8mYz38iEpJWw1YLG', + tokenAddress: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', + relayerUrls: ['https://tron-nile-relayer.thgkjlr.website'], + delegatedProverUrls: [], + coldStorageConfigPath: '', + tokenSymbol: 'USDT', + tokenDecimals: 6, + feeDecimals: 2, + depositScheme: 'approve', + minTxAmount: 50000n, // 0.05 USDT + addressPrefix: 'zkbob_nile', + isTron: true, + }, }, chains: { '11155111': { @@ -191,7 +206,10 @@ const config = { }, '2494104990': { rpcUrls: ['https://api.shasta.trongrid.io'] - } + }, + '3448148188': { + rpcUrls: ['https://nile.trongrid.io'] + }, }, snarkParams: { transferParamsUrl: 'https://r2-staging.zkbob.com/transfer_params_20022023.bin', diff --git a/src/constants/index.js b/src/constants/index.js index 4788bcf5..f3360a05 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -67,6 +67,14 @@ export const NETWORKS = { address: 'https://shasta.tronscan.org/#/address/%s', tx: 'https://shasta.tronscan.org/#/transaction/%s', }, + }, + 3448148188: { + name: 'Nile', + icon: require('assets/tron.png'), + blockExplorerUrls: { + address: 'https://nile.tronscan.org/#/address/%s', + tx: 'https://nile.tronscan.org/#/transaction/%s', + }, } }; diff --git a/yarn.lock b/yarn.lock index 22b4bfb8..f14da1f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12783,15 +12783,15 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libzkbob-rs-wasm-web-mt@1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web-mt/-/libzkbob-rs-wasm-web-mt-1.4.2.tgz#134140557a23f251a40995f64102d9b1091b94a2" - integrity sha512-2jvdNy20Va2e9IAlPbmewZ/7aeZMH1NBEQFhfH9BVObhVqNpNWWhna82zRXT0ebfu+HJwaZGJ1w4n6UzzYO4cw== +libzkbob-rs-wasm-web-mt@1.5.0-beta2: + version "1.5.0-beta2" + resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web-mt/-/libzkbob-rs-wasm-web-mt-1.5.0-beta2.tgz#fd0c3b56585ce7a8ab7630b65c5355fdab47e5c0" + integrity sha512-BVED7gg1qm4Y9bCXIiW8E98SSMv+dtR7IVBJ03i3042exyNAzlqbtynkD9lyEzhAjAB20g/Baziws7AS5v29Ag== -libzkbob-rs-wasm-web@1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web/-/libzkbob-rs-wasm-web-1.4.2.tgz#c69453f387817eb5d0e335d9b6a2bead28ce5b49" - integrity sha512-CJDe7lpxsS70Z/eSvVmufSq4nQZ25M59a49ZIXEyTG86hJ9xCNkCjIiSvJSZh0+UwVQI87PSkmMeQY+AefqDSQ== +libzkbob-rs-wasm-web@1.5.0-beta2: + version "1.5.0-beta2" + resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web/-/libzkbob-rs-wasm-web-1.5.0-beta2.tgz#05d5458557de4dd3273075028d37977ce184cdbf" + integrity sha512-OVXWtebMH4Ik0YwybkvfG4e/kjLONU9gXziWBDRpiNkY9bpD0VfRFs5IVWrmYLmTGTmeAGM2bwH/sAAenpmNxw== lie@3.1.1: version "3.1.1" @@ -19015,10 +19015,10 @@ 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@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.3.0.tgz#b0ae16e560ee86eb278ec0f1c5dfd775724992d8" - integrity sha512-b4WOFjMfXhrwxGOPw8V1+4kL/FkfB7D/SFye/nToG/sAhTP0gi7LhaSvTD/BwqlfKnyyI52KiAXs4TscUf6Tyg== +zkbob-client-js@5.4.0-beta3: + version "5.4.0-beta3" + resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.4.0-beta3.tgz#10d677437c6297b724d6cfde00b0097ab842f780" + integrity sha512-9ZMOeSGDC+khZ2/9WPmSdNSgW3iNfOpdKNPewEMWvaSn4NRuZScW/DsBX7kkQz7GQjXKrb+SPM+11mg/XzavxQ== dependencies: "@ethereumjs/util" "^8.0.2" "@graphprotocol/client-cli" "3.0.0" @@ -19031,8 +19031,8 @@ zkbob-client-js@5.3.0: graphql "16.7.1" hdwallet-babyjub "^0.0.2" idb "^7.0.0" - libzkbob-rs-wasm-web "1.4.2" - libzkbob-rs-wasm-web-mt "1.4.2" + libzkbob-rs-wasm-web "1.5.0-beta2" + libzkbob-rs-wasm-web-mt "1.5.0-beta2" promise-retry "^2.0.1" promise-throttle "^1.1.2" regenerator-runtime "^0.13.9" From ac8c7263af8641425b61975a279731c2d065b4ca Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 2 Nov 2023 18:51:13 +0100 Subject: [PATCH 23/45] fix the interaction with Tron --- src/contexts/WalletContext/index.js | 38 ++++++++++++++++++++------ src/contexts/ZkAccountContext/index.js | 2 +- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/contexts/WalletContext/index.js b/src/contexts/WalletContext/index.js index 051b5367..54ded219 100644 --- a/src/contexts/WalletContext/index.js +++ b/src/contexts/WalletContext/index.js @@ -12,6 +12,8 @@ import { useWallet } from '@tronweb3/tronwallet-adapter-react-hooks'; import PoolContext from 'contexts/PoolContext'; +import config from 'config'; + const WalletContext = createContext({}); export default WalletContext; @@ -81,12 +83,21 @@ const useTronWallet = pool => { const [chainId, setChainId] = useState(null); + const tronWeb = useMemo(() => + new TronWeb({ fullHost: config.chains[pool.chainId].rpcUrls[0] }), + [pool] + ); + useEffect(() => { async function updateChainId() { const { chainId } = await wallet.adapter.network(); setChainId(BigNumber.from(chainId).toNumber()); } - if (wallet) updateChainId(); + if (wallet) { + updateChainId(); + wallet.adapter.on('chainChanged', updateChainId); + return () => wallet.adapter.removeAllListeners(); + } }, [wallet]); const selectWalletAndConnect = useCallback(async ({ connector }) => { @@ -100,27 +111,29 @@ const useTronWallet = pool => { const getBalance = useCallback(async () => { let balance = ethers.constants.Zero; - if (address && window.tronWeb) { + if (address && tronWeb) { try { - const result = await window.tronWeb.trx.getBalance(address); + const result = await tronWeb.trx.getBalance(address); balance = BigNumber.from(result); } catch (error) { console.error(error); } } return balance; - }, [address]); + }, [address, tronWeb]); - const callContract = async (address, abi, method, params = [], isSend = false) => { - if (!window.tronWeb) throw new Error('TronWeb not found'); - const contract = await window.tronWeb.contract(abi, address); + const callContract = async (contractAddress, abi, method, params = [], isSend = false) => { + const tronWebInstance = isSend ? window.tronWeb : tronWeb; + if (!tronWebInstance) throw new Error('TronWeb not found'); + if (!isSend) tronWebInstance.setAddress(address); + const contract = await tronWebInstance.contract(abi, contractAddress); return contract[method](...params)[isSend ? 'send' : 'call'](); } const waitForTx = async tx => { - if (!window.tronWeb) throw new Error('TronWeb not found'); + if (!tronWeb) throw new Error('TronWeb not found'); async function wait(attempt = 0) { - const response = await window.tronWeb.trx.getTransactionInfo(tx); + const response = await tronWeb.trx.getTransactionInfo(tx); if (!response.receipt) { if (attempt > 60) throw new Error('Response timeout'); await new Promise(resolve => setTimeout(resolve, 3000)); @@ -143,6 +156,12 @@ const useTronWallet = pool => { return window.tronWeb.trx.sign(message); } + const switchNetwork = useCallback(async () => { + const chainIdHex = ethers.utils.hexValue(pool.chainId); + await wallet.adapter.switchChain(chainIdHex); + await new Promise(resolve => setTimeout(resolve, 1000)); + }, [pool, wallet]); + return { address, chain: { id: chainId }, @@ -154,6 +173,7 @@ const useTronWallet = pool => { signMessage, signTypedData, sendTransaction: () => {}, + switchNetwork, getBalance, callContract, waitForTx, diff --git a/src/contexts/ZkAccountContext/index.js b/src/contexts/ZkAccountContext/index.js index 607e96cc..f3e44dd8 100644 --- a/src/contexts/ZkAccountContext/index.js +++ b/src/contexts/ZkAccountContext/index.js @@ -310,7 +310,7 @@ export const ZkAccountContextProvider = ({ children }) => { openTxModal(); setTxAmount(amount); try { - if (chain.id !== currentPool.chainId) { + if (chain.id !== currentPool.chainId && !currentPool.isTron) { setTxStatus(TX_STATUSES.SWITCH_NETWORK); try { await switchNetwork(); From d42b22d9921ba9b3f222dd915a93f75f66afeefa Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Fri, 3 Nov 2023 16:58:32 +0100 Subject: [PATCH 24/45] upgrade lib --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2092fa76..c28579ec 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "wagmi": "^0.12.1", "web-vitals": "^1.0.1", "webpack": "^5.70.0", - "zkbob-client-js": "5.4.0-beta3" + "zkbob-client-js": "5.4.0-beta4" }, "scripts": { "start": "react-app-rewired start", diff --git a/yarn.lock b/yarn.lock index f14da1f1..8ab3fe94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19015,10 +19015,10 @@ 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@5.4.0-beta3: - version "5.4.0-beta3" - resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.4.0-beta3.tgz#10d677437c6297b724d6cfde00b0097ab842f780" - integrity sha512-9ZMOeSGDC+khZ2/9WPmSdNSgW3iNfOpdKNPewEMWvaSn4NRuZScW/DsBX7kkQz7GQjXKrb+SPM+11mg/XzavxQ== +zkbob-client-js@5.4.0-beta4: + version "5.4.0-beta4" + resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.4.0-beta4.tgz#570c8bcdab6d4503b41ba83c7e37223b84243449" + integrity sha512-iD1kO6ay4UHjcPHnEhUw6pR836OuRq2ZhzhTSl7/keB3Yja9UxnJrRdxiYPVQ/agmIY6veppNfQco8n98HVq8w== dependencies: "@ethereumjs/util" "^8.0.2" "@graphprotocol/client-cli" "3.0.0" From 059082bd8bd98bd5195c64585af6129343b63511 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 13 Nov 2023 20:00:56 +0100 Subject: [PATCH 25/45] fix tron wallet connection --- src/App.js | 2 +- src/contexts/WalletContext/index.js | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/App.js b/src/App.js index fd560913..c1b3e915 100644 --- a/src/App.js +++ b/src/App.js @@ -67,7 +67,7 @@ export default () => ( - + diff --git a/src/contexts/WalletContext/index.js b/src/contexts/WalletContext/index.js index 54ded219..aded433c 100644 --- a/src/contexts/WalletContext/index.js +++ b/src/contexts/WalletContext/index.js @@ -76,7 +76,7 @@ const convertWalletToConnector = wallet => ({ }); const useTronWallet = pool => { - const { address, connect, disconnect, select, wallet, wallets, signMessage } = useWallet(); + const { address, disconnect, select, wallet, wallets, signMessage } = useWallet(); const connector = useMemo(() => wallet ? convertWalletToConnector(wallet) : null, [wallet]); const connectors = useMemo(() => wallets.map(convertWalletToConnector), [wallets]); @@ -100,15 +100,6 @@ const useTronWallet = pool => { } }, [wallet]); - const selectWalletAndConnect = useCallback(async ({ connector }) => { - try { - select(connector.name); - await connect(); - } catch (error) { - console.error(error); - } - }, [select, connect]); - const getBalance = useCallback(async () => { let balance = ethers.constants.Zero; if (address && tronWeb) { @@ -167,7 +158,7 @@ const useTronWallet = pool => { chain: { id: chainId }, connector, connectors, - connect: selectWalletAndConnect, + connect: ({ connector }) => select(connector.name), disconnect, sign, signMessage, From c0224457b280651a8b63268c8ec37c47767356f1 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Tue, 14 Nov 2023 19:01:03 +0100 Subject: [PATCH 26/45] Update manifest.json --- public/manifest.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/manifest.json b/public/manifest.json index 007d9bc6..fd658bee 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,7 @@ { "short_name": "zkBob", - "name": "zkBob - Your web3 wallet with privacy option for everyday use", + "name": "zkBob", + "description": "Your web3 wallet with privacy option for everyday use", "icons": [ { "src": "favicon.ico", @@ -11,5 +12,6 @@ "start_url": ".", "display": "standalone", "theme_color": "#000000", - "background_color": "#ffffff" + "background_color": "#ffffff", + "iconPath": "logo256.png" } From bced4576e70b265707976142a5ebdf081667bc83 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 16 Nov 2023 22:42:35 +0100 Subject: [PATCH 27/45] add links for tronlink download --- src/App.js | 8 +- .../AccountSetUpModal/Generate/index.js | 20 +++-- src/components/AccountSetUpModal/index.js | 7 +- src/components/WalletConnectors/index.js | 82 ++++++++++++++++++- src/components/WalletModal/index.js | 42 +++++++--- src/contexts/WalletContext/index.js | 10 ++- src/locales/en.json | 10 ++- src/locales/pt.json | 10 ++- src/locales/ru.json | 14 +++- src/locales/zh.json | 14 +++- 10 files changed, 187 insertions(+), 30 deletions(-) diff --git a/src/App.js b/src/App.js index c1b3e915..eb6c3eb1 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,6 @@ import { createGlobalStyle } from 'styled-components'; import { WalletProvider as TronWalletProvider } from '@tronweb3/tronwallet-adapter-react-hooks'; +import { TronLinkAdapter } from '@tronweb3/tronwallet-adapters'; import ThemeProvider from 'providers/ThemeProvider'; import Web3Provider from 'providers/Web3Provider'; @@ -63,11 +64,16 @@ const GlobalStyle = createGlobalStyle` } `; +const tronLinkAdapter = new TronLinkAdapter({ + openUrlWhenWalletNotFound: false, + openTronLinkAppOnMobile: false, +}); + export default () => ( - + diff --git a/src/components/AccountSetUpModal/Generate/index.js b/src/components/AccountSetUpModal/Generate/index.js index c1571304..7728de26 100644 --- a/src/components/AccountSetUpModal/Generate/index.js +++ b/src/components/AccountSetUpModal/Generate/index.js @@ -4,11 +4,24 @@ import { useTranslation } from 'react-i18next'; import WalletConnectors from 'components/WalletConnectors'; -import { PoolContext } from 'contexts'; +import { PoolContext, WalletContext } from 'contexts'; export default ({ next, isCreation }) => { const { t } = useTranslation(); const { currentPool } = useContext(PoolContext); + const { noWalletInstalled, isMobileTronLink } = useContext(WalletContext); + + let description; + if (isCreation) { + description = t('accountSetupModal.createWithWallet.description'); + if (isMobileTronLink) { + description = t('connectWalletModal.tronlinkMobileDescription'); + } else if (noWalletInstalled) { + description = t('connectWalletModal.noWalletDescription', { wallet: 'TronLink' }); + } + } else { + description = t('accountSetupModal.restoreWithWallet.description'); + } return ( @@ -18,10 +31,7 @@ export default ({ next, isCreation }) => { )} - {isCreation - ? t('accountSetupModal.createWithWallet.description') - : t('accountSetupModal.restoreWithWallet.description') - } + {description} { export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) => { const { t } = useTranslation(); - const { tronWallet, evmWallet } = useContext(WalletContext); + const { tronWallet, evmWallet, noWalletInstalled } = useContext(WalletContext); const [step, setStep] = useState(STEP.START); const [newMnemonic, setNewMnemonic] = useState(); const [confirmedMnemonic, setConfirmedMnemonic] = useState(); @@ -188,10 +188,13 @@ export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) prevStep = STEP.START; break; case STEP.CREATE_WITH_WALLET: - title = t('accountSetupModal.createWithWallet.title'); + title = noWalletInstalled + ? t('connectWalletModal.noWalletTitle', { wallet: 'TronLink' }) + : t('accountSetupModal.createWithWallet.title'); component = ( { setIsTronWalletSelected(connector.isTron); setStep(STEP.SING_MESSAGE_TO_CREATE); diff --git a/src/components/WalletConnectors/index.js b/src/components/WalletConnectors/index.js index c4e8e8cf..1ed06cb7 100644 --- a/src/components/WalletConnectors/index.js +++ b/src/components/WalletConnectors/index.js @@ -1,12 +1,17 @@ import React, { useCallback, useContext, useMemo } from 'react'; import styled from 'styled-components'; +import { isAndroid } from 'react-device-detect'; +import { useTranslation } from 'react-i18next'; + +import Link from 'components/Link'; import { WalletContext } from 'contexts'; import { CONNECTORS_ICONS } from 'constants'; export default ({ callback, gaIdPrefix = '', showAll = false }) => { - const { isTron, evmWallet, tronWallet } = useContext(WalletContext); + const { t } = useTranslation(); + const { isTron, evmWallet, tronWallet, noWalletInstalled, isMobileTronLink } = useContext(WalletContext); const connectors = useMemo(() => { if (showAll && isTron) return [...tronWallet.connectors, ...evmWallet.connectors]; @@ -24,7 +29,61 @@ export default ({ callback, gaIdPrefix = '', showAll = false }) => { callback?.(connector); }, [callback, evmWallet, tronWallet]); - return ( + const openDeepLink = e => { + e.preventDefault(); + + let isAppOpened = false; + const handleVisibilityChange = () => { + if (document.hidden) { + isAppOpened = true; + } + } + document.addEventListener('visibilitychange', handleVisibilityChange, false); + + const deepLinkParams = { + // 'url': window.location.origin, + 'action': 'open', + 'protocol': 'tronlink', + 'version': '1.0', + }; + const deepLink = `tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify(deepLinkParams))}`; + + if (isAndroid) { + // Use an iframe for Android + var iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = deepLink; + document.body.appendChild(iframe); + } else { + // Use window.location for other platforms (like iOS) + window.location = deepLink; + } + + // Redirect to the app store after a timeout + setTimeout(() => { + document.removeEventListener('visibilitychange', handleVisibilityChange, false); + if (!isAppOpened) { + if (window.confirm(t('connectWalletModal.downloadQuestion'))) { + // Redirect to the Play Store for Android, App Store for iOS + window.location = isAndroid + ? 'https://play.google.com/store/apps/details?id=com.tronlink.global' + : 'https://apps.apple.com/app/id1453530188'; + } + } + }, 2000); + }; + + return (noWalletInstalled && !showAll) ? ( + + + {t(`connectWalletModal.${isMobileTronLink ? 'open' : 'install'}`, { wallet: 'TronLink' })} + + + + ) : ( <> {connectors.map((connector, index) => connector.ready && 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 WalletConnectorName = styled.span` font-size: 16px; color: ${({ theme }) => theme.text.color.primary}; diff --git a/src/components/WalletModal/index.js b/src/components/WalletModal/index.js index 8f60bd6d..78840262 100644 --- a/src/components/WalletModal/index.js +++ b/src/components/WalletModal/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext, useMemo } from 'react'; import styled from 'styled-components'; import { useTranslation, Trans } from 'react-i18next'; @@ -6,24 +6,40 @@ import Modal from 'components/Modal'; import Link from 'components/Link'; import WalletConnectors from 'components/WalletConnectors'; +import { WalletContext } from 'contexts'; + export default ({ isOpen, close, currentPool }) => { const { t } = useTranslation(); + const { noWalletInstalled, isMobileTronLink } = useContext(WalletContext); + + const { title, description } = useMemo(() => ({ + title: t(`connectWalletModal.${noWalletInstalled ? 'noWalletTitle' : 'title'}`, { wallet: 'TronLink' }), + description: isMobileTronLink + ? t('connectWalletModal.tronlinkMobileDescriptionWithDeposit', { symbol: currentPool?.tokenSymbol }) + : t( + `connectWalletModal.${noWalletInstalled ? 'noWalletDescriptionWithDeposit' : 'description'}`, + { symbol: currentPool?.tokenSymbol, wallet: 'TronLink' } + ), + }), [t, noWalletInstalled, isMobileTronLink, currentPool]); + return ( - - {currentPool && ( + + {description} + + {!noWalletInstalled && ( - {t('connectWalletModal.description', { symbol: currentPool.tokenSymbol })} + , + }} + /> )} - - - , - }} - /> - ); }; diff --git a/src/contexts/WalletContext/index.js b/src/contexts/WalletContext/index.js index aded433c..f5e6bb23 100644 --- a/src/contexts/WalletContext/index.js +++ b/src/contexts/WalletContext/index.js @@ -1,5 +1,6 @@ import { createContext, useContext, useCallback, useMemo, useEffect, useState } from 'react'; import { ethers, BigNumber } from 'ethers'; +import { isMobile } from 'react-device-detect'; import { useAccount, useSignMessage, useConnect, useDisconnect, @@ -181,8 +182,15 @@ export const WalletContextProvider = ({ children }) => { const wallet = currentPool.isTron ? tronWallet : evmWallet; + const noWalletInstalled = useMemo(() => + currentPool.isTron && tronWallet.connectors.length === 1 && !tronWallet.connectors[0].ready, + [currentPool, tronWallet.connectors] + ); + + const isMobileTronLink = useMemo(() => isMobile && currentPool.isTron, [currentPool.isTron]); + return ( - + {children} ); diff --git a/src/locales/en.json b/src/locales/en.json index 91743c71..82071755 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -397,7 +397,15 @@ "connectWalletModal": { "title": "Connect web3 wallet", "description": "Connect your wallet to deposit {{symbol}} into your zkAccount. If you are creating a new zkAccount, your wallet is used to derive a private encryption key for the zkBob application.", - "note": "By connecting a wallet, you agree to zkBob
<1>Terms of Service" + "note": "By connecting a wallet, you agree to zkBob
<1>Terms of Service", + "noWalletTitle": "{{wallet}} wallet is not found", + "noWalletDescription": "Ensure that the {{wallet}} wallet extension is installed.", + "noWalletDescriptionWithDeposit": "Ensure that the {{wallet}} wallet extension is installed. Connect a Web3 wallet to deposit {{symbol}} into your zkAccount.", + "install": "Install {{wallet}} wallet", + "tronlinkMobileDescription": "To use zkBob with TronLink you need to open our app inside the browser of TronLink wallet.", + "tronlinkMobileDescriptionWithDeposit": "To use zkBob with TronLink you need to open our app inside the browser of TronLink wallet. Connect a Web3 wallet to deposit {{symbol}} into your zkAccount.", + "open": "Open {{wallet}} wallet", + "downloadQuestion": "If the app did not open, would you like to download it from the store?" }, "qrCode": { "title": "QR code address", diff --git a/src/locales/pt.json b/src/locales/pt.json index 62293492..12ac5214 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -397,7 +397,15 @@ "connectWalletModal": { "title": "Conectar carteira Web3", "description": "Conecte sua carteira para depositar {{symbol}} em sua conta zkAccount. Se você estiver criando uma nova conta zkAccount, sua carteira será usada para derivar uma chave de criptografia privada para o aplicativo zkBob.", - "note": "Ao conectar uma carteira, você concorda com os <1>Termos de Serviço do zkBob" + "note": "Ao conectar uma carteira, você concorda com os <1>Termos de Serviço do zkBob", + "noWalletTitle": "Carteira {{wallet}} não encontrada", + "noWalletDescription": "Certifique-se de que a extensão da carteira {{wallet}} está instalada.", + "noWalletDescriptionWithDeposit": "Certifique-se de que a extensão da carteira {{wallet}} está instalada. Conecte uma carteira Web3 para depositar {{symbol}} na sua zkAccount.", + "install": "Instalar carteira {{wallet}}", + "tronlinkMobileDescription": "Para usar o zkBob com o TronLink, você precisa abrir nosso aplicativo dentro do navegador da carteira TronLink.", + "tronlinkMobileDescriptionWithDeposit": "Para usar o zkBob com o TronLink, você precisa abrir nosso aplicativo dentro do navegador da carteira TronLink. Conecte uma carteira Web3 para depositar {{symbol}} na sua zkAccount.", + "open": "Abrir carteira {{wallet}}", + "downloadQuestion": "Se o aplicativo não abrir, você gostaria de baixá-lo da loja?" }, "qrCode": { "title": "Endereço do código QR", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4834de00..285afcb9 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -223,7 +223,9 @@ }, "restoreWithWallet": { "title": "Вход в ваш zk-Аккаунт", - "description": "Выберите кошелек, который вы использовали для создания своего zk-Аккаунта" + "description": "Выберите кошелек, который вы использовали для создания своего zk-Аккаунта", + "warning": "Кошелек TronLink будет использоваться только для восстановления доступа к вашему предыдущему аккаунту zkBob", + "warning_tron": "Кошельки MetaMask и Wallet Connect будут использоваться только для восстановления доступа к вашему предыдущему аккаунту zkBob" }, "restoreWithSecret": { "title": "Восстановить zk-Аккаунт", @@ -395,7 +397,15 @@ "connectWalletModal": { "title": "Подключить web3 кошелек", "description": "Подключите свой кошелек, чтобы внести {{symbol}} на свой zk-Аккаунт. Если вы создаете новый zk-Аккаунт, ваш кошелек используется для создания закрытого ключа шифрования для приложения zkBob.", - "note": "Подключая кошелек, вы соглашаетесь с
<1>условиями обслуживания zkBob" + "note": "Подключая кошелек, вы соглашаетесь с
<1>условиями обслуживания zkBob", + "noWalletTitle": "Кошелек {{wallet}} не найден", + "noWalletDescription": "Убедитесь, что установлено расширение кошелька {{wallet}}.", + "noWalletDescriptionWithDeposit": "Убедитесь, что установлено расширение кошелька {{wallet}}. Подключите кошелек Web3, чтобы внести {{symbol}} в ваш zkAccount.", + "install": "Установить кошелек {{wallet}}", + "tronlinkMobileDescription": "Чтобы использовать zkBob с TronLink, вам нужно открыть наше приложение в браузере кошелька TronLink.", + "tronlinkMobileDescriptionWithDeposit": "Чтобы использовать zkBob с TronLink, вам нужно открыть наше приложение в браузере кошелька TronLink. Подключите кошелек Web3, чтобы внести {{symbol}} на ваш zkAccount.", + "open": "Открыть кошелек {{wallet}}", + "downloadQuestion": "Если приложение не открывается, вы хотели бы скачать его из магазина?" }, "qrCode": { "title": "QR-код адреса", diff --git a/src/locales/zh.json b/src/locales/zh.json index fbf75d2f..9737e680 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -223,7 +223,9 @@ }, "restoreWithWallet": { "title": "登入zk账户", - "description": "请选择您用于创建zk账户的钱包地址" + "description": "请选择您用于创建zk账户的钱包地址", + "warning": "TronLink钱包仅用于恢复对您之前的zkBob账户的访问权限", + "warning_tron": "MetaMask和Wallet Connect钱包仅用于恢复对您之前的zkBob账户的访问权限" }, "restoreWithSecret": { "title": "恢复zk账户", @@ -395,7 +397,15 @@ "connectWalletModal": { "title": "连接Web3钱包", "description": "连接您的钱包,并将{{symbol}}存入您的zk账户。若您正在创建新的zk账户,当前连接的钱包将被用于为zkBob应用程序生成私密的加密密钥。", - "note": "点击连接钱包即表示您同意zkBob
<1>服务条款" + "note": "点击连接钱包即表示您同意zkBob
<1>服务条款", + "noWalletTitle": "未找到{{wallet}}钱包", + "noWalletDescription": "确保已安装{{wallet}}钱包扩展。", + "noWalletDescriptionWithDeposit": "确保已安装{{wallet}}钱包扩展。连接Web3钱包以将{{symbol}}存入您的zkAccount。", + "install": "安装{{wallet}}钱包", + "tronlinkMobileDescription": "要在TronLink中使用zkBob,您需要在TronLink钱包的浏览器中打开我们的应用。", + "tronlinkMobileDescriptionWithDeposit": "要在TronLink中使用zkBob,您需要在TronLink钱包的浏览器中打开我们的应用。连接Web3钱包以将{{symbol}}存入您的zkAccount。", + "open": "打开{{wallet}}钱包", + "downloadQuestion": "如果应用程序没有打开,您想从商店下载它吗?" }, "qrCode": { "title": "二维码地址", From 18d3355ac9f0ce3741042c84e73081fcb39519f3 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Fri, 17 Nov 2023 11:40:21 +0100 Subject: [PATCH 28/45] remove steps with secret phrase --- src/components/AccountSetUpModal/index.js | 7 +++++-- src/locales/en.json | 2 +- src/locales/pt.json | 2 +- src/locales/ru.json | 2 +- src/locales/zh.json | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/AccountSetUpModal/index.js b/src/components/AccountSetUpModal/index.js index 85c76fb8..26f8c150 100644 --- a/src/components/AccountSetUpModal/index.js +++ b/src/components/AccountSetUpModal/index.js @@ -115,11 +115,14 @@ export default ({ isOpen, onClose, saveZkAccountMnemonic, closePasswordModal }) }, [onClose]); const setNextStep = useCallback(nextStep => { - let newMnemonic = null; if (nextStep === STEP.CREATE_WITH_SECRET) { + let newMnemonic = null; newMnemonic = ethers.Wallet.createRandom().mnemonic.phrase; + setNewMnemonic(newMnemonic); + setConfirmedMnemonic(newMnemonic); + setStep(STEP.CREATE_PASSWORD_PROMPT); + return; } - setNewMnemonic(newMnemonic); setStep(nextStep); }, []); diff --git a/src/locales/en.json b/src/locales/en.json index 82071755..51a8f691 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -204,7 +204,7 @@ "title": "Choose how you would like to create your account", "description": "By creating zkAccount, you hereby agree to and accept zkBob <1>Terms of Service", "button1": "Use my Web3 wallet", - "button2": "Use zkBob secret phrase" + "button2": "Use instant wallet" }, "restoreOptions": { "title": "How did you create your account?", diff --git a/src/locales/pt.json b/src/locales/pt.json index 12ac5214..29e6ed13 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -204,7 +204,7 @@ "title": "Escolha como você deseja criar sua conta", "description": "Ao criar uma conta zkAccount, você concorda e aceita os <1>Termos de Serviço do zkBob", "button1": "Usar minha carteira Web3", - "button2": "Usar a frase secreta do zkBob" + "button2": "Usar carteira instantânea" }, "restoreOptions": { "title": "Como você criou sua conta?", diff --git a/src/locales/ru.json b/src/locales/ru.json index 285afcb9..77fe7098 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -204,7 +204,7 @@ "title": "Выберите, как вы хотите создать свой аккаунт", "description": "Создавая zk-Аккаунт, вы соглашаетесь и принимаете <1>Условия использованияzkBob.", "button1": "Использовать мой Web3 кошелек", - "button2": "Использовать секретную фразу" + "button2": "Использовать мгновенный кошелек" }, "restoreOptions": { "title": "Как вы создали свой аккаунт?", diff --git a/src/locales/zh.json b/src/locales/zh.json index 9737e680..e9738553 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -204,7 +204,7 @@ "title": "选择您创建账户的方式", "description": "创建zk账户,即表示您同意并接受zkBob<1>服务条款", "button1": "使用Web3钱包", - "button2": "使用zkBob助记词" + "button2": "使用即时钱包" }, "restoreOptions": { "title": "您通过何种方式创建账户?", From db99b1a4970c7dc4311f3bc54b404169e09773b3 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Fri, 17 Nov 2023 16:02:41 +0100 Subject: [PATCH 29/45] change text --- src/locales/en.json | 2 +- src/locales/pt.json | 2 +- src/locales/ru.json | 2 +- src/locales/zh.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 51a8f691..be619cb6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -204,7 +204,7 @@ "title": "Choose how you would like to create your account", "description": "By creating zkAccount, you hereby agree to and accept zkBob <1>Terms of Service", "button1": "Use my Web3 wallet", - "button2": "Use instant wallet" + "button2": "Instant zkBob account" }, "restoreOptions": { "title": "How did you create your account?", diff --git a/src/locales/pt.json b/src/locales/pt.json index 29e6ed13..2a7885cb 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -204,7 +204,7 @@ "title": "Escolha como você deseja criar sua conta", "description": "Ao criar uma conta zkAccount, você concorda e aceita os <1>Termos de Serviço do zkBob", "button1": "Usar minha carteira Web3", - "button2": "Usar carteira instantânea" + "button2": "Conta instantânea zkBob" }, "restoreOptions": { "title": "Como você criou sua conta?", diff --git a/src/locales/ru.json b/src/locales/ru.json index 77fe7098..fe5984b5 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -204,7 +204,7 @@ "title": "Выберите, как вы хотите создать свой аккаунт", "description": "Создавая zk-Аккаунт, вы соглашаетесь и принимаете <1>Условия использованияzkBob.", "button1": "Использовать мой Web3 кошелек", - "button2": "Использовать мгновенный кошелек" + "button2": "Мгновенный аккаунт zkBob" }, "restoreOptions": { "title": "Как вы создали свой аккаунт?", diff --git a/src/locales/zh.json b/src/locales/zh.json index e9738553..213eedb1 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -204,7 +204,7 @@ "title": "选择您创建账户的方式", "description": "创建zk账户,即表示您同意并接受zkBob<1>服务条款", "button1": "使用Web3钱包", - "button2": "使用即时钱包" + "button2": "即时zkBob账户" }, "restoreOptions": { "title": "您通过何种方式创建账户?", From 222b5a5009dd3a3c2d613b3723b2f606d025e4b2 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Fri, 17 Nov 2023 16:59:59 +0100 Subject: [PATCH 30/45] upgrade lib --- package.json | 2 +- yarn.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index c28579ec..306f13ca 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "wagmi": "^0.12.1", "web-vitals": "^1.0.1", "webpack": "^5.70.0", - "zkbob-client-js": "5.4.0-beta4" + "zkbob-client-js": "5.4.0" }, "scripts": { "start": "react-app-rewired start", diff --git a/yarn.lock b/yarn.lock index 8ab3fe94..ad72130d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12783,15 +12783,15 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libzkbob-rs-wasm-web-mt@1.5.0-beta2: - version "1.5.0-beta2" - resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web-mt/-/libzkbob-rs-wasm-web-mt-1.5.0-beta2.tgz#fd0c3b56585ce7a8ab7630b65c5355fdab47e5c0" - integrity sha512-BVED7gg1qm4Y9bCXIiW8E98SSMv+dtR7IVBJ03i3042exyNAzlqbtynkD9lyEzhAjAB20g/Baziws7AS5v29Ag== +libzkbob-rs-wasm-web-mt@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web-mt/-/libzkbob-rs-wasm-web-mt-1.5.0.tgz#6a88fe3fdfcd0fe856fd791fa0976bdde2fea34b" + integrity sha512-2uwwE5mm32ITMvYgW3uPsPXLrPVutnpYB003wzDzMbPfF2EBjP1kh+sQwPUDmEl+ic4OSTXU/q3sdkWWmhUhRQ== -libzkbob-rs-wasm-web@1.5.0-beta2: - version "1.5.0-beta2" - resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web/-/libzkbob-rs-wasm-web-1.5.0-beta2.tgz#05d5458557de4dd3273075028d37977ce184cdbf" - integrity sha512-OVXWtebMH4Ik0YwybkvfG4e/kjLONU9gXziWBDRpiNkY9bpD0VfRFs5IVWrmYLmTGTmeAGM2bwH/sAAenpmNxw== +libzkbob-rs-wasm-web@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/libzkbob-rs-wasm-web/-/libzkbob-rs-wasm-web-1.5.0.tgz#41c2b0a51a9283a96457bd5439cfeed27f866913" + integrity sha512-QlnFMNzqjFakkIDrST4kmOdr+OikMZVZoOi2B73F/kb/3elcPSA5vAjYM/AdmB3+Ojty4ZrQW1GnsFWJrebF5w== lie@3.1.1: version "3.1.1" @@ -19015,10 +19015,10 @@ 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@5.4.0-beta4: - version "5.4.0-beta4" - resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.4.0-beta4.tgz#570c8bcdab6d4503b41ba83c7e37223b84243449" - integrity sha512-iD1kO6ay4UHjcPHnEhUw6pR836OuRq2ZhzhTSl7/keB3Yja9UxnJrRdxiYPVQ/agmIY6veppNfQco8n98HVq8w== +zkbob-client-js@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.4.0.tgz#a4eb6fc03fe56aaa83038f9fb9548fb6ea13b094" + integrity sha512-kMlqGckUsr6pYLI0S+9sZI60vUkSnNHyZQfMFcZ+BT19qNOwlXu00TON7Ro2FIYDAKwKOj98Jz2ACAvEPrHvSw== dependencies: "@ethereumjs/util" "^8.0.2" "@graphprotocol/client-cli" "3.0.0" @@ -19031,8 +19031,8 @@ zkbob-client-js@5.4.0-beta4: graphql "16.7.1" hdwallet-babyjub "^0.0.2" idb "^7.0.0" - libzkbob-rs-wasm-web "1.5.0-beta2" - libzkbob-rs-wasm-web-mt "1.5.0-beta2" + libzkbob-rs-wasm-web "1.5.0" + libzkbob-rs-wasm-web-mt "1.5.0" promise-retry "^2.0.1" promise-throttle "^1.1.2" regenerator-runtime "^0.13.9" From cf2e74fea969d898a762f97b3f952e1a6b89d8d0 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Sun, 19 Nov 2023 18:17:46 +0100 Subject: [PATCH 31/45] remove pool on shasta --- src/config/index.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index 5f42a3b7..941dac35 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -163,21 +163,6 @@ const config = { ddSubgraph: 'zkbob-eth-goerli', addressPrefix: 'zkbob_goerli_eth', }, - 'USDT-shasta': { - chainId: 2494104990, - poolAddress: 'TLTyi81NhoeGfsq8Ef1STDYs6E7HFSAruV', - tokenAddress: 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs', - relayerUrls: ['https://shasta-relayer.thgkjlr.website'], - delegatedProverUrls: [], - coldStorageConfigPath: '', - tokenSymbol: 'USDT', - tokenDecimals: 6, - feeDecimals: 2, - depositScheme: 'approve', - minTxAmount: 50000n, // 0.05 USDT - addressPrefix: 'zkbob_shasta', - isTron: true, - }, 'USDT-nile': { chainId: 3448148188, poolAddress: 'TT8GgygLhEDh88kYwY8mYz38iEpJWw1YLG', From d14fe40e6ba45489a90e904b768c0506a80cb2ce Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Sun, 19 Nov 2023 18:20:59 +0100 Subject: [PATCH 32/45] remove network shasta --- src/config/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index 941dac35..ad015a0e 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -189,9 +189,6 @@ const config = { '420': { rpcUrls: ['https://goerli.optimism.io'] }, - '2494104990': { - rpcUrls: ['https://api.shasta.trongrid.io'] - }, '3448148188': { rpcUrls: ['https://nile.trongrid.io'] }, From 59fe628a615aeb06f4a8e873bc4556b823973e39 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Tue, 21 Nov 2023 17:51:59 +0100 Subject: [PATCH 33/45] fix ru locale --- src/locales/ru.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/locales/ru.json b/src/locales/ru.json index fe5984b5..a89a0864 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -5,7 +5,7 @@ "fee": "Комиссия", "copied": "Скопировано!", "balance": "Баланс", - "poolBalance": "Баланс пула", + "poolBalance": "Баланс в пуле", "supportId": "Идентификатор поддержки", "web": "Web", "library": "Библиотека", @@ -85,7 +85,7 @@ "title": "Мультиперевод", "instruction": "Добавьте zk-Адрес, сумму {{symbol}} для перевода. По одному адресу на строку.", "uploadCSV": "Загрузить CSV файл", - "uploadCSVHint": "Щелкните Загрузить CSV файл, чтобы добавить заранее подготовленный файл .csv с вашего устройства. Каждая строка должна содержать: zk-Адрес, сумму", + "uploadCSVHint": "Нажмите Загрузить CSV файл, чтобы добавить заранее подготовленный файл .csv с вашего устройства. Каждая строка должна содержать: zk-Адрес, сумму", "errors": { "syntax": "{{count}} $t(glossary.rows, {\"count\": {{count}} }) с неправильным $t(glossary.addresses, {\"count\": {{count}} }) или форматированием $t(glossary.issues, {\"count\": {{count}} }).", "duplicates": "Найдены дублирующиеся адреса.", @@ -111,7 +111,7 @@ "withdraw": { "daily": "Лимит для вывода средств в день" }, - "poolSize": "Размера пула", + "poolSize": "Размер пула", "total": "из {{amount}} {{symbol}} всего" }, "latestAction": { @@ -184,8 +184,8 @@ "description": "Проверьте вкладку История или баланс вашего zk-Аккаунта, чтобы получить больше информации." }, "pendingAction": { - "title": "Пожалуйста, подождите, пока ваша $t(glossary.transactions, {\"count\": {{count}} })завершится", - "description": "Не обновляйте страницу в течение минимум 30 секунд! Статус транзакции $t(glossary.statuses, {\"count\": {{count}} }) обновится автоматически.", + "title": "Пожалуйста, подождите, пока ваша транзакция завершится", + "description": "Не обновляйте страницу в течение минимум 30 секунд! Статус транзакции обновится автоматически.", "note": "Вы можете пополнять аккаунт, переводить или выводить средства после завершения транзакции.", "note_other": "Вы можете пополнять аккаунт, переводить или выводить средства после завершения транзакций." }, @@ -233,7 +233,7 @@ }, "confirmSecret": { "title": "Подтвердите секретную фразу", - "description": "Пожалуйста, введите вашу секретную фразу, чтобы восстановить zk-Аккаунт.
Щелкните по слову, чтобы удалить его" + "description": "Пожалуйста, введите вашу секретную фразу, чтобы восстановить zk-Аккаунт.
Нажмите на слово, чтобы удалить его" }, "signMessageToCreate": { "title": "Подпишите сообщение для создания вашего zk-Аккаунта", @@ -289,7 +289,7 @@ "paragraph1": "Для увеличения личных лимитов депозита вам нужно иметь токен BAB на BNB Smart Chain. Что такое токен BAB? <1>Узнайте прямо сейчас.", "paragraph2": "Верифицируйте свой BAB:", "step1": "Подключите кошелек Metamask или WalletConnect, содержащий ваш токен BAB, к zkBob. Вам НЕ нужно переключать сети, оставайтесь на Polygon.", - "step2": "Щелкните «Подтвердить мой токен BAB» ниже.", + "step2": "Нажмите «Подтвердить мой токен BAB» ниже.", "paragraph3": "Вот и все. Ваши лимиты депозита автоматически увеличатся, если ваш подключенный кошелек содержит токен BAB.", "button": "Верифицировать мой токен BAB" }, @@ -365,9 +365,9 @@ "generatingProof": "Генерация доказательства", "waitingForRelayer": "Ожидание релеера", "deposited": "Пополнение кошелька в процессе", - "transferred": "ОТправка средств в процессе", + "transferred": "Отправка средств в процессе", "transferredMulti": "Мультитрансфер в процессе", - "withdrawn": "Вывод средст в процессе", + "withdrawn": "Вывод средств в процессе", "rejected": "Транзакция отклонена", "signatureExpired": "Срок подписи истек", "suspiciousAccountDeposit": "Подключен подозрительный кошелек", From 586ddfa058f6403f40156aef5cabcd704fb89002 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Wed, 29 Nov 2023 19:22:01 +0100 Subject: [PATCH 34/45] strict approval amount --- src/hooks/useApproval.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useApproval.js b/src/hooks/useApproval.js index 131bccd5..736179fc 100644 --- a/src/hooks/useApproval.js +++ b/src/hooks/useApproval.js @@ -47,7 +47,7 @@ export default (pool, tokenAddress, amount, balance, type = 'permit2') => { } } setTxStatus(TX_STATUSES.APPROVE_TOKENS); - const tx = await callContract(tokenAddress, tokenAbi, 'approve', [contractForApproval, ethers.constants.MaxUint256], true); + const tx = await callContract(tokenAddress, tokenAbi, 'approve', [contractForApproval, amount], true); setTxStatus(TX_STATUSES.WAITING_FOR_TRANSACTION); await waitForTx(tx); closeTxModal(); @@ -63,7 +63,7 @@ export default (pool, tokenAddress, amount, balance, type = 'permit2') => { }, [ openTxModal, setTxStatus, setTxError, switchNetwork, chain, waitForTx, updateAllowance, pool.chainId, tokenAddress, closeTxModal, - callContract, contractForApproval, + callContract, contractForApproval, amount, ]); return { isApproved, approve, updateAllowance }; From eb7d95521c4c2d34af1e2ee74391aa07d6abfc92 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Wed, 29 Nov 2023 19:25:54 +0100 Subject: [PATCH 35/45] upgrade lib (fix address format in history) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 306f13ca..d9b120be 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "wagmi": "^0.12.1", "web-vitals": "^1.0.1", "webpack": "^5.70.0", - "zkbob-client-js": "5.4.0" + "zkbob-client-js": "5.4.1-beta" }, "scripts": { "start": "react-app-rewired start", diff --git a/yarn.lock b/yarn.lock index ad72130d..eec0e433 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19015,10 +19015,10 @@ 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@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.4.0.tgz#a4eb6fc03fe56aaa83038f9fb9548fb6ea13b094" - integrity sha512-kMlqGckUsr6pYLI0S+9sZI60vUkSnNHyZQfMFcZ+BT19qNOwlXu00TON7Ro2FIYDAKwKOj98Jz2ACAvEPrHvSw== +zkbob-client-js@5.4.1-beta: + version "5.4.1-beta" + resolved "https://registry.yarnpkg.com/zkbob-client-js/-/zkbob-client-js-5.4.1-beta.tgz#b7e9eee2e37835ed96ddedd69c5e7391f1ebae28" + integrity sha512-+shx8Adt9qsPQMuBQMXW891okxOeQmHcqqpc+VONhk26xgTCtGP076Zuh2Yw0loCavjBH6OG0GdSvgWI0m7YEA== dependencies: "@ethereumjs/util" "^8.0.2" "@graphprotocol/client-cli" "3.0.0" From 7a6b182551892035317641e408db47bf5ea4e542 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Mon, 11 Dec 2023 14:24:18 +0100 Subject: [PATCH 36/45] disable 2 pools on staging --- src/config/index.js | 62 ++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index ad015a0e..d5b66473 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -84,25 +84,25 @@ const config = { }, }, dev: { - defaultPool: 'BOB-sepolia', + defaultPool: 'USDC-goerli', 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=BABTokenBOB', - homepage: 'https://stage.knowyourcat.id/address/%s/BABTokenBOB', - }, - tokenSymbol: 'BOB', - tokenDecimals: 18, - feeDecimals: 2, - depositScheme: 'permit', - addressPrefix: 'zkbob_sepolia', - }, + // '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=BABTokenBOB', + // homepage: 'https://stage.knowyourcat.id/address/%s/BABTokenBOB', + // }, + // tokenSymbol: 'BOB', + // tokenDecimals: 18, + // feeDecimals: 2, + // depositScheme: 'permit', + // addressPrefix: 'zkbob_sepolia', + // }, 'BOB2USDC-goerli': { chainId: 5, poolAddress: '0x49661694a71B3Dab9F25E86D5df2809B170c56E6', @@ -135,19 +135,19 @@ const config = { minTxAmount: 50000n, // 0.05 USDC addressPrefix: 'zkbob_goerli_usdc', }, - 'BOB-op-goerli': { - chainId: 420, - poolAddress:'0x55B81b0730399974Ccad8AC858e766Cf54126596', - tokenAddress:'0x0fA7E69b9344D6434Bd6b79c5950bb5234245a5F', - relayerUrls:['https://gop-relayer.thgkjlr.website'], - delegatedProverUrls: [], - coldStorageConfigPath: '', - tokenSymbol: 'BOB', - tokenDecimals: 18, - feeDecimals: 2, - depositScheme: 'permit', - addressPrefix: 'zkbob_goerli_optimism', - }, + // 'BOB-op-goerli': { + // chainId: 420, + // poolAddress:'0x55B81b0730399974Ccad8AC858e766Cf54126596', + // tokenAddress:'0x0fA7E69b9344D6434Bd6b79c5950bb5234245a5F', + // relayerUrls:['https://gop-relayer.thgkjlr.website'], + // delegatedProverUrls: [], + // coldStorageConfigPath: '', + // tokenSymbol: 'BOB', + // tokenDecimals: 18, + // feeDecimals: 2, + // depositScheme: 'permit', + // addressPrefix: 'zkbob_goerli_optimism', + // }, 'WETH-goerli': { chainId: 5, poolAddress:'0xf9dbCF4005497e042838dE9082C817fCa790e945', From 984c1bba551ca9b45f3429ec69e1f3265722ca48 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Wed, 20 Dec 2023 14:08:48 +0100 Subject: [PATCH 37/45] add new tron pool --- package.json | 2 +- src/config/index.js | 22 ++++++++++++++++++++++ src/constants/index.js | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d9b120be..a769297c 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "wagmi": "^0.12.1", "web-vitals": "^1.0.1", "webpack": "^5.70.0", - "zkbob-client-js": "5.4.1-beta" + "zkbob-client-js": "5.5.0-beta1" }, "scripts": { "start": "react-app-rewired start", diff --git a/src/config/index.js b/src/config/index.js index d5b66473..629f50bd 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -178,6 +178,21 @@ const config = { addressPrefix: 'zkbob_nile', isTron: true, }, + 'USDT-nile-guard': { + chainId: 3448148188, + poolAddress: 'TJp4J3M2rEZ5euswTpGzUaVVEJxkXX66mL', + tokenAddress: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', + relayerUrls: ['https://tron-nile-mpc-relayer.thgkjlr.website'], + delegatedProverUrls: [], + coldStorageConfigPath: '', + tokenSymbol: 'USDT*', + tokenDecimals: 6, + feeDecimals: 2, + depositScheme: 'approve', + minTxAmount: 50000n, // 0.05 USDT + addressPrefix: 'zkbob_nile_g', + isTron: true, + }, }, chains: { '11155111': { @@ -197,6 +212,13 @@ const config = { transferParamsUrl: 'https://r2-staging.zkbob.com/transfer_params_20022023.bin', transferVkUrl: 'https://r2-staging.zkbob.com/transfer_verification_key_20022023.json' }, + extraPrefixes: [ + { + poolId: 16776968, + prefix: 'zkbob_nile_g', + name: 'USDT on Nile testnet (MPC guard contracts)', + }, + ], } }; diff --git a/src/constants/index.js b/src/constants/index.js index f3360a05..d7537a65 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -86,6 +86,7 @@ export const TOKENS_ICONS = { 'USDC': require('assets/usdc.svg').default, 'USDC.e': require('assets/usdc.svg').default, 'USDT': require('assets/usdt.png'), + 'USDT*': require('assets/usdt.png'), }; export const CONNECTORS_ICONS = { From faf80a9f53cae812a40633127158ad1ccc8b5c16 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Wed, 20 Dec 2023 17:40:18 +0100 Subject: [PATCH 38/45] fix lib init --- src/contexts/ZkAccountContext/zp.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contexts/ZkAccountContext/zp.js b/src/contexts/ZkAccountContext/zp.js index a8f9f139..d69ff9ae 100644 --- a/src/contexts/ZkAccountContext/zp.js +++ b/src/contexts/ZkAccountContext/zp.js @@ -12,6 +12,7 @@ const createClient = (currentPoolAlias, supportId, callback) => { pools: config.pools, chains: config.chains, snarkParams: config.snarkParams, + extraPrefixes: config.extraPrefixes, supportId, }, currentPoolAlias, From 7b4ed7dc3a833801132d07e2bdd9692578469e4d Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Wed, 27 Dec 2023 16:26:07 +0100 Subject: [PATCH 39/45] change pool address --- src/config/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/index.js b/src/config/index.js index 629f50bd..13b2610e 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -180,7 +180,7 @@ const config = { }, 'USDT-nile-guard': { chainId: 3448148188, - poolAddress: 'TJp4J3M2rEZ5euswTpGzUaVVEJxkXX66mL', + poolAddress: 'TQyuLqz9xWieAGNPi2tzQvnRYD6U98XLP2', tokenAddress: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', relayerUrls: ['https://tron-nile-mpc-relayer.thgkjlr.website'], delegatedProverUrls: [], From 4ff85d2705cd96317aaaeaf7c919a48c4f4e82ba Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Wed, 27 Dec 2023 16:40:35 +0100 Subject: [PATCH 40/45] fix error check --- src/contexts/ZkAccountContext/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contexts/ZkAccountContext/index.js b/src/contexts/ZkAccountContext/index.js index f3e44dd8..f802015f 100644 --- a/src/contexts/ZkAccountContext/index.js +++ b/src/contexts/ZkAccountContext/index.js @@ -433,7 +433,7 @@ export const ZkAccountContextProvider = ({ children }) => { try { directDepositFee = await fromShieldedAmount(await zkClient.directDepositFee()); } catch (error) { - if (!error.message.includes('No direct deposit processer initialized')) throw error; + if (!error?.message?.includes('No direct deposit processer initialized')) throw error; } if (!zkAccount) { let atomicTxFee = await zkClient.atomicTxFee(txType); From 7a213c1ce9e1ef608bc65889095fe37c5b6ebf7e Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 28 Dec 2023 01:39:42 +0100 Subject: [PATCH 41/45] change pool address --- src/config/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/index.js b/src/config/index.js index 13b2610e..a481916e 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -180,7 +180,7 @@ const config = { }, 'USDT-nile-guard': { chainId: 3448148188, - poolAddress: 'TQyuLqz9xWieAGNPi2tzQvnRYD6U98XLP2', + poolAddress: 'TJRZmRFoUn1NWQ4p7XGVW1UJr8bXtFCrGz', tokenAddress: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', relayerUrls: ['https://tron-nile-mpc-relayer.thgkjlr.website'], delegatedProverUrls: [], From 2f7b0296074f8ed644efa36e35f33c1bb26c2824 Mon Sep 17 00:00:00 2001 From: Max Alekseenko Date: Thu, 28 Dec 2023 10:02:36 +0100 Subject: [PATCH 42/45] change pool address --- src/config/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/index.js b/src/config/index.js index a481916e..28c03e1e 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -180,7 +180,7 @@ const config = { }, 'USDT-nile-guard': { chainId: 3448148188, - poolAddress: 'TJRZmRFoUn1NWQ4p7XGVW1UJr8bXtFCrGz', + poolAddress: 'TVbFjwMgDuzVYqTmMMzrkGQxZkaTfDZ1Gn', tokenAddress: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', relayerUrls: ['https://tron-nile-mpc-relayer.thgkjlr.website'], delegatedProverUrls: [], From 9e3baf7dde04c1536a76ce670634582c1ee48fc3 Mon Sep 17 00:00:00 2001 From: r0wdy1 <103738251+r0wdy1@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:45:38 +0300 Subject: [PATCH 43/45] Feature/external links (#257) * allows adding external links --- src/components/NetworkDropdown/index.js | 11 ++++++++--- src/config/index.js | 23 ++++++++++++++++++++++- src/constants/index.js | 8 ++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/components/NetworkDropdown/index.js b/src/components/NetworkDropdown/index.js index d2f129f0..90cc2313 100644 --- a/src/components/NetworkDropdown/index.js +++ b/src/components/NetworkDropdown/index.js @@ -23,6 +23,7 @@ const poolsByChainId = chainIds.map(chainId => { return { chainId, pools: Object.values(poolsWithAliases).filter(pool => pool.chainId === chainId), + external: Object.entries(config.chains).find(([k,_]) => Number(k) === chainId)[1]["external"] }; }); @@ -30,7 +31,11 @@ const Content = ({ switchToPool, currentPool, close }) => { const { t } = useTranslation(); const [openedChainId, setOpenedChainId] = useState(currentPool.chainId); - const showPools = useCallback(chainId => { + const showPools = useCallback((chainId,external) => { + if(external) { + window.open(external); + return; + } if (openedChainId === chainId) { setOpenedChainId(null); } else { @@ -46,10 +51,10 @@ const Content = ({ switchToPool, currentPool, close }) => { return ( {t('networks.title')} - {poolsByChainId.map(({ chainId, pools }, index) => + {poolsByChainId.map(({ chainId, pools,external }, index) => showPools(chainId)} + onClick={() => showPools(chainId,external)} className={openedChainId === chainId ? 'active' : ''} > diff --git a/src/config/index.js b/src/config/index.js index 28c03e1e..b8f6ecaf 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -66,6 +66,21 @@ const config = { addressPrefix: 'zkbob_optimism_eth', paymentContractAddress: '0x7a8006Ea0Dda93C56E60187Bd55109AbfF486c6F', }, + 'USDT-tron': { + chainId: 728126428, + poolAddress: 'TXViaNRhEugXpAZApviBqBnbTSKUgejnR9', + tokenAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + relayerUrls: ['https://relayer-tron-mpc.zkbob.com'], + delegatedProverUrls: [], + coldStorageConfigPath: '', + tokenSymbol: 'USDT', + tokenDecimals: 6, + feeDecimals: 2, + depositScheme: 'approve', + minTxAmount: 50000n, // 0.05 USDT + addressPrefix: 'zkbob_tron', + isTron: true, + }, }, chains: { '137': { @@ -74,6 +89,11 @@ const config = { '10': { rpcUrls: ['https://rpc.ankr.com/optimism'], }, + + '728126428': { + rpcUrls: ['https://few-methodical-breeze.tron-mainnet.quiknode.pro/c9e0de7204463ff25a6ca3afd1bd32caf880561e', 'https://api.trongrid.io'], + external: "https://tron.zkbob.com/" + }, }, snarkParams: process.env.REACT_APP_HOSTING === 'netlify' ? { transferParamsUrl: 'https://r2.zkbob.com/transfer_params_22022023.bin', @@ -205,7 +225,8 @@ const config = { rpcUrls: ['https://goerli.optimism.io'] }, '3448148188': { - rpcUrls: ['https://nile.trongrid.io'] + rpcUrls: ['https://nile.trongrid.io'], + external: "https://deploy-preview-250--shimmering-douhua-023cc6.netlify.app" }, }, snarkParams: { diff --git a/src/constants/index.js b/src/constants/index.js index d7537a65..e1346dfe 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -75,6 +75,14 @@ export const NETWORKS = { address: 'https://nile.tronscan.org/#/address/%s', tx: 'https://nile.tronscan.org/#/transaction/%s', }, + }, + 728126428: { + name: 'Tron', + icon: require('assets/tron.png'), + blockExplorerUrls: { + address: 'https://tronscan.org/#/address/%s', + tx: 'https://tronscan.org/#/transaction/%s', + }, } }; From 917d203692f2494671f6252a506674b03d8ec5d4 Mon Sep 17 00:00:00 2001 From: vladimir Date: Thu, 1 Feb 2024 18:42:56 +0300 Subject: [PATCH 44/45] bump client lib, update version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a769297c..ecd558df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zkbob-ui", - "version": "3.4.0", + "version": "3.5.0", "private": true, "dependencies": { "@dicebear/avatars": "^4.10.2", @@ -54,7 +54,7 @@ "wagmi": "^0.12.1", "web-vitals": "^1.0.1", "webpack": "^5.70.0", - "zkbob-client-js": "5.5.0-beta1" + "zkbob-client-js": "5.5.0" }, "scripts": { "start": "react-app-rewired start", From b7efe5946b6dd2800a9cc48bdba26b5c8f50fa48 Mon Sep 17 00:00:00 2001 From: vladimir Date: Thu, 1 Feb 2024 20:06:22 +0300 Subject: [PATCH 45/45] hot fix --- src/pages/Payment/index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/Payment/index.js b/src/pages/Payment/index.js index 75244e88..a54e4106 100644 --- a/src/pages/Payment/index.js +++ b/src/pages/Payment/index.js @@ -42,11 +42,7 @@ const Payment = ({ pool }) => { const { supportId } = useContext(SupportIdContext); const history = useHistory(); const params = useParams(); - const addressPrefix = params.address.split(':')[0]; - const pool = Object.values(pools).find(pool => pool.addressPrefix === addressPrefix); - if (!pool.paymentContractAddress) { - history.push('/'); - } + const currency = ['USDC.e', 'USDC', 'BOB'].includes(pool.tokenSymbol) ? 'USD' : pool.tokenSymbol; const { address: account } = useContext(WalletContext); const [displayedAmount, setDisplayedAmount] = useState('');